diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b0c48605..e5d8caad 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -2,10 +2,10 @@ "version": 1, "isRoot": true, "tools": { - "signclient": { - "version": "1.3.155", + "sign": { + "version": "0.9.1-beta.23530.1", "commands": [ - "SignClient" + "sign" ] } } diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 378b3123..e5eb2a2a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,5 +1,5 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.177.0/containers/dotnet/.devcontainer/base.Dockerfile # [Choice] .NET version: 7.0, 6.0, 5.0 -ARG VARIANT="6.0" +ARG VARIANT="8.0" FROM mcr.microsoft.com/vscode/devcontainers/dotnet \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c40d7ef5..0ee6f006 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,7 +20,7 @@ "remoteEnv": { "PATH": "${containerWorkspaceFolder}/.dotnet:${containerEnv:PATH}", "DOTNET_MULTILEVEL_LOOKUP": "0", - "TARGET": "net6.0", + "TARGET": "net8.0", "DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER": "true" }, // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. diff --git a/.editorconfig b/.editorconfig index fe5a4a9e..68505e1c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,8 @@ root = true # don't use tabs for indentation [*] indent_style = space +vsspell_section_id = 41b65011239a40959ccaae2a4ec7044a +vsspell_ignored_words_41b65011239a40959ccaae2a4ec7044a = Accessor|app|clr|Edm|inline|middleware|Mvc|odata|Validator|Deconstruct # code files [*.{cs,csx,vb,vbx}] @@ -88,8 +90,8 @@ csharp_space_between_method_declaration_parameter_list_parentheses = true csharp_space_between_method_call_parameter_list_parentheses = true csharp_space_between_parentheses = control_flow_statements, expressions -# ide code suppressions -# dotnet_diagnostic.IDE0079.severity = none +# primary construcrtors +csharp_style_prefer_primary_constructors = false:none # style code suppressions dotnet_diagnostic.SA1002.severity = none @@ -110,6 +112,13 @@ dotnet_diagnostic.SA1502.severity = none dotnet_diagnostic.SA1516.severity = none dotnet_diagnostic.SA1600.severity = none +# TEMP: currently suppressed rules due to false positives +# REF: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 +# REF: https://github.com/dotnet/aspnetcore/issues/52556 +dotnet_diagnostic.SA1010.severity = none # Opening square brackets should be spaced correctly +dotnet_diagnostic.ASP0022.severity = none # Route conflict detected between route handlers +dotnet_diagnostic.ASP0023.severity = none # Route conflict detected between route handlers + # test settings # Default severity for analyzer diagnostics with category 'Reliability' @@ -128,6 +137,7 @@ dotnet_diagnostic.CA1707.severity = none dotnet_diagnostic.CA1711.severity = none dotnet_diagnostic.CA1716.severity = none dotnet_diagnostic.CA1806.severity = none +dotnet_diagnostic.CA1861.severity = none dotnet_diagnostic.CA2007.severity = none dotnet_diagnostic.CA2234.severity = none dotnet_code_quality.CA2000.excluded_symbol_names = HttpRequestMessage|HttpResponseMessage|HttpConfiguration|HttpRouteCollection|HostedHttpRouteCollection|HttpServer|HttpClient @@ -138,6 +148,13 @@ dotnet_diagnostic.SA1300.severity = none dotnet_diagnostic.SA1507.severity = none dotnet_diagnostic.SA1601.severity = none +# TEMP: currently suppressed rules due to false positives +# REF: https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 +# REF: https://github.com/dotnet/aspnetcore/issues/52556 +dotnet_diagnostic.SA1010.severity = none # Opening square brackets should be spaced correctly +dotnet_diagnostic.ASP0022.severity = none # Route conflict detected between route handlers +dotnet_diagnostic.ASP0023.severity = none # Route conflict detected between route handlers + # test methods should use all lowercase characters dotnet_naming_symbols.test_methods.applicable_kinds = method dotnet_naming_symbols.test_methods.applicable_accessibilities = public @@ -180,4 +197,4 @@ dotnet_naming_style.test_methods.word_separator = _ dotnet_naming_rule.test_methods.style = test_methods dotnet_naming_rule.test_methods.symbols = test_methods -dotnet_naming_rule.test_methods.severity = error \ No newline at end of file +dotnet_naming_rule.test_methods.severity = error diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6978ee55..14290721 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,20 +14,4 @@ updates: - "commonsensesoftware" commit-message: prefix: "[main] " - include: scope - - # only servicing 5.x release - - package-ecosystem: "nuget" - directory: "/" - target-branch: "release/5.0" - schedule: - interval: "monthly" - allow: - - dependency-type: "all" - assignees: - - "commonsensesoftware" - reviewers: - - "commonsensesoftware" - commit-message: - prefix: "[release/5.0] " include: scope \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index f9b40dff..ec3be18c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,8 +29,12 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 + id: installdotnet with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x + + - name: Create temporary global.json + run: echo '{"sdk":{"version":"${{ steps.installdotnet.outputs.dotnet-version }}"}}' > ./global.json # build a temporary *.slnf file that only contains source projects and put it in ~/obj # so that it is not tracked by git. then run 'dotnet build' using the *.slnf, which diff --git a/.vscode/settings.json b/.vscode/settings.json index 4bce8bd4..86a81124 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,10 +26,10 @@ "dotnet-test-explorer.runInParallel": true, "dotnet-test-explorer.testProjectPath": "**/*Tests.csproj", "editor.formatOnType": true, - "omnisharp.enableImportCompletion": true, "omnisharp.enableRoslynAnalyzers": true, "omnisharp.organizeImportsOnFormat": true, "omnisharp.useModernNet": true, "omnisharp.enableMsBuildLoadProjectsOnDemand": true, - "omnisharp.enableEditorConfigSupport": true + "omnisharp.enableEditorConfigSupport": true, + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true } \ No newline at end of file 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** diff --git a/asp.sln b/asp.sln index 57868d94..0df9e4ed 100644 --- a/asp.sln +++ b/asp.sln @@ -26,9 +26,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Abstractions", "Abstractions", "{7B0FA6C2-47BA-4C34-90E0-B75DF44F2124}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNet", "AspNet", "{34A0373F-12C9-44A8-9A1C-5EEE7218C877}" - ProjectSection(SolutionItems) = preProject - src\AspNet\Directory.Build.targets = src\AspNet\Directory.Build.targets - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AspNetCore", "AspNetCore", "{EBC9F217-E8BC-4DCE-9C67-F22150959EAF}" EndProject diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a9aa3dc4..f75e041a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -2,6 +2,7 @@ trigger: branches: include: - main + - release/* paths: exclude: - .config @@ -16,6 +17,7 @@ trigger: pr: - main +- release/* # build at least once a month so the build badge is up-to-date schedules: diff --git a/build/nuget.props b/build/nuget.props index c913c57b..f679d6d3 100644 --- a/build/nuget.props +++ b/build/nuget.props @@ -26,16 +26,17 @@ + true + snupkg true true - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true - + diff --git a/build/signing.json b/build/signing.json deleted file mode 100644 index 3276a45d..00000000 --- a/build/signing.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "SignClient": { - "AzureAd": { - "AADInstance": "https://login.microsoftonline.com/", - "ClientId": "c248d68a-ba6f-4aa9-8a68-71fe872063f8", - "TenantId": "16076fdc-fcc1-4a15-b1ca-32c9a255900e" - }, - "Service": { - "Url": "https://codesign.dotnetfoundation.org/", - "ResourceId": "https://SignService/3c30251f-36f3-490b-a955-520addb85001" - } - } -} \ No newline at end of file diff --git a/build/steps-ci.yml b/build/steps-ci.yml index 7ec23ad6..c2304d7d 100644 --- a/build/steps-ci.yml +++ b/build/steps-ci.yml @@ -11,7 +11,7 @@ steps: displayName: Install .NET SDK inputs: packageType: sdk - version: 7.0.x # https://github.com/dotnet/core/blob/main/release-notes/releases-index.json + version: 8.0.x # https://github.com/dotnet/core/blob/main/release-notes/releases-index.json - task: DotNetCoreCLI@2 displayName: Build and Test diff --git a/build/steps-release.yml b/build/steps-release.yml index 60fd3c3a..ca070468 100644 --- a/build/steps-release.yml +++ b/build/steps-release.yml @@ -16,41 +16,32 @@ steps: command: pack projects: ${{ parameters.solution }} ${{ if eq(parameters.versionSuffix, '') }}: - arguments: --configuration ${{ parameters.configuration }} + arguments: --no-build --configuration ${{ parameters.configuration }} ${{ else }}: - arguments: --configuration ${{ parameters.configuration }} --version-suffix ${{ parameters.versionSuffix }} + arguments: --no-build --configuration ${{ parameters.configuration }} --version-suffix ${{ parameters.versionSuffix }} outputDir: $(Build.ArtifactStagingDirectory)/packages noBuild: true - script: dotnet tool restore displayName: Restore Tools -- pwsh: > - Compress-Archive - -Path $(Build.ArtifactStagingDirectory)/packages/* - -DestinationPath $(Build.ArtifactStagingDirectory)/packages.zip - displayName: Package Artifacts for Signing - - script: > - dotnet signclient sign - --config build/signing.json - --input $(Build.ArtifactStagingDirectory)/packages.zip - --user "$(codesign_user)" - --secret "$(codesign_secret)" - --name "ASP.NET API Versioning" + dotnet sign code azure-key-vault "*.nupkg" + --base-directory "$(Build.ArtifactStagingDirectory)/packages" + --publisher-name "ASP.NET API Versioning" --description "Adds versioning semantics to APIs built with ASP.NET" - --descriptionUrl "https://github.com/dotnet/aspnet-api-versioning" + --description-url "https://github.com/dotnet/aspnet-api-versioning" + --azure-key-vault-tenant-id "$(SignTenantId)" + --azure-key-vault-client-id "$(SignClientId)" + --azure-key-vault-client-secret "$(SignClientSecret)" + --azure-key-vault-certificate "$(SignKeyVaultCertificate)" + --azure-key-vault-url "$(SignKeyVaultUrl)" + --timestamp-url http://timestamp.digicert.com displayName: Sign Artifacts -- pwsh: > - Expand-Archive - -Path $(Build.ArtifactStagingDirectory)/packages.zip - -DestinationPath $(Build.ArtifactStagingDirectory)/signed-packages - displayName: Extract Signed Artifacts - - task: PublishBuildArtifacts@1 - displayName: Publish package artifacts + displayName: Publish Artifacts inputs: - pathToPublish: $(Build.ArtifactStagingDirectory)/signed-packages + pathToPublish: $(Build.ArtifactStagingDirectory)/packages publishLocation: Container artifactName: NuGet Packages \ No newline at end of file diff --git a/build/test.targets b/build/test.targets index 286e53f6..a3fc5fea 100644 --- a/build/test.targets +++ b/build/test.targets @@ -3,8 +3,13 @@ 6.8.0 - 4.18.2 - 2.4.5 + + + 4.20.69 + 2.5.0 @@ -15,14 +20,14 @@ - + - + - + diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/PersonModelConfiguration.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/PersonModelConfiguration.cs index 86759c34..aa3c7000 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/PersonModelConfiguration.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/PersonModelConfiguration.cs @@ -18,6 +18,7 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout person.HasKey( p => p.Id ); person.Select().OrderBy( "firstName", "lastName" ); + person.Page( maxTopValue: 100, pageSizeValue: default ); if ( apiVersion < ApiVersions.V3 ) { diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/ProductConfiguration.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/ProductConfiguration.cs index b6c151f0..edc33de9 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/ProductConfiguration.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/ProductConfiguration.cs @@ -18,8 +18,10 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout return; } - var product = builder.EntitySet( "Products" ).EntityType.HasKey( p => p.Id ); + var product = builder.EntitySet( "Products" ).EntityType; + product.HasKey( p => p.Id ); + product.Page( maxTopValue: 100, pageSizeValue: default ); product.Action( "Rate" ).Parameter( "stars" ); product.Collection.Action( "Rate" ).Parameter( "stars" ); } diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/SupplierConfiguration.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/SupplierConfiguration.cs index f6e2f817..b1f91df2 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/SupplierConfiguration.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/Configuration/SupplierConfiguration.cs @@ -18,7 +18,11 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout return; } - builder.EntitySet( "Suppliers" ).EntityType.HasKey( p => p.Id ); + var supplier = builder.EntitySet( "Suppliers" ).EntityType; + + supplier.HasKey( p => p.Id ); + supplier.Page( maxTopValue: 100, pageSizeValue: default ); + builder.Singleton( "Acme" ); } } \ No newline at end of file diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/Models/Order.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/Models/Order.cs index 6ed326dc..e3f15cc4 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/Models/Order.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/Models/Order.cs @@ -7,6 +7,7 @@ /// /// Represents an order. /// +[Page( MaxTop = 100 )] [Select] [Select( "effectiveDate", SelectType = SelectExpandType.Disabled )] public class Order diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/Startup.cs index e867f18d..2577d01c 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; @@ -118,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() ) @@ -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/OpenApiODataWebApiExample/SwaggerDefaultValues.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/SwaggerDefaultValues.cs index 67cd7607..2ce56dcf 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/SwaggerDefaultValues.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/SwaggerDefaultValues.cs @@ -30,17 +30,11 @@ public void Apply( Operation operation, SchemaRegistry schemaRegistry, ApiDescri var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.name ); // REF: https://github.com/domaindrivendev/Swashbuckle/issues/1101 - if ( parameter.description == null ) - { - parameter.description = description.Documentation; - } + parameter.description ??= description.Documentation; // REF: https://github.com/domaindrivendev/Swashbuckle/issues/1089 // REF: https://github.com/domaindrivendev/Swashbuckle/pull/1090 - if ( parameter.@default == null ) - { - parameter.@default = description.ParameterDescriptor?.DefaultValue; - } + parameter.@default ??= description.ParameterDescriptor?.DefaultValue; } } } \ No newline at end of file diff --git a/examples/AspNet/OData/OpenApiODataWebApiExample/V3/SuppliersController.cs b/examples/AspNet/OData/OpenApiODataWebApiExample/V3/SuppliersController.cs index 6e1412da..af837960 100644 --- a/examples/AspNet/OData/OpenApiODataWebApiExample/V3/SuppliersController.cs +++ b/examples/AspNet/OData/OpenApiODataWebApiExample/V3/SuppliersController.cs @@ -118,7 +118,7 @@ public IHttpActionResult Put( [FromODataUri] int key, [FromBody] Supplier update /// /// The supplier identifier. /// The associated supplier products. - [EnableQuery] + [EnableQuery( MaxTop = 100 )] public IQueryable GetProducts( [FromODataUri] int key ) => suppliers.Where( s => s.Id == key ).SelectMany( s => s.Products ); diff --git a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Book.cs b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Book.cs index bd9fae58..a0a736b0 100644 --- a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Book.cs +++ b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Book.cs @@ -1,8 +1,15 @@ namespace ApiVersioning.Examples; +using Microsoft.AspNet.OData.Query; + +// TODO: Model Bound settings can be performed via attributes if the +// return type is known to the API Explorer or can be explicitly done +// via one or more IModelConfiguration implementations + /// /// Represents a book. /// +[Filter( "author", "published" )] public class Book { /// diff --git a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/BooksController.cs b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/BooksController.cs index 05dbf26b..6017627b 100644 --- a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/BooksController.cs +++ b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/BooksController.cs @@ -14,15 +14,15 @@ [RoutePrefix( "api/books" )] public class BooksController : ApiController { - private static readonly Book[] books = new Book[] - { + private static readonly Book[] books = + [ new() { Id = "9781847490599", Title = "Anna Karenina", Author = "Leo Tolstoy", Published = 1878 }, new() { Id = "9780198800545", Title = "War and Peace", Author = "Leo Tolstoy", Published = 1869 }, new() { Id = "9780684801520", Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", Published = 1925 }, new() { Id = "9780486280615", Title = "The Adventures of Huckleberry Finn", Author = "Mark Twain", Published = 1884 }, new() { Id = "9780140430820", Title = "Moby Dick", Author = "Herman Melville", Published = 1851 }, new() { Id = "9780060934347", Title = "Don Quixote", Author = "Miguel de Cervantes", Published = 1605 }, - }; + ]; /// /// Gets all books. diff --git a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs index 73be263e..1ac7209a 100644 --- a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs +++ b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/Startup.cs @@ -1,8 +1,8 @@ namespace ApiVersioning.Examples; -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; @@ -68,6 +68,10 @@ public void Configuration( IAppBuilder builder ) .Allow( Skip | Count ) .AllowTop( 100 ) .AllowOrderBy( "title", "published" ); + + // applies model bound settings implicitly using an ad hoc EDM. alternatively, you can create your own + // IModelConfiguration + IODataQueryOptionsConvention for full control over what goes in the ad hoc EDM. + options.AdHocModelBuilder.ModelConfigurations.Add( new ImplicitModelBoundSettingsConvention() ); } ); configuration.EnableSwagger( @@ -88,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() ) @@ -101,25 +105,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/SwaggerDefaultValues.cs b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/SwaggerDefaultValues.cs index 67cd7607..2ce56dcf 100644 --- a/examples/AspNet/OData/SomeOpenApiODataWebApiExample/SwaggerDefaultValues.cs +++ b/examples/AspNet/OData/SomeOpenApiODataWebApiExample/SwaggerDefaultValues.cs @@ -30,17 +30,11 @@ public void Apply( Operation operation, SchemaRegistry schemaRegistry, ApiDescri var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.name ); // REF: https://github.com/domaindrivendev/Swashbuckle/issues/1101 - if ( parameter.description == null ) - { - parameter.description = description.Documentation; - } + parameter.description ??= description.Documentation; // REF: https://github.com/domaindrivendev/Swashbuckle/issues/1089 // REF: https://github.com/domaindrivendev/Swashbuckle/pull/1090 - if ( parameter.@default == null ) - { - parameter.@default = description.ParameterDescriptor?.DefaultValue; - } + parameter.@default ??= description.ParameterDescriptor?.DefaultValue; } } } \ No newline at end of file diff --git a/examples/AspNet/WebApi/BasicWebApiExample/Controllers/Values2Controller.cs b/examples/AspNet/WebApi/BasicWebApiExample/Controllers/Values2Controller.cs index 7dd4690b..ef7aa0ba 100644 --- a/examples/AspNet/WebApi/BasicWebApiExample/Controllers/Values2Controller.cs +++ b/examples/AspNet/WebApi/BasicWebApiExample/Controllers/Values2Controller.cs @@ -1,6 +1,7 @@ namespace ApiVersioning.Examples.Controllers; using Asp.Versioning; +using System.Net.Http; using System.Web.Http; [ApiVersion( 2.0 )] diff --git a/examples/AspNet/WebApi/ConventionsWebApiExample/Controllers/Values2Controller.cs b/examples/AspNet/WebApi/ConventionsWebApiExample/Controllers/Values2Controller.cs index e8ceaf54..1d700c5c 100644 --- a/examples/AspNet/WebApi/ConventionsWebApiExample/Controllers/Values2Controller.cs +++ b/examples/AspNet/WebApi/ConventionsWebApiExample/Controllers/Values2Controller.cs @@ -1,5 +1,6 @@ namespace ApiVersioning.Examples.Controllers; +using System.Net.Http; using System.Web.Http; [RoutePrefix( "api/values" )] diff --git a/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs b/examples/AspNet/WebApi/OpenApiWebApiExample/Startup.cs index b3651855..2a060f11 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; @@ -75,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() ) @@ -88,25 +89,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/AspNet/WebApi/OpenApiWebApiExample/SwaggerDefaultValues.cs b/examples/AspNet/WebApi/OpenApiWebApiExample/SwaggerDefaultValues.cs index 2fa156c5..dc26ff66 100644 --- a/examples/AspNet/WebApi/OpenApiWebApiExample/SwaggerDefaultValues.cs +++ b/examples/AspNet/WebApi/OpenApiWebApiExample/SwaggerDefaultValues.cs @@ -30,17 +30,11 @@ public void Apply( Operation operation, SchemaRegistry schemaRegistry, ApiDescri var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.name ); // REF: https://github.com/domaindrivendev/Swashbuckle/issues/1101 - if ( parameter.description == null ) - { - parameter.description = description.Documentation; - } + parameter.description ??= description.Documentation; // REF: https://github.com/domaindrivendev/Swashbuckle/issues/1089 // REF: https://github.com/domaindrivendev/Swashbuckle/pull/1090 - if ( parameter.@default == null ) - { - parameter.@default = description.ParameterDescriptor?.DefaultValue; - } + parameter.@default ??= description.ParameterDescriptor?.DefaultValue; } } } \ No newline at end of file diff --git a/src/AspNet/Directory.Build.targets b/examples/AspNetCore/Directory.Build.props similarity index 59% rename from src/AspNet/Directory.Build.targets rename to examples/AspNetCore/Directory.Build.props index 6a993f01..28e9057c 100644 --- a/src/AspNet/Directory.Build.targets +++ b/examples/AspNetCore/Directory.Build.props @@ -3,8 +3,8 @@ - - - + + net8.0 + \ No newline at end of file diff --git a/examples/AspNetCore/OData/Directory.Build.props b/examples/AspNetCore/OData/Directory.Build.props index 0d88b71c..0c6cf639 100644 --- a/examples/AspNetCore/OData/Directory.Build.props +++ b/examples/AspNetCore/OData/Directory.Build.props @@ -1,11 +1,10 @@ - + - + \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/OrderModelConfiguration.cs b/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/OrderModelConfiguration.cs index af356c02..b0a79db7 100644 --- a/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/OrderModelConfiguration.cs +++ b/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/OrderModelConfiguration.cs @@ -9,7 +9,7 @@ public class OrderModelConfiguration : IModelConfiguration { private static readonly ApiVersion V2 = new( 2, 0 ); - private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) { var order = builder.EntitySet( "Orders" ).EntityType; diff --git a/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/PersonModelConfiguration.cs b/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/PersonModelConfiguration.cs index 0ab0aed8..f462cb9a 100644 --- a/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/PersonModelConfiguration.cs +++ b/examples/AspNetCore/OData/ODataAdvancedExample/Configuration/PersonModelConfiguration.cs @@ -7,16 +7,16 @@ public class PersonModelConfiguration : IModelConfiguration { - private void ConfigureV1( ODataModelBuilder builder ) + private static void ConfigureV1( ODataModelBuilder builder ) { var person = ConfigureCurrent( builder ); person.Ignore( p => p.Email ); person.Ignore( p => p.Phone ); } - private void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); + private static void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); - private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) { var person = builder.EntitySet( "People" ).EntityType; diff --git a/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj b/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj index f0d91cbe..ecccb3a9 100644 --- a/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj +++ b/examples/AspNetCore/OData/ODataAdvancedExample/ODataAdvancedExample.csproj @@ -1,9 +1,5 @@  - - net7.0 - - diff --git a/examples/AspNetCore/OData/ODataAdvancedExample/Program.cs b/examples/AspNetCore/OData/ODataAdvancedExample/Program.cs index 0c88e968..e12ad205 100644 --- a/examples/AspNetCore/OData/ODataAdvancedExample/Program.cs +++ b/examples/AspNetCore/OData/ODataAdvancedExample/Program.cs @@ -6,6 +6,7 @@ // Add services to the container. builder.Services.AddControllers().AddOData(); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => { diff --git a/examples/AspNetCore/OData/ODataBasicExample/Configuration/OrderModelConfiguration.cs b/examples/AspNetCore/OData/ODataBasicExample/Configuration/OrderModelConfiguration.cs index 26573aec..c757921a 100644 --- a/examples/AspNetCore/OData/ODataBasicExample/Configuration/OrderModelConfiguration.cs +++ b/examples/AspNetCore/OData/ODataBasicExample/Configuration/OrderModelConfiguration.cs @@ -9,7 +9,7 @@ public class OrderModelConfiguration : IModelConfiguration { private static readonly ApiVersion V1 = new( 1, 0 ); - private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) { var order = builder.EntitySet( "Orders" ).EntityType; diff --git a/examples/AspNetCore/OData/ODataBasicExample/Configuration/PersonModelConfiguration.cs b/examples/AspNetCore/OData/ODataBasicExample/Configuration/PersonModelConfiguration.cs index a0d2c5f8..5ebaa0db 100644 --- a/examples/AspNetCore/OData/ODataBasicExample/Configuration/PersonModelConfiguration.cs +++ b/examples/AspNetCore/OData/ODataBasicExample/Configuration/PersonModelConfiguration.cs @@ -7,16 +7,16 @@ public class PersonModelConfiguration : IModelConfiguration { - private void ConfigureV1( ODataModelBuilder builder ) + private static void ConfigureV1( ODataModelBuilder builder ) { var person = ConfigureCurrent( builder ); person.Ignore( p => p.Email ); person.Ignore( p => p.Phone ); } - private void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); + private static void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); - private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) { var person = builder.EntitySet( "People" ).EntityType; diff --git a/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj b/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj index fc184237..6bdc8cef 100644 --- a/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj +++ b/examples/AspNetCore/OData/ODataBasicExample/ODataBasicExample.csproj @@ -1,9 +1,5 @@  - - net7.0 - - diff --git a/examples/AspNetCore/OData/ODataBasicExample/Program.cs b/examples/AspNetCore/OData/ODataBasicExample/Program.cs index 420bfaca..a269f238 100644 --- a/examples/AspNetCore/OData/ODataBasicExample/Program.cs +++ b/examples/AspNetCore/OData/ODataBasicExample/Program.cs @@ -5,6 +5,7 @@ // Add services to the container. builder.Services.AddControllers().AddOData(); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning() .AddOData( options => diff --git a/examples/AspNetCore/OData/ODataConventionsExample/Configuration/OrderModelConfiguration.cs b/examples/AspNetCore/OData/ODataConventionsExample/Configuration/OrderModelConfiguration.cs index 26573aec..c757921a 100644 --- a/examples/AspNetCore/OData/ODataConventionsExample/Configuration/OrderModelConfiguration.cs +++ b/examples/AspNetCore/OData/ODataConventionsExample/Configuration/OrderModelConfiguration.cs @@ -9,7 +9,7 @@ public class OrderModelConfiguration : IModelConfiguration { private static readonly ApiVersion V1 = new( 1, 0 ); - private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) { var order = builder.EntitySet( "Orders" ).EntityType; diff --git a/examples/AspNetCore/OData/ODataConventionsExample/Configuration/PersonModelConfiguration.cs b/examples/AspNetCore/OData/ODataConventionsExample/Configuration/PersonModelConfiguration.cs index a0d2c5f8..5ebaa0db 100644 --- a/examples/AspNetCore/OData/ODataConventionsExample/Configuration/PersonModelConfiguration.cs +++ b/examples/AspNetCore/OData/ODataConventionsExample/Configuration/PersonModelConfiguration.cs @@ -7,16 +7,16 @@ public class PersonModelConfiguration : IModelConfiguration { - private void ConfigureV1( ODataModelBuilder builder ) + private static void ConfigureV1( ODataModelBuilder builder ) { var person = ConfigureCurrent( builder ); person.Ignore( p => p.Email ); person.Ignore( p => p.Phone ); } - private void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); + private static void ConfigureV2( ODataModelBuilder builder ) => ConfigureCurrent( builder ).Ignore( p => p.Phone ); - private EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) + private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuilder builder ) { var person = builder.EntitySet( "People" ).EntityType; diff --git a/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj b/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj index f0d91cbe..ecccb3a9 100644 --- a/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj +++ b/examples/AspNetCore/OData/ODataConventionsExample/ODataConventionsExample.csproj @@ -1,9 +1,5 @@  - - net7.0 - - diff --git a/examples/AspNetCore/OData/ODataConventionsExample/Program.cs b/examples/AspNetCore/OData/ODataConventionsExample/Program.cs index 2b65536a..9f937fda 100644 --- a/examples/AspNetCore/OData/ODataConventionsExample/Program.cs +++ b/examples/AspNetCore/OData/ODataConventionsExample/Program.cs @@ -7,6 +7,7 @@ // Add services to the container. builder.Services.AddControllers().AddOData(); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => { diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/PersonModelConfiguration.cs b/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/PersonModelConfiguration.cs index 5bae1fe3..98d8b060 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/PersonModelConfiguration.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/PersonModelConfiguration.cs @@ -18,6 +18,7 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout person.HasKey( p => p.Id ); person.Select().OrderBy( "firstName", "lastName" ); + person.Page( maxTopValue: 100, pageSizeValue: default ); if ( apiVersion < ApiVersions.V3 ) { diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/ProductConfiguration.cs b/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/ProductConfiguration.cs index e8936359..88f7d83a 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/ProductConfiguration.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/ProductConfiguration.cs @@ -18,6 +18,9 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout return; } - var product = builder.EntitySet( "Products" ).EntityType.HasKey( p => p.Id ); + var product = builder.EntitySet( "Products" ).EntityType; + + product.HasKey( p => p.Id ); + product.Page( maxTopValue: 100, pageSizeValue: default ); } } \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/SupplierConfiguration.cs b/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/SupplierConfiguration.cs index 7a803487..e8f1f9e0 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/SupplierConfiguration.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Configuration/SupplierConfiguration.cs @@ -18,7 +18,12 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string rout return; } - builder.EntitySet( "Suppliers" ).EntityType.HasKey( p => p.Id ); + var supplier = builder.EntitySet( "Suppliers" ).EntityType; + + + supplier.HasKey( p => p.Id ); + supplier.Page( maxTopValue: 100, pageSizeValue: default ); + builder.Singleton( "Acme" ); } } \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs index d0575a5e..5384c160 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -1,9 +1,9 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; 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; @@ -50,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() ) @@ -63,25 +63,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/ODataOpenApiExample/Models/Order.cs b/examples/AspNetCore/OData/ODataOpenApiExample/Models/Order.cs index 009e9862..1df9452e 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Models/Order.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Models/Order.cs @@ -6,6 +6,7 @@ /// /// Represents an order. /// +[Page( MaxTop = 100 )] [Select] [Select( "effectiveDate", SelectType = SelectExpandType.Disabled )] public class Order diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj b/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj index 79a6a4b8..3bffcf5e 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj +++ b/examples/AspNetCore/OData/ODataOpenApiExample/ODataOpenApiExample.csproj @@ -1,16 +1,15 @@  - net7.0 true - + - + \ No newline at end of file diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs b/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs index 2aeeb5f0..4dd5e683 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs @@ -23,6 +23,7 @@ options.RouteOptions.EnableQualifiedOperationCall = false; options.RouteOptions.EnableUnqualifiedOperationCall = true; } ); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => { @@ -79,15 +80,17 @@ 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(); } app.UseSwagger(); +if ( app.Environment.IsDevelopment() ) +{ app.UseSwaggerUI( options => { @@ -101,6 +104,7 @@ options.SwaggerEndpoint( url, name ); } } ); +} app.UseHttpsRedirection(); app.UseAuthorization(); diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/SwaggerDefaultValues.cs b/examples/AspNetCore/OData/ODataOpenApiExample/SwaggerDefaultValues.cs index 34369a0f..fbf06c2f 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/SwaggerDefaultValues.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/SwaggerDefaultValues.cs @@ -50,10 +50,7 @@ public void Apply( OpenApiOperation operation, OperationFilterContext context ) { var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); - if ( parameter.Description == null ) - { - parameter.Description = description.ModelMetadata?.Description; - } + parameter.Description ??= description.ModelMetadata?.Description; if ( parameter.Schema.Default == null && description.DefaultValue != null ) { diff --git a/examples/AspNetCore/OData/ODataOpenApiExample/V3/SuppliersController.cs b/examples/AspNetCore/OData/ODataOpenApiExample/V3/SuppliersController.cs index 401e1a59..652b4d4e 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/V3/SuppliersController.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/V3/SuppliersController.cs @@ -17,9 +17,9 @@ public class SuppliersController : ODataController { private readonly IQueryable suppliers = new[] - { - NewSupplier( 1 ), - NewSupplier( 2 ), + { + NewSupplier( 1 ), + NewSupplier( 2 ), NewSupplier( 3 ), }.AsQueryable(); @@ -46,7 +46,7 @@ public class SuppliersController : ODataController [Produces( "application/json" )] [ProducesResponseType( typeof( Supplier ), Status200OK )] [ProducesResponseType( Status404NotFound )] - public SingleResult Get( int key ) => + public SingleResult Get( int key ) => SingleResult.Create( suppliers.Where( p => p.Id == key ) ); /// @@ -147,7 +147,7 @@ public IActionResult Put( int key, [FromBody] Supplier update ) /// The supplier identifier. /// The associated supplier products. [HttpGet] - [EnableQuery] + [EnableQuery( MaxTop = 100 )] public IQueryable GetProducts( int key ) => suppliers.Where( s => s.Id == key ).SelectMany( s => s.Products ); @@ -181,8 +181,8 @@ public IActionResult CreateRef( [ProducesResponseType( Status204NoContent )] [ProducesResponseType( Status404NotFound )] public IActionResult DeleteRef( - int key, - int relatedKey, + int key, + int relatedKey, string navigationProperty ) => NoContent(); private static Supplier NewSupplier( int id ) => diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/Book.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/Book.cs index bd9fae58..c54dcba5 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/Book.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/Book.cs @@ -1,8 +1,15 @@ namespace ApiVersioning.Examples; +using Microsoft.OData.ModelBuilder; + +// TODO: Model Bound settings can be performed via attributes if the +// return type is known to the API Explorer or can be explicitly done +// via one or more IModelConfiguration implementations + /// /// Represents a book. /// +[Filter( "author", "published" )] public class Book { /// diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/BooksController.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/BooksController.cs index 6266bc41..68e97255 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/BooksController.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/BooksController.cs @@ -13,15 +13,15 @@ [Route( "api/[controller]" )] public class BooksController : ControllerBase { - private static readonly Book[] books = new Book[] - { + private static readonly Book[] books = + [ new() { Id = "9781847490599", Title = "Anna Karenina", Author = "Leo Tolstoy", Published = 1878 }, new() { Id = "9780198800545", Title = "War and Peace", Author = "Leo Tolstoy", Published = 1869 }, new() { Id = "9780684801520", Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", Published = 1925 }, new() { Id = "9780486280615", Title = "The Adventures of Huckleberry Finn", Author = "Mark Twain", Published = 1884 }, new() { Id = "9780140430820", Title = "Moby Dick", Author = "Herman Melville", Published = 1851 }, new() { Id = "9780060934347", Title = "Don Quixote", Author = "Miguel de Cervantes", Published = 1605 }, - }; + ]; /// /// Gets all books. diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs index 987145a1..b725ab72 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/ConfigureSwaggerOptions.cs @@ -1,9 +1,9 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; 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; @@ -50,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() ) @@ -63,25 +63,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/Program.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs index a20f2ae0..55e70b17 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs @@ -27,6 +27,7 @@ builder.Services.AddControllers() .AddOData( options => options.Select() ); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning() .AddODataApiExplorer( options => @@ -63,20 +64,22 @@ // Configure the HTTP request pipeline. app.UseSwagger(); -app.UseSwaggerUI( +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 ); - } - } ); - + { + 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(); app.MapControllers(); diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj b/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj index 79a6a4b8..3bffcf5e 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/SomeODataOpenApiExample.csproj @@ -1,16 +1,15 @@  - net7.0 true - + - + \ No newline at end of file diff --git a/examples/AspNetCore/OData/SomeODataOpenApiExample/SwaggerDefaultValues.cs b/examples/AspNetCore/OData/SomeODataOpenApiExample/SwaggerDefaultValues.cs index 34369a0f..fbf06c2f 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/SwaggerDefaultValues.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/SwaggerDefaultValues.cs @@ -50,10 +50,7 @@ public void Apply( OpenApiOperation operation, OperationFilterContext context ) { var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); - if ( parameter.Description == null ) - { - parameter.Description = description.ModelMetadata?.Description; - } + parameter.Description ??= description.ModelMetadata?.Description; if ( parameter.Schema.Default == null && description.DefaultValue != null ) { diff --git a/examples/AspNetCore/WebApi/BasicExample/BasicExample.csproj b/examples/AspNetCore/WebApi/BasicExample/BasicExample.csproj index 24772e6a..f283af35 100644 --- a/examples/AspNetCore/WebApi/BasicExample/BasicExample.csproj +++ b/examples/AspNetCore/WebApi/BasicExample/BasicExample.csproj @@ -1,9 +1,5 @@  - - net7.0 - - diff --git a/examples/AspNetCore/WebApi/BasicExample/Program.cs b/examples/AspNetCore/WebApi/BasicExample/Program.cs index 8e6c97ca..79d79eda 100644 --- a/examples/AspNetCore/WebApi/BasicExample/Program.cs +++ b/examples/AspNetCore/WebApi/BasicExample/Program.cs @@ -5,6 +5,7 @@ // Add services to the container. builder.Services.AddControllers(); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => { diff --git a/examples/AspNetCore/WebApi/ByNamespaceExample/ByNamespaceExample.csproj b/examples/AspNetCore/WebApi/ByNamespaceExample/ByNamespaceExample.csproj index 24772e6a..f283af35 100644 --- a/examples/AspNetCore/WebApi/ByNamespaceExample/ByNamespaceExample.csproj +++ b/examples/AspNetCore/WebApi/ByNamespaceExample/ByNamespaceExample.csproj @@ -1,9 +1,5 @@  - - net7.0 - - diff --git a/examples/AspNetCore/WebApi/ByNamespaceExample/Program.cs b/examples/AspNetCore/WebApi/ByNamespaceExample/Program.cs index ab09b7fc..68a223c4 100644 --- a/examples/AspNetCore/WebApi/ByNamespaceExample/Program.cs +++ b/examples/AspNetCore/WebApi/ByNamespaceExample/Program.cs @@ -7,6 +7,7 @@ // Add services to the container. builder.Services.AddControllers(); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => { diff --git a/examples/AspNetCore/WebApi/ConventionsExample/ConventionsExample.csproj b/examples/AspNetCore/WebApi/ConventionsExample/ConventionsExample.csproj index 24772e6a..f283af35 100644 --- a/examples/AspNetCore/WebApi/ConventionsExample/ConventionsExample.csproj +++ b/examples/AspNetCore/WebApi/ConventionsExample/ConventionsExample.csproj @@ -1,9 +1,5 @@  - - net7.0 - - diff --git a/examples/AspNetCore/WebApi/ConventionsExample/Program.cs b/examples/AspNetCore/WebApi/ConventionsExample/Program.cs index 388989cd..756c678e 100644 --- a/examples/AspNetCore/WebApi/ConventionsExample/Program.cs +++ b/examples/AspNetCore/WebApi/ConventionsExample/Program.cs @@ -8,6 +8,7 @@ // Add services to the container. builder.Services.AddControllers(); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => { diff --git a/examples/AspNetCore/WebApi/MinimalApiExample/MinimalApiExample.csproj b/examples/AspNetCore/WebApi/MinimalApiExample/MinimalApiExample.csproj index ad99ee2f..3fc9fc03 100644 --- a/examples/AspNetCore/WebApi/MinimalApiExample/MinimalApiExample.csproj +++ b/examples/AspNetCore/WebApi/MinimalApiExample/MinimalApiExample.csproj @@ -1,7 +1,6 @@  - net7.0 enable diff --git a/examples/AspNetCore/WebApi/MinimalApiExample/Program.cs b/examples/AspNetCore/WebApi/MinimalApiExample/Program.cs index 10aa2033..4282c9f5 100644 --- a/examples/AspNetCore/WebApi/MinimalApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/MinimalApiExample/Program.cs @@ -1,9 +1,9 @@ -using Asp.Versioning.Conventions; - var builder = WebApplication.CreateBuilder( args ); // Add services to the container. +builder.Services.AddProblemDetails(); + // enable api versioning and return the headers // "api-supported-versions" and "api-deprecated-versions" builder.Services.AddApiVersioning( options => options.ReportApiVersions = true ); @@ -17,10 +17,10 @@ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; -var forecast = app.MapGroup( "/weatherforecast" ).WithApiVersionSet(); +var forecast = app.NewVersionedApi(); // GET /weatherforecast?api-version=1.0 -forecast.MapGet( "/", () => +forecast.MapGet( "/weatherforecast", () => { return Enumerable.Range( 1, 5 ).Select( index => new WeatherForecast @@ -33,24 +33,25 @@ .HasApiVersion( 1.0 ); // GET /weatherforecast?api-version=2.0 -forecast.MapGet( "/", () => - { - return Enumerable.Range( 0, summaries.Length ).Select( index => - new WeatherForecast - ( - DateTime.Now.AddDays( index ), - Random.Shared.Next( -20, 55 ), - summaries[Random.Shared.Next( summaries.Length )] - ) ); - } ) - .HasApiVersion( 2.0 ); +var v2 = forecast.MapGroup( "/weatherforecast" ) + .HasApiVersion( 2.0 ); + +v2.MapGet( "/", () => + { + return Enumerable.Range( 0, summaries.Length ).Select( index => + new WeatherForecast + ( + DateTime.Now.AddDays( index ), + Random.Shared.Next( -20, 55 ), + summaries[Random.Shared.Next( summaries.Length )] + ) ); + } ); // POST /weatherforecast?api-version=2.0 -forecast.MapPost( "/", ( WeatherForecast forecast ) => { } ) - .HasApiVersion( 2.0 ); +v2.MapPost( "/", ( WeatherForecast forecast ) => Results.Ok() ); // DELETE /weatherforecast -forecast.MapDelete( "/", () => Results.NoContent() ) +forecast.MapDelete( "/weatherforecast", () => Results.NoContent() ) .IsApiVersionNeutral(); app.Run(); diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs index bee2b1f6..d531cea4 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/ConfigureSwaggerOptions.cs @@ -1,9 +1,9 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; 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; @@ -50,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() ) @@ -63,25 +63,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/MinimalOpenApiExample.csproj b/examples/AspNetCore/WebApi/MinimalOpenApiExample/MinimalOpenApiExample.csproj index 47611fa4..d2daf6ff 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/MinimalOpenApiExample.csproj +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/MinimalOpenApiExample.csproj @@ -1,11 +1,7 @@  - - net7.0 - - - + diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs index 7895b722..909d8261 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs @@ -1,6 +1,5 @@ using ApiVersioning.Examples; using Asp.Versioning; -using Asp.Versioning.Conventions; using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.SwaggerGen; using OrderV1 = ApiVersioning.Examples.Models.V1.Order; @@ -14,6 +13,7 @@ var services = builder.Services; // Add services to the container. +services.AddProblemDetails(); services.AddEndpointsApiExplorer(); services.AddApiVersioning( options => @@ -47,231 +47,237 @@ // Configure the HTTP request pipeline. var app = builder.Build(); -var orders = app.MapGroup( "/api/orders" ).WithApiVersionSet( "Orders" ); -var people = app.MapGroup( "/api/v{version:apiVersion}/people" ).WithApiVersionSet( "People" ); +var orders = app.NewVersionedApi( "Orders" ); +var people = app.NewVersionedApi( "People" ); // 1.0 -orders.MapGet( "/{id:int}", ( int id ) => new OrderV1() { Id = id, Customer = "John Doe" } ) - .Produces() - .Produces( 404 ) - .HasDeprecatedApiVersion( 0.9 ) - .HasApiVersion( 1.0 ); +var ordersV1 = orders.MapGroup( "/api/orders" ) + .HasDeprecatedApiVersion( 0.9 ) + .HasApiVersion( 1.0 ); -orders.MapPost( "/", ( HttpRequest request, OrderV1 order ) => - { - order.Id = 42; - var scheme = request.Scheme; - var host = request.Host; - var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" ); - return Results.Created( location, order ); - } ) - .Accepts( "application/json" ) - .Produces( 201 ) - .Produces( 400 ) - .HasApiVersion( 1.0 ); +ordersV1.MapGet( "/{id:int}", ( int id ) => new OrderV1() { Id = id, Customer = "John Doe" } ) + .Produces() + .Produces( 404 ); -orders.MapMethods( "/{id:int}", new[] { HttpMethod.Patch.Method }, ( int id, OrderV1 order ) => Results.NoContent() ) - .Accepts( "application/json" ) - .Produces( 204 ) - .Produces( 400 ) - .Produces( 404 ) - .HasApiVersion( 1.0 ); +ordersV1.MapPost( "/", ( HttpRequest request, OrderV1 order ) => + { + order.Id = 42; + var scheme = request.Scheme; + var host = request.Host; + var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" ); + return Results.Created( location, order ); + } ) + .Accepts( "application/json" ) + .Produces( 201 ) + .Produces( 400 ) + .MapToApiVersion( 1.0 ); + +ordersV1.MapPatch( "/{id:int}", ( int id, OrderV1 order ) => Results.NoContent() ) + .Accepts( "application/json" ) + .Produces( 204 ) + .Produces( 400 ) + .Produces( 404 ) + .MapToApiVersion( 1.0 ); // 2.0 -orders.MapGet( "/", () => - new OrderV2[] - { - new(){ Id = 1, Customer = "John Doe" }, - new(){ Id = 2, Customer = "Bob Smith" }, - new(){ Id = 3, Customer = "Jane Doe", EffectiveDate = DateTimeOffset.UtcNow.AddDays( 7d ) }, - } ) - .Produces>() - .Produces( 404 ) - .HasApiVersion( 2.0 ); +var ordersV2 = orders.MapGroup( "/api/orders" ) + .HasApiVersion( 2.0 ); + +ordersV2.MapGet( "/", () => + new OrderV2[] + { + new(){ Id = 1, Customer = "John Doe" }, + new(){ Id = 2, Customer = "Bob Smith" }, + new(){ Id = 3, Customer = "Jane Doe", EffectiveDate = DateTimeOffset.UtcNow.AddDays( 7d ) }, + } ) + .Produces>() + .Produces( 404 ); + +ordersV2.MapGet( "/{id:int}", ( int id ) => new OrderV2() { Id = id, Customer = "John Doe" } ) + .Produces() + .Produces( 404 ); -orders.MapGet( "/{id:int}", ( int id ) => new OrderV2() { Id = id, Customer = "John Doe" } ) - .Produces() - .Produces( 404 ) - .HasApiVersion( 2.0 ); +ordersV2.MapPost( "/", ( HttpRequest request, OrderV2 order ) => + { + order.Id = 42; + var scheme = request.Scheme; + var host = request.Host; + var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" ); + return Results.Created( location, order ); + } ) + .Accepts( "application/json" ) + .Produces( 201 ) + .Produces( 400 ); -orders.MapPost( "/", ( HttpRequest request, OrderV2 order ) => - { - order.Id = 42; - var scheme = request.Scheme; - var host = request.Host; - var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" ); - return Results.Created( location, order ); - } ) - .Accepts( "application/json" ) - .Produces( 201 ) - .Produces( 400 ) - .HasApiVersion( 2.0 ); -orders.MapMethods( "/{id:int}", new[] { HttpMethod.Patch.Method }, ( int id, OrderV2 order ) => Results.NoContent() ) - .Accepts( "application/json" ) - .Produces( 204 ) - .Produces( 400 ) - .Produces( 404 ) - .HasApiVersion( 2.0 ); +ordersV2.MapPatch( "/{id:int}", ( int id, OrderV2 order ) => Results.NoContent() ) + .Accepts( "application/json" ) + .Produces( 204 ) + .Produces( 400 ) + .Produces( 404 ); // 3.0 -orders.MapGet( "/", () => - new OrderV3[] - { - new(){ Id = 1, Customer = "John Doe" }, - new(){ Id = 2, Customer = "Bob Smith" }, - new(){ Id = 3, Customer = "Jane Doe", EffectiveDate = DateTimeOffset.UtcNow.AddDays( 7d ) }, - } ) - .Produces>() - .HasApiVersion( 3.0 ); +var ordersV3 = orders.MapGroup( "/api/orders" ) + .HasApiVersion( 3.0 ); -orders.MapGet( "/{id:int}", ( int id ) => new OrderV3() { Id = id, Customer = "John Doe" } ) - .Produces() - .Produces( 404 ) - .HasApiVersion( 3.0 ); +ordersV3.MapGet( "/", () => + new OrderV3[] + { + new(){ Id = 1, Customer = "John Doe" }, + new(){ Id = 2, Customer = "Bob Smith" }, + new(){ Id = 3, Customer = "Jane Doe", EffectiveDate = DateTimeOffset.UtcNow.AddDays( 7d ) }, + } ) + .Produces>(); -orders.MapPost( "/", ( HttpRequest request, OrderV3 order ) => - { - order.Id = 42; - var scheme = request.Scheme; - var host = request.Host; - var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" ); - return Results.Created( location, order ); - } ) - .Accepts( "application/json" ) - .Produces( 201 ) - .Produces( 400 ) - .HasApiVersion( 3.0 ); +ordersV3.MapGet( "/{id:int}", ( int id ) => new OrderV3() { Id = id, Customer = "John Doe" } ) + .Produces() + .Produces( 404 ); -orders.MapDelete( "/{id:int}", ( int id ) => Results.NoContent() ) - .Produces( 204 ) - .HasApiVersion( 3.0 ); +ordersV3.MapPost( "/", ( HttpRequest request, OrderV3 order ) => + { + order.Id = 42; + var scheme = request.Scheme; + var host = request.Host; + var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/api/orders/{order.Id}" ); + return Results.Created( location, order ); + } ) + .Accepts( "application/json" ) + .Produces( 201 ) + .Produces( 400 ); + +ordersV3.MapDelete( "/{id:int}", ( int id ) => Results.NoContent() ) + .Produces( 204 ); // 1.0 -people.MapGet( "/{id:int}", ( int id ) => - new PersonV1() - { - Id = id, - FirstName = "John", - LastName = "Doe", - } ) - .Produces() - .Produces( 404 ) - .HasDeprecatedApiVersion( 0.9 ) - .HasApiVersion( 1.0 ); +var peopleV1 = people.MapGroup( "/api/v{version:apiVersion}/people" ) + .HasDeprecatedApiVersion( 0.9 ) + .HasApiVersion( 1.0 ); + +peopleV1.MapGet( "/{id:int}", ( int id ) => + new PersonV1() + { + Id = id, + FirstName = "John", + LastName = "Doe", + } ) + .Produces() + .Produces( 404 ); // 2.0 -people.MapGet( "/", () => - new PersonV2[] - { - new() - { - Id = 1, - FirstName = "John", - LastName = "Doe", - Email = "john.doe@somewhere.com", - }, - new() - { - Id = 2, - FirstName = "Bob", - LastName = "Smith", - Email = "bob.smith@somewhere.com", - }, - new() - { - Id = 3, - FirstName = "Jane", - LastName = "Doe", - Email = "jane.doe@somewhere.com", - }, - } ) - .Produces>() - .HasApiVersion( 2.0 ); +var peopleV2 = people.MapGroup( "/api/v{version:apiVersion}/people" ) + .HasApiVersion( 2.0 ); -people.MapGet( "/{id:int}", ( int id ) => - new PersonV2() - { - Id = id, - FirstName = "John", - LastName = "Doe", - Email = "john.doe@somewhere.com", - } ) - .Produces() - .Produces( 404 ) - .HasApiVersion( 2.0 ); +peopleV2.MapGet( "/", () => + new PersonV2[] + { + new() + { + Id = 1, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@somewhere.com", + }, + new() + { + Id = 2, + FirstName = "Bob", + LastName = "Smith", + Email = "bob.smith@somewhere.com", + }, + new() + { + Id = 3, + FirstName = "Jane", + LastName = "Doe", + Email = "jane.doe@somewhere.com", + }, + } ) + .Produces>(); + +peopleV2.MapGet( "/{id:int}", ( int id ) => + new PersonV2() + { + Id = id, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@somewhere.com", + } ) + .Produces() + .Produces( 404 ); // 3.0 -people.MapGet( "/", () => - new PersonV3[] - { - new() - { - Id = 1, - FirstName = "John", - LastName = "Doe", - Email = "john.doe@somewhere.com", - Phone = "555-987-1234", - }, - new() - { - Id = 2, - FirstName = "Bob", - LastName = "Smith", - Email = "bob.smith@somewhere.com", - Phone = "555-654-4321", - }, - new() - { - Id = 3, - FirstName = "Jane", - LastName = "Doe", - Email = "jane.doe@somewhere.com", - Phone = "555-789-3456", - }, - } ) - .Produces>() - .HasApiVersion( 3.0 ); +var peopleV3 = people.MapGroup( "/api/v{version:apiVersion}/people" ) + .HasApiVersion( 3.0 ); -people.MapGet( "/{id:int}", ( int id ) => - new PersonV3() - { - Id = id, - FirstName = "John", - LastName = "Doe", - Email = "john.doe@somewhere.com", - Phone = "555-987-1234", - } ) - .Produces() - .Produces( 404 ) - .HasApiVersion( 3.0 ); +peopleV3.MapGet( "/", () => + new PersonV3[] + { + new() + { + Id = 1, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@somewhere.com", + Phone = "555-987-1234", + }, + new() + { + Id = 2, + FirstName = "Bob", + LastName = "Smith", + Email = "bob.smith@somewhere.com", + Phone = "555-654-4321", + }, + new() + { + Id = 3, + FirstName = "Jane", + LastName = "Doe", + Email = "jane.doe@somewhere.com", + Phone = "555-789-3456", + }, + } ) + .Produces>(); -people.MapPost( "/", ( HttpRequest request, ApiVersion version, PersonV3 person ) => - { - person.Id = 42; - var scheme = request.Scheme; - var host = request.Host; - var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/v{version}/api/people/{person.Id}" ); - return Results.Created( location, person ); - } ) - .Accepts( "application/json" ) - .Produces( 201 ) - .Produces( 400 ) - .HasApiVersion( 3.0 ); +peopleV3.MapGet( "/{id:int}", ( int id ) => + new PersonV3() + { + Id = id, + FirstName = "John", + LastName = "Doe", + Email = "john.doe@somewhere.com", + Phone = "555-987-1234", + } ) + .Produces() + .Produces( 404 ); -app.UseSwagger(); -app.UseSwaggerUI( - options => - { - var descriptions = app.DescribeApiVersions(); +peopleV3.MapPost( "/", ( HttpRequest request, ApiVersion version, PersonV3 person ) => + { + person.Id = 42; + var scheme = request.Scheme; + var host = request.Host; + var location = new Uri( $"{scheme}{Uri.SchemeDelimiter}{host}/v{version}/api/people/{person.Id}" ); + return Results.Created( location, person ); + } ) + .Accepts( "application/json" ) + .Produces( 201 ) + .Produces( 400 ); - // build a swagger endpoint for each discovered API version - foreach ( var description in descriptions ) +app.UseSwagger(); +if ( app.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 diff --git a/examples/AspNetCore/WebApi/MinimalOpenApiExample/SwaggerDefaultValues.cs b/examples/AspNetCore/WebApi/MinimalOpenApiExample/SwaggerDefaultValues.cs index 796cdd9d..0bcae0a8 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/SwaggerDefaultValues.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/SwaggerDefaultValues.cs @@ -47,10 +47,7 @@ public void Apply( OpenApiOperation operation, OperationFilterContext context ) { var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); - if ( parameter.Description == null ) - { - parameter.Description = description.ModelMetadata?.Description; - } + parameter.Description ??= description.ModelMetadata?.Description; if ( parameter.Schema.Default == null && description.DefaultValue != null && diff --git a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs index bee2b1f6..d531cea4 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/ConfigureSwaggerOptions.cs @@ -1,9 +1,9 @@ namespace ApiVersioning.Examples; -using Asp.Versioning; 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; @@ -50,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() ) @@ -63,25 +63,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/OpenApiExample.csproj b/examples/AspNetCore/WebApi/OpenApiExample/OpenApiExample.csproj index 5c6ed4ea..2be84679 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/OpenApiExample.csproj +++ b/examples/AspNetCore/WebApi/OpenApiExample/OpenApiExample.csproj @@ -1,12 +1,11 @@  - net7.0 true - + diff --git a/examples/AspNetCore/WebApi/OpenApiExample/Program.cs b/examples/AspNetCore/WebApi/OpenApiExample/Program.cs index 74bab524..454c60d7 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/Program.cs @@ -10,6 +10,7 @@ // Add services to the container. builder.Services.AddControllers(); +builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => { @@ -56,19 +57,22 @@ // Configure the HTTP request pipeline. app.UseSwagger(); -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 ); - } - } ); +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 ); + } + } ); +} app.UseHttpsRedirection(); app.UseAuthorization(); diff --git a/examples/AspNetCore/WebApi/OpenApiExample/SwaggerDefaultValues.cs b/examples/AspNetCore/WebApi/OpenApiExample/SwaggerDefaultValues.cs index 796cdd9d..0bcae0a8 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/SwaggerDefaultValues.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/SwaggerDefaultValues.cs @@ -47,10 +47,7 @@ public void Apply( OpenApiOperation operation, OperationFilterContext context ) { var description = apiDescription.ParameterDescriptions.First( p => p.Name == parameter.Name ); - if ( parameter.Description == null ) - { - parameter.Description = description.ModelMetadata?.Description; - } + parameter.Description ??= description.ModelMetadata?.Description; if ( parameter.Schema.Default == null && description.DefaultValue != null && diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs index eed5d62c..ca183af6 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs @@ -1,12 +1,14 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 +#pragma warning disable CA1019 +#pragma warning disable CA1033 +#pragma warning disable CA1813 + namespace Asp.Versioning; using static System.AttributeTargets; -#pragma warning disable CA1019 -#pragma warning disable CA1813 - /// /// Represents the metadata that describes the advertised API versions. /// @@ -71,9 +73,7 @@ public AdvertiseApiVersionsAttribute( string version ) : base( version ) { } public AdvertiseApiVersionsAttribute( string version, params string[] otherVersions ) : base( version, otherVersions ) { } -#pragma warning disable CA1033 // Interface methods should be callable by child types ApiVersionProviderOptions IApiVersionProvider.Options => options; -#pragma warning restore CA1033 // Interface methods should be callable by child types /// /// Gets or sets a value indicating whether the specified set of API versions are deprecated. @@ -97,5 +97,5 @@ public bool Deprecated } /// - public override int GetHashCode() => HashCode.Combine( GetHashCode(), Deprecated ); + public override int GetHashCode() => HashCode.Combine( base.GetHashCode(), Deprecated ); } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/AmbiguousApiVersionException.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/AmbiguousApiVersionException.cs index 24577d7a..4a780ad5 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/AmbiguousApiVersionException.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/AmbiguousApiVersionException.cs @@ -12,14 +12,14 @@ public partial class AmbiguousApiVersionException : Exception /// /// Initializes a new instance of the class. /// - public AmbiguousApiVersionException() => apiVersions = Array.Empty(); + public AmbiguousApiVersionException() => apiVersions = []; /// /// Initializes a new instance of the class. /// /// The associated error message. public AmbiguousApiVersionException( string message ) - : base( message ) => apiVersions = Array.Empty(); + : base( message ) => apiVersions = []; /// /// Initializes a new instance of the class. @@ -27,7 +27,7 @@ public AmbiguousApiVersionException( string message ) /// The associated error message. /// The inner exception that caused the current exception, if any. public AmbiguousApiVersionException( string message, Exception innerException ) - : base( message, innerException ) => apiVersions = Array.Empty(); + : base( message, innerException ) => apiVersions = []; /// /// Initializes a new instance of the class. diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersion.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersion.cs index 3edda7f0..1bf4eeaa 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersion.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersion.cs @@ -74,7 +74,7 @@ protected ApiVersion( double version, string? status, Func isVali Status = ValidateStatus( status, - isValidStatus ?? throw new ArgumentNullException( nameof( isValidStatus ) ) ); + isValidStatus ?? throw new System.ArgumentNullException( nameof( isValidStatus ) ) ); var number = new decimal( version ); var bits = decimal.GetBits( number ); @@ -124,10 +124,7 @@ protected internal ApiVersion( /// The instance to derive from. protected ApiVersion( ApiVersion other ) { - if ( other == null ) - { - throw new ArgumentNullException( nameof( other ) ); - } + ArgumentNullException.ThrowIfNull( other ); hashCode = other.hashCode; GroupVersion = other.GroupVersion; @@ -341,9 +338,11 @@ public virtual int CompareTo( ApiVersion? other ) public virtual string ToString( string? format, IFormatProvider? formatProvider ) { var provider = ApiVersionFormatProvider.GetInstance( formatProvider ); +#pragma warning disable IDE0079 #pragma warning disable CA1062 // Validate arguments of public methods return provider.Format( format, this, formatProvider ); #pragma warning restore CA1062 // Validate arguments of public methods +#pragma warning restore IDE0079 } private static string? ValidateStatus( string? status, Func isValid ) @@ -353,7 +352,7 @@ public virtual string ToString( string? format, IFormatProvider? formatProvider return status; } - var message = string.Format( CultureInfo.CurrentCulture, SR.ApiVersionBadStatus, status ); + var message = string.Format( CultureInfo.CurrentCulture, Format.ApiVersionBadStatus, status ); throw new ArgumentException( message, nameof( status ) ); } } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionAttribute.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionAttribute.cs index 2d56db41..90657500 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionAttribute.cs @@ -1,12 +1,14 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 +#pragma warning disable CA1019 +#pragma warning disable CA1033 +#pragma warning disable CA1813 + namespace Asp.Versioning; using static System.AttributeTargets; -#pragma warning disable CA1019 -#pragma warning disable CA1813 - /// /// Represents the metadata that describes the versions associated with an API. /// @@ -42,9 +44,7 @@ protected ApiVersionAttribute( IApiVersionParser parser, string version ) : base /// The API version string. public ApiVersionAttribute( string version ) : base( version ) { } -#pragma warning disable CA1033 // Interface methods should be callable by child types ApiVersionProviderOptions IApiVersionProvider.Options => options; -#pragma warning restore CA1033 // Interface methods should be callable by child types /// /// Gets or sets a value indicating whether the specified set of API versions are deprecated. diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs index 92e472ab..6ade14b6 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0079 -#pragma warning disable SA1121 // Use built-in type alias +#pragma warning disable SA1121 namespace Asp.Versioning; @@ -200,7 +200,7 @@ public ApiVersionFormatProvider() /// /// The used by the format provider. public ApiVersionFormatProvider( DateTimeFormatInfo dateTimeFormat ) - : this( dateTimeFormat ?? throw new ArgumentNullException( nameof( dateTimeFormat ) ), dateTimeFormat.Calendar ) { } + : this( dateTimeFormat ?? throw new System.ArgumentNullException( nameof( dateTimeFormat ) ), dateTimeFormat.Calendar ) { } /// /// Initializes a new instance of the class. @@ -293,20 +293,11 @@ protected virtual void FormatVersionPart( Text format, IFormatProvider formatProvider ) { - if ( text == null ) - { - throw new ArgumentNullException( nameof( text ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } - - if ( Str.IsNullOrEmpty( format ) ) - { - throw new ArgumentNullException( nameof( format ) ); - } + ArgumentNullException.ThrowIfNull( text ); + ArgumentNullException.ThrowIfNull( apiVersion ); +#if NETSTANDARD1_0 + ArgumentNullException.ThrowIfNull( format ); +#endif switch ( format[0] ) { @@ -334,16 +325,8 @@ protected virtual void FormatStatusPart( Text format, IFormatProvider formatProvider ) { - if ( text == null ) - { - throw new ArgumentNullException( nameof( text ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } - + ArgumentNullException.ThrowIfNull( text ); + ArgumentNullException.ThrowIfNull( apiVersion ); text.Append( apiVersion.Status ); } diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionMetadata.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionMetadata.cs index 4211a665..d7e49d28 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionMetadata.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionMetadata.cs @@ -34,10 +34,7 @@ public class ApiVersionMetadata /// The other instance to initialize from. protected ApiVersionMetadata( ApiVersionMetadata other ) { - if ( other == null ) - { - throw new ArgumentNullException( nameof( other ) ); - } + ArgumentNullException.ThrowIfNull( other ); apiModel = other.apiModel; endpointModel = other.endpointModel; diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelDebugView.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelDebugView.cs index 4d0c3ddb..00cccc5b 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelDebugView.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelDebugView.cs @@ -2,16 +2,11 @@ namespace Asp.Versioning; -#pragma warning disable CA1812 - using static System.String; -internal sealed class ApiVersionModelDebugView +internal sealed class ApiVersionModelDebugView( ApiVersionModel model ) { private const string Comma = ", "; - private readonly ApiVersionModel model; - - public ApiVersionModelDebugView( ApiVersionModel model ) => this.model = model; public bool VersionNeutral => model.IsApiVersionNeutral; diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelExtensions.cs index e45e0f5c..efdc86f3 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelExtensions.cs @@ -17,15 +17,8 @@ public static class ApiVersionModelExtensions /// other version information and the current version information. public static ApiVersionModel Aggregate( this ApiVersionModel version, ApiVersionModel otherVersion ) { - if ( version == null ) - { - throw new ArgumentNullException( nameof( version ) ); - } - - if ( otherVersion == null ) - { - throw new ArgumentNullException( nameof( otherVersion ) ); - } + ArgumentNullException.ThrowIfNull( version ); + ArgumentNullException.ThrowIfNull( otherVersion ); var implemented = new SortedSet( version.ImplementedApiVersions ); var supported = new SortedSet( version.SupportedApiVersions ); @@ -50,15 +43,8 @@ public static ApiVersionModel Aggregate( this ApiVersionModel version, ApiVersio /// other version information and the current version information. public static ApiVersionModel Aggregate( this ApiVersionModel version, IEnumerable otherVersions ) { - if ( version == null ) - { - throw new ArgumentNullException( nameof( version ) ); - } - - if ( otherVersions == null ) - { - throw new ArgumentNullException( nameof( otherVersions ) ); - } + ArgumentNullException.ThrowIfNull( version ); + ArgumentNullException.ThrowIfNull( otherVersions ); if ( ( otherVersions is ICollection collection && collection.Count == 0 ) || ( otherVersions is IReadOnlyCollection readOnlyCollection && readOnlyCollection.Count == 0 ) ) @@ -99,10 +85,7 @@ public static ApiVersionModel Aggregate( this ApiVersionModel version, IEnumerab /// A new that is the aggregated result of the provided version information. public static ApiVersionModel Aggregate( this IEnumerable versions ) { - if ( versions == null ) - { - throw new ArgumentNullException( nameof( versions ) ); - } + ArgumentNullException.ThrowIfNull( versions ); if ( ( versions is ICollection collection && collection.Count == 0 ) || ( versions is IReadOnlyCollection readOnlyCollection && readOnlyCollection.Count == 0 ) ) diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionParser.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionParser.cs index 8bd12675..97ec10bb 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionParser.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionParser.cs @@ -2,7 +2,6 @@ #pragma warning disable IDE0079 #pragma warning disable SA1121 -#pragma warning disable SA1114 // Parameter list should follow declaration namespace Asp.Versioning; @@ -188,8 +187,6 @@ public virtual ApiVersion Parse( Text text ) /// #if NETSTANDARD1_0 public virtual bool TryParse( Text? text, out ApiVersion apiVersion ) -#elif NETSTANDARD2_0 - public virtual bool TryParse( Text text, out ApiVersion apiVersion ) #else public virtual bool TryParse( Text text, [MaybeNullWhen( false )] out ApiVersion apiVersion ) #endif @@ -339,11 +336,11 @@ public virtual bool TryParse( Text text, [MaybeNullWhen( false )] out ApiVersion [MethodImpl( MethodImplOptions.AggressiveInlining )] private static FormatException InvalidGroupVersion( string value ) => - new( string.Format( CultureInfo.CurrentCulture, SR.ApiVersionBadGroupVersion, value ) ); + new( string.Format( CultureInfo.CurrentCulture, Format.ApiVersionBadGroupVersion, value ) ); [MethodImpl( MethodImplOptions.AggressiveInlining )] private static FormatException InvalidStatus( string value ) => - new( string.Format( CultureInfo.CurrentCulture, SR.ApiVersionBadStatus, value ) ); + new( string.Format( CultureInfo.CurrentCulture, Format.ApiVersionBadStatus, value ) ); private static bool IsDateLike( Text value ) { diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionsBaseAttribute.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionsBaseAttribute.cs index 212e20ac..4c966ae5 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionsBaseAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionsBaseAttribute.cs @@ -96,7 +96,7 @@ protected ApiVersionsBaseAttribute( string version, params string[] otherVersion /// The parser used to parse the specified versions. /// The API version string. protected ApiVersionsBaseAttribute( IApiVersionParser parser, string version ) => - Versions = new[] { ( parser ?? throw new ArgumentNullException( nameof( parser ) ) ).Parse( version ) }; + Versions = new[] { ( parser ?? throw new System.ArgumentNullException( nameof( parser ) ) ).Parse( version ) }; /// /// Initializes a new instance of the class. @@ -106,10 +106,7 @@ protected ApiVersionsBaseAttribute( IApiVersionParser parser, string version ) = /// An array of API other version strings. protected ApiVersionsBaseAttribute( IApiVersionParser parser, string version, params string[] otherVersions ) { - if ( parser == null ) - { - throw new ArgumentNullException( nameof( parser ) ); - } + ArgumentNullException.ThrowIfNull( parser ); int count; 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 13cf5936..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,18 +1,22 @@  - + - 7.0.0 - 7.0.0.0 - net7.0;netstandard1.0;netstandard2.0 + 8.1.0 + 8.1.0.0 + $(DefaultTargetFramework);netstandard1.0;netstandard2.0 API Versioning Abstractions The abstractions library for API versioning. Asp.Versioning Asp;AspNet;AspNetCore;Versioning + + true + + - - + + @@ -22,21 +26,28 @@ - - + + - + + + + + + + + - + - + \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderBase.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderBase.cs index 4a431637..4ebe62bd 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderBase.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderBase.cs @@ -39,25 +39,25 @@ protected ApiVersionConventionBuilderBase() { } /// Gets the collection of API versions supported by the current controller. /// /// A collection of supported API versions. - protected ICollection SupportedVersions => supported ??= new(); + protected ICollection SupportedVersions => supported ??= []; /// /// Gets the collection of API versions deprecated by the current controller. /// /// A collection of deprecated API versions. - protected ICollection DeprecatedVersions => deprecated ??= new(); + protected ICollection DeprecatedVersions => deprecated ??= []; /// /// Gets the collection of API versions advertised by the current controller. /// /// A collection of advertised API versions. - protected ICollection AdvertisedVersions => advertised ??= new(); + protected ICollection AdvertisedVersions => advertised ??= []; /// /// Gets the collection of API versions advertised and deprecated by the current controller. /// /// A collection of advertised and deprecated API versions. - protected ICollection DeprecatedAdvertisedVersions => deprecatedAdvertised ??= new(); + protected ICollection DeprecatedAdvertisedVersions => deprecatedAdvertised ??= []; /// /// Merges API version information from the specified attributes with the current conventions. @@ -72,10 +72,7 @@ protected virtual void MergeAttributesWithConventions( IEnumerable attri /// The read-only list of attributes to merge. protected virtual void MergeAttributesWithConventions( IReadOnlyList attributes ) { - if ( attributes == null ) - { - throw new ArgumentNullException( nameof( attributes ) ); - } + ArgumentNullException.ThrowIfNull( attributes ); if ( VersionNeutral ) { @@ -102,19 +99,19 @@ protected virtual void MergeAttributesWithConventions( IReadOnlyList att switch ( provider.Options ) { case None: - target = newSupported ??= new(); + target = newSupported ??= []; source = provider.Versions; break; case Deprecated: - target = newDeprecated ??= new(); + target = newDeprecated ??= []; source = provider.Versions; break; case Advertised: - target = newAdvertised ??= new(); + target = newAdvertised ??= []; source = provider.Versions; break; case DeprecatedAdvertised: - target = newDeprecatedAdvertised ??= new(); + target = newDeprecatedAdvertised ??= []; source = provider.Versions; break; default: diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderExtensions.cs index 3813f478..9d528546 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Conventions/ApiVersionConventionBuilderExtensions.cs @@ -84,10 +84,7 @@ public static class ApiVersionConventionBuilderExtensions public static T HasApiVersions( this T builder, IEnumerable apiVersions ) where T : notnull, IDeclareApiVersionConventionBuilder { - if ( apiVersions == null ) - { - throw new ArgumentNullException( nameof( apiVersions ) ); - } + ArgumentNullException.ThrowIfNull( apiVersions ); foreach ( var apiVersion in apiVersions ) { @@ -170,10 +167,7 @@ public static T HasApiVersions( this T builder, IEnumerable apiVe public static T HasDeprecatedApiVersions( this T builder, IEnumerable apiVersions ) where T : notnull, IDeclareApiVersionConventionBuilder { - if ( apiVersions == null ) - { - throw new ArgumentNullException( nameof( apiVersions ) ); - } + ArgumentNullException.ThrowIfNull( apiVersions ); foreach ( var apiVersion in apiVersions ) { @@ -256,10 +250,7 @@ public static T HasDeprecatedApiVersions( this T builder, IEnumerable( this T builder, IEnumerable apiVersions ) where T : notnull, IDeclareApiVersionConventionBuilder { - if ( apiVersions == null ) - { - throw new ArgumentNullException( nameof( apiVersions ) ); - } + ArgumentNullException.ThrowIfNull( apiVersions ); foreach ( var apiVersion in apiVersions ) { @@ -342,10 +333,7 @@ public static T AdvertisesApiVersions( this T builder, IEnumerable( this T builder, IEnumerable apiVersions ) where T : notnull, IDeclareApiVersionConventionBuilder { - if ( apiVersions == null ) - { - throw new ArgumentNullException( nameof( apiVersions ) ); - } + ArgumentNullException.ThrowIfNull( apiVersions ); foreach ( var apiVersion in apiVersions ) { @@ -428,10 +416,7 @@ public static T AdvertisesDeprecatedApiVersions( this T builder, IEnumerable< public static T MapToApiVersions( this T builder, IEnumerable apiVersions ) where T : notnull, IMapToApiVersionConventionBuilder { - if ( apiVersions == null ) - { - throw new ArgumentNullException( nameof( apiVersions ) ); - } + ArgumentNullException.ThrowIfNull( apiVersions ); foreach ( var apiVersion in apiVersions ) { diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/Format.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/Format.cs new file mode 100644 index 00000000..dbea4469 --- /dev/null +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Format.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +#if NET +using System.Text; +#endif + +internal static class Format +{ +#if NETSTANDARD + internal static readonly string ApiVersionBadStatus = SR.ApiVersionBadStatus; + internal static readonly string ApiVersionBadGroupVersion = SR.ApiVersionBadGroupVersion; +#else + internal static readonly CompositeFormat ApiVersionBadStatus = CompositeFormat.Parse( SR.ApiVersionBadStatus ); + internal static readonly CompositeFormat ApiVersionBadGroupVersion = CompositeFormat.Parse( SR.ApiVersionBadGroupVersion ); +#endif +} \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionNeutral.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionNeutral.cs index a3ba5fd2..375541ef 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionNeutral.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionNeutral.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -namespace Asp.Versioning; +#pragma warning disable IDE0079 #pragma warning disable CA1040 +namespace Asp.Versioning; + /// /// Defines the behavior of an API that is version-neutral. /// diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionParameterSourceExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionParameterSourceExtensions.cs index 4a3dd194..55022bce 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionParameterSourceExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionParameterSourceExtensions.cs @@ -18,10 +18,7 @@ public static class IApiVersionParameterSourceExtensions /// True if the parameter source versions by query string; otherwise, false. public static bool VersionsByQueryString( this IApiVersionParameterSource source, bool allowMultipleLocations = true ) { - if ( source == null ) - { - throw new ArgumentNullException( nameof( source ) ); - } + ArgumentNullException.ThrowIfNull( source ); var context = new DescriptionContext( Query ); @@ -39,10 +36,7 @@ public static bool VersionsByQueryString( this IApiVersionParameterSource source /// True if the parameter source versions by HTTP header; otherwise, false. public static bool VersionsByHeader( this IApiVersionParameterSource source, bool allowMultipleLocations = true ) { - if ( source == null ) - { - throw new ArgumentNullException( nameof( source ) ); - } + ArgumentNullException.ThrowIfNull( source ); var context = new DescriptionContext( Header ); @@ -60,10 +54,7 @@ public static bool VersionsByHeader( this IApiVersionParameterSource source, boo /// True if the parameter source versions by URL path segment; otherwise, false. public static bool VersionsByUrl( this IApiVersionParameterSource source, bool allowMultipleLocations = true ) { - if ( source == null ) - { - throw new ArgumentNullException( nameof( source ) ); - } + ArgumentNullException.ThrowIfNull( source ); var context = new DescriptionContext( Path ); @@ -81,10 +72,7 @@ public static bool VersionsByUrl( this IApiVersionParameterSource source, bool a /// True if the parameter source versions by media type; otherwise, false. public static bool VersionsByMediaType( this IApiVersionParameterSource source, bool allowMultipleLocations = true ) { - if ( source == null ) - { - throw new ArgumentNullException( nameof( source ) ); - } + ArgumentNullException.ThrowIfNull( source ); var context = new DescriptionContext( MediaTypeParameter ); @@ -102,10 +90,7 @@ public static bool VersionsByMediaType( this IApiVersionParameterSource source, /// or null. public static string GetParameterName( this IApiVersionParameterSource source, ApiVersionParameterLocation location ) { - if ( source == null ) - { - throw new ArgumentNullException( nameof( source ) ); - } + ArgumentNullException.ThrowIfNull( source ); var context = new DescriptionContext( location ); @@ -122,10 +107,7 @@ public static string GetParameterName( this IApiVersionParameterSource source, A /// The names of the parameters defined by the parameter source for the specified . public static IReadOnlyList GetParameterNames( this IApiVersionParameterSource source, ApiVersionParameterLocation location ) { - if ( source == null ) - { - throw new ArgumentNullException( nameof( source ) ); - } + ArgumentNullException.ThrowIfNull( source ); var context = new DescriptionContext( location ); diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs index e2040ba2..048d6d49 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersioningPolicyBuilderExtensions.cs @@ -19,11 +19,7 @@ public static class IApiVersioningPolicyBuilderExtensions /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, string name ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( name, default ); } @@ -43,11 +39,7 @@ public static ISunsetPolicyBuilder Sunset( int? minorVersion = default, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( name, new ApiVersion( majorVersion, minorVersion, status ) ); } @@ -61,11 +53,7 @@ public static ISunsetPolicyBuilder Sunset( /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, string name, double version, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( name, new ApiVersion( version, status ) ); } @@ -81,11 +69,7 @@ public static ISunsetPolicyBuilder Sunset( /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, string name, int year, int month, int day, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( name, new ApiVersion( new DateOnly( year, month, day ), status ) ); } @@ -99,11 +83,7 @@ public static ISunsetPolicyBuilder Sunset( /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, string name, DateOnly groupVersion, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( name, new ApiVersion( groupVersion, status ) ); } @@ -115,11 +95,7 @@ public static ISunsetPolicyBuilder Sunset( /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, ApiVersion apiVersion ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( default, apiVersion ); } @@ -137,11 +113,7 @@ public static ISunsetPolicyBuilder Sunset( int? minorVersion = default, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( default, new ApiVersion( majorVersion, minorVersion, status ) ); } @@ -154,11 +126,7 @@ public static ISunsetPolicyBuilder Sunset( /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, double version, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( default, new ApiVersion( version, status ) ); } @@ -173,11 +141,7 @@ public static ISunsetPolicyBuilder Sunset( /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, int year, int month, int day, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( default, new ApiVersion( new DateOnly( year, month, day ), status ) ); } @@ -190,11 +154,7 @@ public static ISunsetPolicyBuilder Sunset( /// A new sunset policy builder. public static ISunsetPolicyBuilder Sunset( this IApiVersioningPolicyBuilder builder, DateOnly groupVersion, string? status = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Sunset( default, new ApiVersion( groupVersion, status ) ); } } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ILinkBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ILinkBuilderExtensions.cs index ccb9a64e..acf444f9 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ILinkBuilderExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ILinkBuilderExtensions.cs @@ -15,11 +15,7 @@ public static class ILinkBuilderExtensions /// A new link builder. public static ILinkBuilder Link( this ILinkBuilder builder, string linkTarget ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Link( new Uri( linkTarget, UriKind.RelativeOrAbsolute ) ); } } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs index 778f78d7..4a81ebd4 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyBuilderExtensions.cs @@ -15,11 +15,7 @@ public static class ISunsetPolicyBuilderExtensions /// A new link builder. public static ILinkBuilder Link( this ISunsetPolicyBuilder builder, string linkTarget ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.Link( new Uri( linkTarget, UriKind.RelativeOrAbsolute ) ); } @@ -35,11 +31,7 @@ public static ILinkBuilder Link( this ISunsetPolicyBuilder builder, string linkT public static TBuilder Effective( this TBuilder builder, int year, int month, int day ) where TBuilder : notnull, ISunsetPolicyBuilder { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); builder.Effective( new DateTimeOffset( new DateTime( year, month, day ) ) ); return builder; } diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs index 77a175a7..de2dd77e 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManager.cs @@ -18,11 +18,5 @@ public interface ISunsetPolicyManager /// policy for the specified API version. If /// API version is null, it is assumed the caller intends to match /// any sunset policy for the specified . - bool TryGetPolicy( - string? name, - ApiVersion? apiVersion, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out SunsetPolicy sunsetPolicy ); + bool TryGetPolicy( string? name, ApiVersion? apiVersion, [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ); } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs index 11e21279..f28f9cca 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs @@ -17,16 +17,9 @@ public static class ISunsetPolicyManagerExtensions public static bool TryGetPolicy( this ISunsetPolicyManager policyManager, ApiVersion apiVersion, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out SunsetPolicy sunsetPolicy ) + [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) { - if ( policyManager == null ) - { - throw new ArgumentNullException( nameof( policyManager ) ); - } - + ArgumentNullException.ThrowIfNull( policyManager ); return policyManager.TryGetPolicy( default, apiVersion, out sunsetPolicy ); } @@ -40,16 +33,9 @@ public static bool TryGetPolicy( public static bool TryGetPolicy( this ISunsetPolicyManager policyManager, string name, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out SunsetPolicy sunsetPolicy ) + [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) { - if ( policyManager == null ) - { - throw new ArgumentNullException( nameof( policyManager ) ); - } - + ArgumentNullException.ThrowIfNull( policyManager ); return policyManager.TryGetPolicy( name, default, out sunsetPolicy ); } @@ -60,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 @@ -72,10 +58,7 @@ public static bool TryGetPolicy( string? name, ApiVersion? apiVersion ) { - if ( policyManager == null ) - { - throw new ArgumentNullException( nameof( policyManager ) ); - } + ArgumentNullException.ThrowIfNull( policyManager ); if ( policyManager.TryResolvePolicy( name, apiVersion, out var policy ) ) { @@ -93,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 @@ -104,15 +87,9 @@ public static bool TryResolvePolicy( this ISunsetPolicyManager policyManager, string? name, ApiVersion? apiVersion, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out SunsetPolicy sunsetPolicy ) + [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) { - if ( policyManager == null ) - { - throw new ArgumentNullException( nameof( policyManager ) ); - } + ArgumentNullException.ThrowIfNull( policyManager ); if ( !string.IsNullOrEmpty( name ) ) { @@ -125,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/LinkHeaderValue.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/LinkHeaderValue.cs index fed3d08e..94088bc9 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/LinkHeaderValue.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/LinkHeaderValue.cs @@ -1,5 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 +#pragma warning disable SA1121 + namespace Asp.Versioning; #if !NETSTANDARD1_0 @@ -13,7 +16,6 @@ namespace Asp.Versioning; #endif #pragma warning disable IDE0079 -#pragma warning disable SA1121 /// /// Represents a HTTP Link header value. @@ -83,7 +85,7 @@ public StringSegment Language #endif if ( languages is null ) { - languages = new() { value }; + languages = [value]; } else if ( languages.Count == 0 ) { @@ -103,7 +105,7 @@ public StringSegment Language /// This is only a hint; for example, it does not override the Content-Language header field of /// a HTTP response obtained by actually following the link. A single link may indicate that multiple /// languages are available from the indicated resource. - public IList Languages => languages ??= new(); + public IList Languages => languages ??= []; /// /// Gets or sets the link media. @@ -159,10 +161,7 @@ public StringSegment Language public static bool TryParse( StringSegment input, Func? resolveRelativeUrl, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out LinkHeaderValue parsedValue ) + [MaybeNullWhen( false )] out LinkHeaderValue parsedValue ) { #if NETSTANDARD1_0 if ( string.IsNullOrEmpty( input ) ) @@ -207,11 +206,11 @@ public static bool TryParse( type = attribute.Value; break; case "hreflang": - languages ??= new(); + languages ??= []; languages.Add( attribute.Value ); break; default: - extensions ??= new(); + extensions ??= []; extensions.Add( attribute ); break; } @@ -300,9 +299,7 @@ public static bool TryParse( public static bool TryParseList( IList? input, Func? resolveRelativeUrl, -#if !NETSTANDARD [MaybeNullWhen( false )] -#endif out IList parsedValues ) { if ( input == null ) @@ -402,10 +399,7 @@ private static void AppendTargetAttribute( StringBuilder builder, ReadOnlySpan? resolveRelativeUrl, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out Uri targetLink ) + [MaybeNullWhen( false )] out Uri targetLink ) { var start = segment.IndexOf( '<' ); @@ -493,7 +487,7 @@ public bool Remove( KeyValuePair item ) => public bool TryGetValue( StringSegment key, -#if !NETSTANDARD +#if !NETSTANDARD1_0 [MaybeNullWhen( false )] #endif out StringSegment value ) => items.TryGetValue( key, out value ); @@ -538,16 +532,10 @@ private static ref StringSegment ValidateKey( ref StringSegment key ) } } - private struct TargetAttributesEnumerator : IEnumerable> + private struct TargetAttributesEnumerator( StringSegment remaining ) + : IEnumerable> { - private readonly StringSegment remaining; - private int start; - - public TargetAttributesEnumerator( StringSegment remaining ) - { - this.remaining = remaining; - start = 0; - } + private int start = 0; public IEnumerator> GetEnumerator() { @@ -570,7 +558,7 @@ public IEnumerator> GetEnumerator() } // REF: https://datatracker.ietf.org/doc/html/rfc8288#appendix-B.3 #9 -#pragma warning disable CA1308 // Normalize strings to uppercase +#pragma warning disable CA1308 // Normalize strings to uppercase (all ascii and should normalize to lowercase) #if NETSTANDARD1_0 var key = remaining.Substring( start, end - start ).ToLowerInvariant(); #else @@ -649,10 +637,13 @@ private static StringSegment RemoveQuotes( StringSegment input ) return input; } +#pragma warning disable IDE0056 // Use index operator private static bool IsQuoted( StringSegment input ) => -#pragma warning disable IDE0056 - !StringSegment.IsNullOrEmpty( input ) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"'; -#pragma warning restore IDE0056 + !StringSegment.IsNullOrEmpty( input ) && + input.Length >= 2 && + input[0] == '"' && + input[input.Length - 1] == '"'; +#pragma warning restore IDE0056 // Use index operator private static StringSegment UnescapeAsQuotedString( StringSegment input ) { diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs index 35c9aeb9..e90f5c58 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs @@ -1,12 +1,14 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 +#pragma warning disable CA1019 +#pragma warning disable CA1033 +#pragma warning disable CA1813 + namespace Asp.Versioning; using static System.AttributeTargets; -#pragma warning disable CA1019 -#pragma warning disable CA1813 - /// /// Represents the metadata that describes the version-specific implementation of an API. /// @@ -38,7 +40,5 @@ public MapToApiVersionAttribute( double version ) : base( version ) { } /// The API version string. public MapToApiVersionAttribute( string version ) : base( version ) { } -#pragma warning disable CA1033 ApiVersionProviderOptions IApiVersionProvider.Options => ApiVersionProviderOptions.Mapped; -#pragma warning restore CA1033 } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs index 46d64d23..a4246749 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs @@ -1,8 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0079 +#pragma warning disable SA1114 #pragma warning disable SA1121 -#pragma warning disable SA1114 // Parameter list should follow declaration namespace Asp.Versioning; @@ -53,10 +53,7 @@ public class NamespaceParser /// A read-only list of API verisons. public IReadOnlyList Parse( Type type ) { - if ( type == null ) - { - throw new ArgumentNullException( nameof( type ) ); - } + ArgumentNullException.ThrowIfNull( type ); if ( string.IsNullOrEmpty( type.Namespace ) ) { @@ -95,7 +92,7 @@ public IReadOnlyList Parse( Type type ) } else if ( versions is null ) { - versions = new() { version, result }; + versions = [version, result]; } else { diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/Str.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/Str.cs index 850e5049..0c899a4b 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/Str.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Str.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0079 -#pragma warning disable SA1121 // Use built-in type alias +#pragma warning disable SA1121 namespace Asp.Versioning; diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/net7.0/ApiVersion.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/net#.0/ApiVersion.cs similarity index 91% rename from src/Abstractions/src/Asp.Versioning.Abstractions/net7.0/ApiVersion.cs rename to src/Abstractions/src/Asp.Versioning.Abstractions/net#.0/ApiVersion.cs index 2a2db484..c9eb3b00 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/net7.0/ApiVersion.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/net#.0/ApiVersion.cs @@ -11,8 +11,10 @@ public partial class ApiVersion : ISpanFormattable public virtual bool TryFormat( Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider ) { var instance = ApiVersionFormatProvider.GetInstance( provider ); +#pragma warning disable IDE0079 #pragma warning disable CA1062 // Validate arguments of public methods return instance.TryFormat( destination, out charsWritten, format, this, provider ); #pragma warning restore CA1062 // Validate arguments of public methods +#pragma warning restore IDE0079 } } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard1.0/ApiVersionFormatProvider.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard1.0/ApiVersionFormatProvider.cs index faf0e6df..2cf84bb0 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard1.0/ApiVersionFormatProvider.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard1.0/ApiVersionFormatProvider.cs @@ -22,15 +22,8 @@ protected virtual void FormatGroupVersionPart( string? format, IFormatProvider? formatProvider ) { - if ( text == null ) - { - throw new ArgumentNullException( nameof( text ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( text ); + ArgumentNullException.ThrowIfNull( apiVersion ); if ( !apiVersion.GroupVersion.HasValue || string.IsNullOrEmpty( format ) ) { @@ -106,15 +99,8 @@ protected virtual void FormatAllParts( string? format, IFormatProvider? formatProvider ) { - if ( text == null ) - { - throw new ArgumentNullException( nameof( text ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( text ); + ArgumentNullException.ThrowIfNull( apiVersion ); if ( apiVersion.GroupVersion.HasValue ) { diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/AmbiguousApiVersionException.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/AmbiguousApiVersionException.cs index 7215f3e9..baf164e4 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/AmbiguousApiVersionException.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/AmbiguousApiVersionException.cs @@ -2,6 +2,7 @@ namespace Asp.Versioning; +using System.ComponentModel; using System.Runtime.Serialization; /// @@ -10,11 +11,23 @@ namespace Asp.Versioning; [Serializable] public partial class AmbiguousApiVersionException : Exception { + private const string LegacyFormatterImplMessage = "This API supports obsolete formatter-based serialization. It should not be called or extended by application code."; +#if NET + private const string LegacyFormatterImplDiagId = "SYSLIB0051"; + private const string SharedUrlFormat = "https://aka.ms/dotnet-warnings/{0}"; +#endif + /// /// Initializes a new instance of the class. /// /// The serialization info the exception is being deserialized with. /// The streaming context the exception is being deserialized from. +#if NET + [Obsolete( LegacyFormatterImplMessage, DiagnosticId = LegacyFormatterImplDiagId, UrlFormat = SharedUrlFormat )] +#else + [Obsolete( LegacyFormatterImplMessage )] +#endif + [EditorBrowsable( EditorBrowsableState.Never )] protected AmbiguousApiVersionException( SerializationInfo info, StreamingContext context ) : base( info, context ) => apiVersions = (string[]) info.GetValue( nameof( apiVersions ), typeof( string[] ) )!; @@ -23,6 +36,10 @@ protected AmbiguousApiVersionException( SerializationInfo info, StreamingContext /// /// The serialization info the exception is being serialized with. /// The streaming context the exception is being serialized in. +#if NET + [Obsolete( LegacyFormatterImplMessage, DiagnosticId = LegacyFormatterImplDiagId, UrlFormat = SharedUrlFormat )] +#endif + [EditorBrowsable( EditorBrowsableState.Never )] public override void GetObjectData( SerializationInfo info, StreamingContext context ) { base.GetObjectData( info, context ); diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/ApiVersionFormatProvider.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/ApiVersionFormatProvider.cs index ddf26eca..76ad58af 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/ApiVersionFormatProvider.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/ApiVersionFormatProvider.cs @@ -354,15 +354,8 @@ protected virtual void FormatGroupVersionPart( in ReadOnlySpan format, IFormatProvider? formatProvider ) { - if ( text == null ) - { - throw new ArgumentNullException( nameof( text ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( text ); + ArgumentNullException.ThrowIfNull( apiVersion ); if ( !apiVersion.GroupVersion.HasValue || format.IsEmpty ) { @@ -448,15 +441,8 @@ protected virtual void FormatAllParts( in ReadOnlySpan format, IFormatProvider? formatProvider ) { - if ( text == null ) - { - throw new ArgumentNullException( nameof( text ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( text ); + ArgumentNullException.ThrowIfNull( apiVersion ); Span buffer = stackalloc char[10]; diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/FormatWriter.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/FormatWriter.cs index 3fa0645f..b0c8019a 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/FormatWriter.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/FormatWriter.cs @@ -45,7 +45,7 @@ internal FormatWriter( public bool Succeeded { get; private set; } - public int Written => totalWritten; + public readonly int Written => totalWritten; public void Write( in FormatToken token ) { diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParser.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParser.cs index c806e8dd..cfbf616e 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParser.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParser.cs @@ -20,10 +20,5 @@ public interface IApiVersionParser /// The text to parse as an API version. /// The parsed API version or null. /// True if the parsing was successful; otherwise false. - bool TryParse( - ReadOnlySpan text, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out ApiVersion apiVersion ); + bool TryParse( ReadOnlySpan text, [MaybeNullWhen( false )] out ApiVersion apiVersion ); } \ No newline at end of file diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParserExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParserExtensions.cs index 1eabc3b4..3d23a71e 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParserExtensions.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/netstandard2.0/IApiVersionParserExtensions.cs @@ -15,11 +15,7 @@ public static class IApiVersionParserExtensions /// The parsed API version. public static ApiVersion Parse( this IApiVersionParser parser, string? text ) { - if ( parser == null ) - { - throw new ArgumentNullException( nameof( parser ) ); - } - + ArgumentNullException.ThrowIfNull( parser ); return parser.Parse( text == null ? default : text.AsSpan() ); } @@ -33,16 +29,9 @@ public static ApiVersion Parse( this IApiVersionParser parser, string? text ) public static bool TryParse( this IApiVersionParser parser, string? text, -#if !NETSTANDARD - [MaybeNullWhen( false )] -#endif - out ApiVersion apiVersion ) + [MaybeNullWhen( false )] out ApiVersion apiVersion ) { - if ( parser == null ) - { - throw new ArgumentNullException( nameof( parser ) ); - } - + ArgumentNullException.ThrowIfNull( parser ); return parser.TryParse( text == null ? default : text.AsSpan(), out apiVersion ); } } \ No newline at end of file diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs index 64b440a8..7b8d2245 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionFormatProviderTest.cs @@ -41,6 +41,7 @@ public void get_format_should_return_expected_format_provider() } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_allow_null_or_empty_format_string( ApiVersionFormatProvider provider ) { @@ -56,6 +57,7 @@ public void format_should_allow_null_or_empty_format_string( ApiVersionFormatPro } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_full_formatted_string_without_optional_components( ApiVersionFormatProvider provider ) { @@ -70,6 +72,7 @@ public void format_should_return_full_formatted_string_without_optional_componen } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_full_formatted_string_with_optional_components( ApiVersionFormatProvider provider ) { @@ -84,6 +87,7 @@ public void format_should_return_full_formatted_string_with_optional_components( } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_original_string_format_when_argument_cannot_be_formatted( ApiVersionFormatProvider provider ) { @@ -113,6 +117,7 @@ public void format_should_not_allow_malformed_literal_string( ApiVersionFormatPr } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( GroupVersionFormatData ) )] public void format_should_return_formatted_group_version_string( ApiVersionFormatProvider provider, string format ) { @@ -129,6 +134,7 @@ public void format_should_return_formatted_group_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_minor_version_string( ApiVersionFormatProvider provider ) { @@ -143,6 +149,7 @@ public void format_should_return_formatted_minor_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_major_version_string( ApiVersionFormatProvider provider ) { @@ -157,6 +164,7 @@ public void format_should_return_formatted_major_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_major_and_minor_version_string( ApiVersionFormatProvider provider ) { @@ -171,6 +179,7 @@ public void format_should_return_formatted_major_and_minor_version_string( ApiVe } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_short_version_string( ApiVersionFormatProvider provider ) { @@ -185,6 +194,7 @@ public void format_should_return_formatted_short_version_string( ApiVersionForma } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_long_version_string( ApiVersionFormatProvider provider ) { @@ -199,6 +209,7 @@ public void format_should_return_formatted_long_version_string( ApiVersionFormat } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( FormatProvidersData ) )] public void format_should_return_formatted_status_string( ApiVersionFormatProvider provider ) { @@ -213,6 +224,7 @@ public void format_should_return_formatted_status_string( ApiVersionFormatProvid } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( PaddedMinorVersionFormatData ) )] public void format_should_return_formatted_minor_version_with_padding_string( ApiVersionFormatProvider provider, string format ) { @@ -233,6 +245,7 @@ public void format_should_return_formatted_minor_version_with_padding_string( Ap } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( PaddedMajorVersionFormatData ) )] public void format_should_return_formatted_major_version_with_padding_string( ApiVersionFormatProvider provider, string format ) { @@ -253,6 +266,7 @@ public void format_should_return_formatted_major_version_with_padding_string( Ap } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( CustomFormatData ) )] public void format_should_return_custom_format_string( Func format, string expected ) { @@ -268,6 +282,7 @@ public void format_should_return_custom_format_string( Func } [Theory] + [AssumeCulture( "en-us" )] [MemberData( nameof( MultipleFormatParameterData ) )] public void format_should_return_formatted_string_with_multiple_parameters( ApiVersionFormatProvider provider, string format, object secondArgument, string expected ) { @@ -284,6 +299,7 @@ public void format_should_return_formatted_string_with_multiple_parameters( ApiV } [Fact] + [AssumeCulture( "en-us" )] public void format_should_return_formatted_string_with_escape_sequence() { // arrange diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionTest.cs index 07186a3e..11c896a5 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/ApiVersionTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: DX + namespace Asp.Versioning; public partial class ApiVersionTest @@ -595,27 +597,27 @@ public void api_version_1_ge_api_version_2_should_return_expected_result( string } public static IEnumerable FormatData => - new[] + new object[][] { - new[] { null, "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha" }, - new[] { "", "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha" }, - new[] { "F", "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha" }, - new[] { "G", "2013-08-06", "2013-08-06" }, - new[] { "GG", "2013-08-06-Alpha", "2013-08-06-Alpha" }, - new[] { "G", "1.1", "" }, - new[] { "G", "1.1-Alpha", "" }, - new[] { "G", "2013-08-06.1.1", "2013-08-06" }, - new[] { "GG", "2013-08-06.1.1-Alpha", "2013-08-06-Alpha" }, - new[] { "V", "2013-08-06", "" }, - new[] { "VVVV", "2013-08-06-Alpha", "" }, - new[] { "VV", "1.1", "1.1" }, - new[] { "VVVV", "1.1-Alpha", "1.1-Alpha" }, - new[] { "VV", "2013-08-06.1.1", "1.1" }, - new[] { "VVVV", "2013-08-06.1.1-Alpha", "1.1-Alpha" }, - new[] { "S", "1.1-Alpha", "Alpha" }, - new[] { "'v'VVV", "1.1", "v1.1" }, - new[] { "'Major': %V, 'Minor': %v", "1.1", "Major: 1, Minor: 1" }, - new[] { "MMM yyyy '('S')'", "2013-08-06-preview.1", "Aug 2013 (preview.1)" }, + [null, "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha"], + ["", "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha"], + ["F", "2013-08-06.1.1-Alpha", "2013-08-06.1.1-Alpha"], + ["G", "2013-08-06", "2013-08-06"], + ["GG", "2013-08-06-Alpha", "2013-08-06-Alpha"], + ["G", "1.1", ""], + ["G", "1.1-Alpha", ""], + ["G", "2013-08-06.1.1", "2013-08-06"], + ["GG", "2013-08-06.1.1-Alpha", "2013-08-06-Alpha"], + ["V", "2013-08-06", ""], + ["VVVV", "2013-08-06-Alpha", ""], + ["VV", "1.1", "1.1"], + ["VVVV", "1.1-Alpha", "1.1-Alpha"], + ["VV", "2013-08-06.1.1", "1.1"], + ["VVVV", "2013-08-06.1.1-Alpha", "1.1-Alpha"], + ["S", "1.1-Alpha", "Alpha"], + ["'v'VVV", "1.1", "v1.1"], + ["'Major': %V, 'Minor': %v", "1.1", "Major: 1, Minor: 1"], + ["MMM yyyy '('S')'", "2013-08-06-preview.1", "Aug 2013 (preview.1)"], }; #if NETFRAMEWORK diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj index a7c04d59..e2fcdd62 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/Asp.Versioning.Abstractions.Tests.csproj @@ -1,14 +1,14 @@  - net7.0;net472;net452 + $(DefaultTargetFramework);net452;net472 Asp.Versioning - - - + + + - + diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/AssumeCultureAttribute.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/AssumeCultureAttribute.cs new file mode 100644 index 00000000..9c956d87 --- /dev/null +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/AssumeCultureAttribute.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Globalization; +using System.Reflection; +using Xunit.Sdk; +using static System.AttributeTargets; +using static System.Threading.Thread; + +/// +/// Allows a test method to assume that it is running in a specific locale. +/// +[AttributeUsage( Class | Method, AllowMultiple = false, Inherited = true )] +public sealed class AssumeCultureAttribute : BeforeAfterTestAttribute +{ + private CultureInfo originalCulture; + private CultureInfo originalUICulture; + + public AssumeCultureAttribute( string name ) => Name = name; + + public string Name { get; } + + public override void Before( MethodInfo methodUnderTest ) + { + originalCulture = CurrentThread.CurrentCulture; + originalUICulture = CurrentThread.CurrentUICulture; + + var culture = CultureInfo.CreateSpecificCulture( Name ); + + CurrentThread.CurrentCulture = culture; + CurrentThread.CurrentUICulture = culture; + } + + public override void After( MethodInfo methodUnderTest ) + { + CurrentThread.CurrentCulture = originalCulture; + CurrentThread.CurrentUICulture = originalUICulture; + } +} \ No newline at end of file diff --git a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IApiVersionParameterSourceExtensionsTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IApiVersionParameterSourceExtensionsTest.cs index b1186e85..486977b0 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IApiVersionParameterSourceExtensionsTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/IApiVersionParameterSourceExtensionsTest.cs @@ -146,6 +146,7 @@ public void get_parameter_names_should_return_matching_names() { // arrange var source = new Mock(); + var expected = new[] { "api-version", "ver" }; source.Setup( s => s.AddParameters( It.IsAny() ) ) .Callback( ( IApiVersionParameterDescriptionContext context ) => @@ -160,6 +161,6 @@ public void get_parameter_names_should_return_matching_names() var names = source.Object.GetParameterNames( Query ); // assert - names.Should().BeEquivalentTo( new[] { "api-version", "ver" } ); + names.Should().BeEquivalentTo( expected ); } } \ 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/Abstractions/test/Asp.Versioning.Abstractions.Tests/net7.0/ApiVersionTest.cs b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net#.0/ApiVersionTest.cs similarity index 95% rename from src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net7.0/ApiVersionTest.cs rename to src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net#.0/ApiVersionTest.cs index b650303b..b1b6f794 100644 --- a/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net7.0/ApiVersionTest.cs +++ b/src/Abstractions/test/Asp.Versioning.Abstractions.Tests/net#.0/ApiVersionTest.cs @@ -6,6 +6,7 @@ public partial class ApiVersionTest { [Theory] [MemberData( nameof( FormatData ) )] + [AssumeCulture( "en-us" )] public void try_format_format_should_return_expected_string( string format, string text, string formattedString ) { // arrange diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Asp.Versioning.WebApi.Acceptance.Tests.csproj b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Asp.Versioning.WebApi.Acceptance.Tests.csproj index 4a958c4f..29f5fbf5 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Asp.Versioning.WebApi.Acceptance.Tests.csproj +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Asp.Versioning.WebApi.Acceptance.Tests.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OrdersController.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OrdersController.cs index 84f14bd6..d9ffd452 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OrdersController.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OrdersController.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0060 + namespace Asp.Versioning.Http.Basic.Controllers; using Asp.Versioning.Http.Basic.Models; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OverlappingRouteTemplateController.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OverlappingRouteTemplateController.cs index 320ec366..4907c19e 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OverlappingRouteTemplateController.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/Controllers/OverlappingRouteTemplateController.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0060 + namespace Asp.Versioning.Http.Basic.Controllers; using System.Web.Http; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/InteropFixture.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/InteropFixture.cs new file mode 100644 index 00000000..b61d1d68 --- /dev/null +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/InteropFixture.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +// Ignore Spelling: Interop +namespace Asp.Versioning.Http.Basic; + +using System.Web.Http; + +public class InteropFixture : BasicFixture +{ + protected override void OnConfigure( HttpConfiguration configuration ) + { + configuration.ConvertProblemDetailsToErrorObject(); + base.OnConfigure( configuration ); + } +} \ No newline at end of file diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when error objects are enabled.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when error objects are enabled.cs new file mode 100644 index 00000000..b40bfa14 --- /dev/null +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when error objects are enabled.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace given_a_versioned_ApiController; + +using Asp.Versioning; +using Asp.Versioning.Http.Basic; + +public class when_error_objects_are_enabled : AcceptanceTest, IClassFixture +{ + [Fact] + public async Task then_the_response_should_not_be_problem_details() + { + // arrange + var example = new + { + error = new + { + code = default( string ), + message = default( string ), + target = default( string ), + innerError = new + { + message = default( string ), + }, + }, + }; + + // act + var response = await GetAsync( "api/values?api-version=3.0" ); + var error = await response.Content.ReadAsExampleAsync( example ); + + // assert + response.Content.Headers.ContentType.MediaType.Should().Be( "application/json" ); + error.Should().BeEquivalentTo( + new + { + error = new + { + code = "UnsupportedApiVersion", + message = "Unsupported API version", + innerError = new + { + message = "No route providing a controller name with API version '3.0' " + + "was found to match request URI 'http://localhost/api/values'.", + }, + }, + } ); + } + + public when_error_objects_are_enabled( InteropFixture fixture ) : base( fixture ) { } +} \ No newline at end of file diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs index fab6d7d1..cffde314 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/Basic/given a versioned ApiController/when using a query string and split into two types.cs @@ -34,7 +34,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -44,7 +44,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/Controllers/OrdersController.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/Controllers/OrdersController.cs index 7e6d3e27..1b52ff0d 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/Controllers/OrdersController.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/Controllers/OrdersController.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0060 + namespace Asp.Versioning.Http.UsingConventions.Controllers; using Asp.Versioning.Http.UsingConventions.Models; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs index 08ecf18c..1080b60e 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingConventions/given a versioned ApiController using conventions/when using a query string and split into two types.cs @@ -35,7 +35,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -45,7 +45,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/Controllers/V1/OrdersController.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/Controllers/V1/OrdersController.cs index 8e19bda5..d547a271 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/Controllers/V1/OrdersController.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/Controllers/V1/OrdersController.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0060 + namespace Asp.Versioning.Http.UsingNamespace.Controllers.V1; using Asp.Versioning.Http.UsingNamespace.Models; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs index 652b9347..06c84e76 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a query string.cs @@ -32,7 +32,7 @@ public async Task then_get_should_return_200( Type controllerType, string apiVer } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -42,7 +42,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and attribute-based routing.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and attribute-based routing.cs index 0beae68c..8cd50259 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and attribute-based routing.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and attribute-based routing.cs @@ -2,6 +2,8 @@ #pragma warning disable IDE1006 // Naming Styles +//// Ignore Spelling: Dbased + namespace given_a_versioned_ApiController_per_namespace; using Asp.Versioning; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and convention-based routing.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and convention-based routing.cs index 63801fb2..88f11bf8 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and convention-based routing.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/Http/UsingNamespace/given a versioned ApiController per namespace/when using a url segment and convention-based routing.cs @@ -2,6 +2,8 @@ #pragma warning disable IDE1006 // Naming Styles +//// Ignore Spelling: Dbased + namespace given_a_versioned_ApiController_per_namespace; using Asp.Versioning; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs index 9070597e..b354d6f6 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with Web API controllers/when people is any version.cs @@ -9,7 +9,7 @@ namespace given_a_versioned_ODataController_mixed_with_Web_API_controllers; public class when_people_is_any_version : AdvancedAcceptanceTest { [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { lastName = "Me" }; @@ -19,7 +19,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs index 26c45c91..3d05e49f 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0060 // Remove unused parameter -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.OData.Basic.Controllers; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs index f32b79c3..ad4f5364 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -38,7 +38,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } @@ -73,7 +73,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version( stri } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -83,7 +83,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs index 591fc75f..b73cde2c 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs @@ -24,7 +24,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -34,7 +34,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs index 1515dd37..27b3e2d9 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs index ed7eb5a7..05ec159e 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs index 1ebc086a..302e5c38 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs index 83609324..357bb455 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs index 32491a77..9c420a43 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dspecific + namespace Asp.Versioning.OData; using static System.Net.HttpStatusCode; @@ -37,7 +39,7 @@ public async Task then_the_service_document_should_be_versionX2Dspecific( string } [Fact] - public async Task then_the_service_document_should_return_404_for_an_unsupported_version() + public async Task then_the_service_document_should_return_400_for_an_unsupported_version() { // arrange @@ -47,7 +49,7 @@ public async Task then_the_service_document_should_return_404_for_an_unsupported var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } @@ -83,7 +85,7 @@ public async Task then_X24metadata_should_be_versionX2Dspecific( string apiVersi } [Fact] - public async Task then_X24metadata_should_return_404_for_an_unsupported_version() + public async Task then_X24metadata_should_return_400_for_an_unsupported_version() { // arrange Client.DefaultRequestHeaders.Clear(); @@ -93,7 +95,7 @@ public async Task then_X24metadata_should_return_404_for_an_unsupported_version( var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs index 0cd5170d..46e15b8d 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -38,7 +38,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } @@ -73,7 +73,7 @@ public async Task then_patch_should_return_405_if_supported_in_any_version( stri } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -83,7 +83,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs index ba475388..4956d346 100644 --- a/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs +++ b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs @@ -24,7 +24,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -34,7 +34,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var problem = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); } diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/AdHocEdmScope.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/AdHocEdmScope.cs new file mode 100644 index 00000000..f6744216 --- /dev/null +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/AdHocEdmScope.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning.Conventions; +using Asp.Versioning.Description; +using Asp.Versioning.OData; +using Microsoft.OData.Edm; +using System.Collections.Generic; +using System.Web.Http; +using System.Web.Http.Description; + +internal sealed class AdHocEdmScope : IDisposable +{ + private readonly IReadOnlyList results; + private bool disposed; + + internal AdHocEdmScope( + IReadOnlyList apiDescriptions, + VersionedODataModelBuilder builder ) + { + var conventions = builder.ModelConfigurations.OfType().ToArray(); + + results = FilterResults( apiDescriptions, conventions ); + + if ( results.Count > 0 ) + { + ApplyAdHocEdm( builder.GetEdmModels(), results ); + } + } + + public void Dispose() + { + if ( disposed ) + { + return; + } + + disposed = true; + + for ( var i = 0; i < results.Count; i++ ) + { + results[i].SetProperty( default( IEdmModel ) ); + } + } + + private static IReadOnlyList FilterResults( + IReadOnlyList apiDescriptions, + IReadOnlyList conventions ) + { + if ( conventions.Count == 0 ) + { + return Array.Empty(); + } + + var results = default( List ); + + for ( var i = 0; i < apiDescriptions.Count; i++ ) + { + var apiDescription = apiDescriptions[i]; + + if ( apiDescription.EdmModel() != null || !apiDescription.IsODataLike() ) + { + continue; + } + + results ??= []; + results.Add( apiDescription ); + + for ( var j = 0; j < conventions.Count; j++ ) + { + conventions[j].ApplyTo( apiDescription ); + } + } + + return results?.ToArray() ?? []; + } + + private static void ApplyAdHocEdm( + IReadOnlyList models, + IReadOnlyList results ) + { + for ( var i = 0; i < models.Count; i++ ) + { + var model = models[i]; + var version = model.GetApiVersion(); + + for ( var j = 0; j < results.Count; j++ ) + { + var result = results[j]; + var metadata = result.ActionDescriptor.GetApiVersionMetadata(); + + if ( metadata.IsMappedTo( version ) ) + { + result.SetProperty( model ); + } + } + } + } +} \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs index f6fda7f9..bb3eed90 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorer.cs @@ -7,6 +7,7 @@ namespace Asp.Versioning.ApiExplorer; using Asp.Versioning.OData; using Asp.Versioning.Routing; using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Builder; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Formatter; using Microsoft.AspNet.OData.Routing; @@ -16,6 +17,7 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.OData.UriParser; using System.Collections.ObjectModel; using System.Net.Http.Formatting; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Web.Http; using System.Web.Http.Controllers; @@ -50,7 +52,11 @@ public ODataApiExplorer( HttpConfiguration configuration ) /// The current HTTP configuration. /// The associated API explorer options. public ODataApiExplorer( HttpConfiguration configuration, ODataApiExplorerOptions options ) - : base( configuration, options ) => this.options = options; + : base( configuration, options ) + { + this.options = options; + options.AdHocModelBuilder.OnModelCreated += MarkAsAdHoc; + } /// /// Gets the options associated with the API explorer. @@ -72,10 +78,7 @@ protected override bool ShouldExploreAction( IHttpRoute route, ApiVersion apiVersion ) { - if ( actionDescriptor == null ) - { - throw new ArgumentNullException( nameof( actionDescriptor ) ); - } + ArgumentNullException.ThrowIfNull( actionDescriptor ); if ( route is not ODataRoute ) { @@ -120,15 +123,8 @@ protected override bool ShouldExploreController( IHttpRoute route, ApiVersion apiVersion ) { - if ( controllerDescriptor == null ) - { - throw new ArgumentNullException( nameof( controllerDescriptor ) ); - } - - if ( route == null ) - { - throw new ArgumentNullException( nameof( route ) ); - } + ArgumentNullException.ThrowIfNull( controllerDescriptor ); + ArgumentNullException.ThrowIfNull( route ); if ( controllerDescriptor.ControllerType.IsMetadataController() ) { @@ -162,20 +158,30 @@ protected override Collection ExploreRouteControllers( IHttpRoute route, ApiVersion apiVersion ) { - if ( controllerMappings == null ) - { - throw new ArgumentNullException( nameof( controllerMappings ) ); - } + ArgumentNullException.ThrowIfNull( controllerMappings ); Collection apiDescriptions; if ( route is not ODataRoute ) { apiDescriptions = base.ExploreRouteControllers( controllerMappings, route, apiVersion ); - return ExploreQueryOptions( route, apiDescriptions ); + + if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + else if ( apiDescriptions.Count > 0 ) + { + using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + } + + return apiDescriptions; } - apiDescriptions = new(); + apiDescriptions = []; var modelSelector = Configuration.GetODataRootContainer( route ).GetRequiredService(); var edmModel = modelSelector.SelectModel( apiVersion ); @@ -199,7 +205,8 @@ protected override Collection ExploreRouteControllers( } } - return ExploreQueryOptions( route, apiDescriptions ); + ExploreQueryOptions( route, apiDescriptions ); + return apiDescriptions; } /// @@ -210,7 +217,25 @@ protected override Collection ExploreDirectRouteControl ApiVersion apiVersion ) { var apiDescriptions = base.ExploreDirectRouteControllers( controllerDescriptor, candidateActionDescriptors, route, apiVersion ); - return ExploreQueryOptions( route, apiDescriptions ); + + if ( apiDescriptions.Count == 0 ) + { + return apiDescriptions; + } + + if ( Options.AdHocModelBuilder.ModelConfigurations.Count == 0 ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + else if ( apiDescriptions.Count > 0 ) + { + using ( new AdHocEdmScope( apiDescriptions, Options.AdHocModelBuilder ) ) + { + ExploreQueryOptions( route, apiDescriptions ); + } + } + + return apiDescriptions; } /// @@ -222,10 +247,7 @@ protected virtual void ExploreQueryOptions( IEnumerable apiDescriptions, ODataUriResolver uriResolver ) { - if ( uriResolver == null ) - { - throw new ArgumentNullException( nameof( uriResolver ) ); - } + ArgumentNullException.ThrowIfNull( uriResolver ); var queryOptions = Options.QueryOptions; var settings = new ODataQueryOptionSettings() @@ -238,20 +260,20 @@ protected virtual void ExploreQueryOptions( queryOptions.ApplyTo( apiDescriptions, settings ); } - private Collection ExploreQueryOptions( - IHttpRoute route, - Collection apiDescriptions ) + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) => + model.SetAnnotationValue( model, AdHocAnnotation.Instance ); + + private void ExploreQueryOptions( IHttpRoute route, Collection apiDescriptions ) { if ( apiDescriptions.Count == 0 ) { - return apiDescriptions; + return; } var uriResolver = Configuration.GetODataRootContainer( route ).GetRequiredService(); ExploreQueryOptions( apiDescriptions, uriResolver ); - - return apiDescriptions; } private ResponseDescription CreateResponseDescriptionWithRoute( diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs index b9d34d6b..4bf5b113 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs @@ -15,7 +15,8 @@ public partial class ODataApiExplorerOptions : ApiExplorerOptions /// Initializes a new instance of the class. /// /// The current configuration associated with the options. - public ODataApiExplorerOptions( HttpConfiguration configuration ) : base( configuration ) { } + public ODataApiExplorerOptions( HttpConfiguration configuration ) + : base( configuration ) => AdHocModelBuilder = new( configuration ); /// /// Gets or sets a value indicating whether the API explorer settings are honored. diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj index 44cd9e06..b8bedf18 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Asp.Versioning.WebApi.OData.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 7.1.0 + 7.1.0.0 net45;net472 Asp.Versioning ASP.NET Web API Versioning API Explorer for OData v4.0 @@ -15,8 +15,12 @@ + + + + @@ -27,6 +31,13 @@ + + + + + + + diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs new file mode 100644 index 00000000..771af4de --- /dev/null +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Conventions; + +using Asp.Versioning.OData; +using System.Web.Http.Description; + +/// +/// Provides additional implementation specific to ASP.NET Web API. +/// +public partial class ImplicitModelBoundSettingsConvention : IModelConfiguration, IODataQueryOptionsConvention +{ + /// + public void ApplyTo( ApiDescription apiDescription ) + { + var response = apiDescription.ResponseDescription; + var type = response.ResponseType ?? response.DeclaredType; + + if ( type == null ) + { + return; + } + + if ( type.IsEnumerable( out var itemType ) ) + { + type = itemType; + } + + types.Add( type! ); + } +} \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/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/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs index 04f8cf6d..8b297ebb 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs @@ -2,7 +2,6 @@ namespace Asp.Versioning.Conventions; -using Microsoft.AspNet.OData; using System.Runtime.CompilerServices; using System.Web.Http.Description; @@ -14,20 +13,4 @@ public partial class ODataQueryOptionsConventionBuilder [MethodImpl( MethodImplOptions.AggressiveInlining )] private static Type GetController( ApiDescription apiDescription ) => apiDescription.ActionDescriptor.ControllerDescriptor.ControllerType; - - [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static bool IsODataLike( ApiDescription description ) - { - var parameters = description.ParameterDescriptions; - - for ( var i = 0; i < parameters.Count; i++ ) - { - if ( parameters[i].ParameterDescriptor.ParameterType.IsODataQueryOptions() ) - { - return true; - } - } - - return false; - } } \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs index 7b008e3a..087a1b21 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs @@ -19,10 +19,7 @@ public partial class ODataValidationSettingsConvention /// public virtual void ApplyTo( ApiDescription apiDescription ) { - if ( apiDescription == null ) - { - throw new ArgumentNullException( nameof( apiDescription ) ); - } + ArgumentNullException.ThrowIfNull( apiDescription ); if ( !IsSupported( apiDescription.HttpMethod.Method ) ) { diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Routing/ODataRouteBuilderContext.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Routing/ODataRouteBuilderContext.cs index 23fe0a48..3ff17b36 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Routing/ODataRouteBuilderContext.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Routing/ODataRouteBuilderContext.cs @@ -355,7 +355,7 @@ private static bool IsNavigationPropertyLink( IEdmEntitySet? entitySet, IEdmSing { if ( propertyNames is null ) { - propertyNames = new(); + propertyNames = []; } else { diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/SR.Designer.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/SR.Designer.cs new file mode 100644 index 00000000..978b3fa3 --- /dev/null +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/SR.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Asp.Versioning { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class SR { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SR() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Asp.Versioning.SR", typeof(SR).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The value cannot be an empty string.. + /// + internal static string Argument_EmptyString { + get { + return ResourceManager.GetString("Argument_EmptyString", resourceCulture); + } + } + } +} diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/SR.resx b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/SR.resx new file mode 100644 index 00000000..9222b107 --- /dev/null +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/SR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The value cannot be an empty string. + + \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs index 3c19916d..cba8d13e 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/System.Web.Http/Description/ApiDescriptionExtensions.cs @@ -1,100 +1,114 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -namespace System.Web.Http.Description -{ - using Asp.Versioning.Description; - using Microsoft.AspNet.OData.Routing; - using Microsoft.OData.Edm; +namespace System.Web.Http.Description; + +using Asp.Versioning.Description; +using Microsoft.AspNet.OData.Routing; +using Microsoft.OData.Edm; +/// +/// Provides extension methods for the class. +/// +public static class ApiDescriptionExtensions +{ /// - /// Provides extension methods for the class. + /// Gets the entity data model (EDM) associated with the API description. /// - public static class ApiDescriptionExtensions + /// The API description to get the model for. + /// The associated EDM model or null if there is no associated model. + public static IEdmModel? EdmModel( this ApiDescription apiDescription ) { - /// - /// Gets the entity data model (EDM) associated with the API description. - /// - /// The API description to get the model for. - /// The associated EDM model or null if there is no associated model. - public static IEdmModel? EdmModel( this ApiDescription apiDescription ) + if ( apiDescription is VersionedApiDescription description ) { - if ( apiDescription is VersionedApiDescription description ) - { - return description.GetProperty(); - } + return description.GetProperty(); + } + return default; + } + + /// + /// Gets the entity set associated with the API description. + /// + /// The API description to get the entity set for. + /// The associated entity set or null if there is no associated entity set. + public static IEdmEntitySet? EntitySet( this ApiDescription apiDescription ) + { + if ( apiDescription is not VersionedApiDescription description ) + { return default; } - /// - /// Gets the entity set associated with the API description. - /// - /// The API description to get the entity set for. - /// The associated entity set or null if there is no associated entity set. - public static IEdmEntitySet? EntitySet( this ApiDescription apiDescription ) + var key = typeof( IEdmEntitySet ); + + if ( description.Properties.TryGetValue( key, out var value ) ) { - if ( apiDescription is not VersionedApiDescription description ) - { - return default; - } + return (IEdmEntitySet) value; + } - var key = typeof( IEdmEntitySet ); + var container = description.EdmModel()?.EntityContainer; - if ( description.Properties.TryGetValue( key, out var value ) ) - { - return (IEdmEntitySet) value; - } + if ( container == null ) + { + return default; + } - var container = description.EdmModel()?.EntityContainer; + var entitySetName = description.ActionDescriptor.ControllerDescriptor.ControllerName; + var entitySet = container.FindEntitySet( entitySetName ); - if ( container == null ) - { - return default; - } + description.Properties[key] = entitySet; - var entitySetName = description.ActionDescriptor.ControllerDescriptor.ControllerName; - var entitySet = container.FindEntitySet( entitySetName ); + return entitySet; + } - description.Properties[key] = entitySet; + /// + /// Gets the entity type associated with the API description. + /// + /// The API description to get the entity type for. + /// The associated entity type or null if there is no associated entity type. + public static IEdmEntityType? EntityType( this ApiDescription apiDescription ) => apiDescription.EntitySet()?.EntityType(); - return entitySet; + /// + /// Gets the operation associated with the API description. + /// + /// The API description to get the operation for. + /// The associated EDM operation or null if there is no associated operation. + public static IEdmOperation? Operation( this ApiDescription apiDescription ) + { + if ( apiDescription is VersionedApiDescription description ) + { + return description.GetProperty(); } - /// - /// Gets the entity type associated with the API description. - /// - /// The API description to get the entity type for. - /// The associated entity type or null if there is no associated entity type. - public static IEdmEntityType? EntityType( this ApiDescription apiDescription ) => apiDescription.EntitySet()?.EntityType(); - - /// - /// Gets the operation associated with the API description. - /// - /// The API description to get the operation for. - /// The associated EDM operation or null if there is no associated operation. - public static IEdmOperation? Operation( this ApiDescription apiDescription ) - { - if ( apiDescription is VersionedApiDescription description ) - { - return description.GetProperty(); - } + return default; + } - return default; + /// + /// Gets the route prefix associated with the API description. + /// + /// The API description to get the route prefix for. + /// The associated route prefix or null. + public static string? RoutePrefix( this ApiDescription apiDescription ) + { + if ( apiDescription == null ) + { + throw new ArgumentNullException( nameof( apiDescription ) ); } - /// - /// Gets the route prefix associated with the API description. - /// - /// The API description to get the route prefix for. - /// The associated route prefix or null. - public static string? RoutePrefix( this ApiDescription apiDescription ) + return apiDescription.Route is ODataRoute route ? route.RoutePrefix : default; + } + + internal static bool IsODataLike( this ApiDescription description ) + { + var parameters = description.ParameterDescriptions; + + for ( var i = 0; i < parameters.Count; i++ ) { - if ( apiDescription == null ) + if ( parameters[i].ParameterDescriptor.ParameterType.IsODataQueryOptions() ) { - throw new ArgumentNullException( nameof( apiDescription ) ); + return true; } - - return apiDescription.Route is ODataRoute route ? route.RoutePrefix : default; } + + return false; } } \ No newline at end of file diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj index 504275fe..7229ae69 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 7.1.0 + 7.1.0.0 net45;net472 Asp.Versioning API Versioning for ASP.NET Web API with OData v4.0 diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs index cba6623f..ed7497b9 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/EdmModelSelector.cs @@ -53,8 +53,8 @@ public EdmModelSelector( IEnumerable models, IApiVersionSelector apiV break; default: - versions = new(); - collection = new(); + versions = []; + collection = []; foreach ( var model in models ) { @@ -130,16 +130,12 @@ public EdmModelSelector( IEnumerable models, IApiVersionSelector apiV private static void AddVersionFromModel( IEdmModel model, IList versions, IDictionary collection ) { - var annotation = model.GetAnnotationValue( model ); - - if ( annotation == null ) + if ( model.GetApiVersion() is not ApiVersion version ) { var message = string.Format( CultureInfo.CurrentCulture, SR.MissingAnnotation, typeof( ApiVersionAnnotation ).Name ); throw new ArgumentException( message ); } - var version = annotation.ApiVersion; - collection.Add( version, model ); versions.Add( version ); } diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/VersionedODataModelBuilder.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/VersionedODataModelBuilder.cs index 7f493939..f08a3c18 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/VersionedODataModelBuilder.cs +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/OData/VersionedODataModelBuilder.cs @@ -78,7 +78,7 @@ protected virtual IReadOnlyList GetApiVersions() if ( versions.Count > 0 && supported == null ) { - supported = new(); + supported = []; } for ( var j = 0; j < versions.Count; j++ ) @@ -90,7 +90,7 @@ protected virtual IReadOnlyList GetApiVersions() if ( versions.Count > 0 && deprecated == null ) { - deprecated = new(); + deprecated = []; } for ( var j = 0; j < versions.Count; j++ ) diff --git a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt +++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs index 1a255dad..72c1025f 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs @@ -1,5 +1,10 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dlike +//// Ignore Spelling: Multipart +//// Ignore Spelling: nonaction +//// Ignore Spelling: nonquery + namespace Asp.Versioning.Conventions; using Asp.Versioning.Description; @@ -10,7 +15,6 @@ namespace Asp.Versioning.Conventions; using Microsoft.AspNet.OData.Extensions; using Microsoft.AspNet.OData.Query; using Microsoft.OData.Edm; -using System.Collections.ObjectModel; using System.Net.Http; using System.Reflection; using System.Web.Http; @@ -565,8 +569,8 @@ public static IEnumerable EnableQueryAttributeData var controller = new Mock() { CallBase = true }; var action = new Mock() { CallBase = true }; - controller.Setup( m => m.GetCustomAttributes( It.IsAny() ) ).Returns( new Collection() ); - action.Setup( m => m.GetCustomAttributes( It.IsAny() ) ).Returns( new Collection() ); + controller.Setup( m => m.GetCustomAttributes( It.IsAny() ) ).Returns( [] ); + action.Setup( m => m.GetCustomAttributes( It.IsAny() ) ).Returns( [] ); var actionDescriptor = action.Object; var responseType = singleResult ? typeof( object ) : typeof( IEnumerable ); @@ -604,7 +608,6 @@ private static ApiDescription NewApiDescription( Type controllerType, Type respo } #pragma warning disable IDE0060 // Remove unused parameter -#pragma warning disable CA1034 // Nested types should not be visible public class SinglePartController : ODataController { diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs index 534d10a3..34540a00 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Description/ODataApiExplorerTest.cs @@ -3,9 +3,13 @@ namespace Asp.Versioning.Description; using Asp.Versioning.ApiExplorer; +using Asp.Versioning.Controllers; +using Asp.Versioning.Conventions; using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Extensions; using System.Net.Http; using System.Web.Http; +using System.Web.Http.Dispatcher; using static System.Net.Http.HttpMethod; public class ODataApiExplorerTest @@ -210,4 +214,34 @@ public void api_description_group_should_explore_navigation_properties() }, options => options.ExcludingMissingMembers() ); } + + [Fact] + public void api_description_group_should_explore_model_bound_settings() + { + // arrange + var configuration = new HttpConfiguration(); + var controllerTypeResolver = new ControllerTypeCollection( + typeof( VersionedMetadataController ), + typeof( Simulators.V1.BooksController ) ); + + configuration.Services.Replace( typeof( IHttpControllerTypeResolver ), controllerTypeResolver ); + configuration.EnableDependencyInjection(); + configuration.AddApiVersioning(); + configuration.MapHttpAttributeRoutes(); + + var apiVersion = new ApiVersion( 1.0 ); + var options = new ODataApiExplorerOptions( configuration ); + var apiExplorer = new ODataApiExplorer( configuration, options ); + + options.AdHocModelBuilder.ModelConfigurations.Add( new ImplicitModelBoundSettingsConvention() ); + + // act + var descriptionGroup = apiExplorer.ApiDescriptions[apiVersion]; + var description = descriptionGroup.ApiDescriptions[0]; + + // assert + var parameter = description.ParameterDescriptions.Single( p => p.Name == "$filter" ); + + parameter.Documentation.Should().EndWith( "author, published." ); + } } \ No newline at end of file diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs index 7d892736..2e0352bb 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.Simulators.Configuration; using Asp.Versioning.OData; diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs index 6dd8b03a..9b04a783 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.Simulators.Configuration; using Asp.Versioning.OData; diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs index 404bf071..72b12f9b 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Book.cs @@ -2,6 +2,9 @@ namespace Asp.Versioning.Simulators.Models; +using Microsoft.AspNet.OData.Query; + +[Filter( "author", "published" )] public class Book { public string Id { get; set; } diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs index 83a08d66..19470142 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA2227 // Collection properties should be read only - namespace Asp.Versioning.Simulators.Models; public class Supplier diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs index e6e24820..bab6814e 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs @@ -14,17 +14,18 @@ namespace Asp.Versioning.Simulators.V1; /// Represents a RESTful service of books. /// [ApiVersion( 1.0 )] +[RoutePrefix( "api/books" )] public class BooksController : ApiController { - private static readonly Book[] books = new Book[] - { + private static readonly Book[] books = + [ new() { Id = "9781847490599", Title = "Anna Karenina", Author = "Leo Tolstoy", Published = 1878 }, new() { Id = "9780198800545", Title = "War and Peace", Author = "Leo Tolstoy", Published = 1869 }, new() { Id = "9780684801520", Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", Published = 1925 }, new() { Id = "9780486280615", Title = "The Adventures of Huckleberry Finn", Author = "Mark Twain", Published = 1884 }, new() { Id = "9780140430820", Title = "Moby Dick", Author = "Herman Melville", Published = 1851 }, new() { Id = "9780060934347", Title = "Don Quixote", Author = "Miguel de Cervantes", Published = 1605 }, - }; + ]; /// /// Gets all books. @@ -33,6 +34,7 @@ public class BooksController : ApiController /// All available books. /// The successfully retrieved books. [HttpGet] + [Route] [ResponseType( typeof( IEnumerable ) )] public IHttpActionResult Get( ODataQueryOptions options ) => Ok( options.ApplyTo( books.AsQueryable() ) ); @@ -46,6 +48,7 @@ public IHttpActionResult Get( ODataQueryOptions options ) => /// The book was successfully retrieved. /// The book does not exist. [HttpGet] + [Route( "{id}" )] [ResponseType( typeof( Book ) )] public IHttpActionResult Get( string id, ODataQueryOptions options ) { diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V3/SuppliersController.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V3/SuppliersController.cs index f3264880..a27e95ff 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V3/SuppliersController.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/Simulators/V3/SuppliersController.cs @@ -1,9 +1,11 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0060 // Remove unused parameter +#pragma warning disable IDE0079 // Remove unnecessary suppression #pragma warning disable SA1625 // Element documentation should not be copied and pasted namespace Asp.Versioning.Simulators.V3; +#pragma warning restore IDE0079 // Remove unnecessary suppression using Asp.Versioning.OData; using Asp.Versioning.Simulators.Models; diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/System.Web.Http/Description/ApiDescriptionExtensionsTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/System.Web.Http/Description/ApiDescriptionExtensionsTest.cs index 88a780dc..5d56a57a 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/System.Web.Http/Description/ApiDescriptionExtensionsTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.ApiExplorer.Tests/System.Web.Http/Description/ApiDescriptionExtensionsTest.cs @@ -68,7 +68,7 @@ private static VersionedApiDescription CreateApiDescription( IEdmModel model ) { var configuration = new HttpConfiguration(); var controllerType = typeof( Asp.Versioning.Simulators.V1.OrdersController ); - var actionMethod = controllerType.GetRuntimeMethod( "Get", new[] { typeof( int ) } ); + var actionMethod = controllerType.GetRuntimeMethod( "Get", [typeof( int )] ); var controllerDescriptor = new HttpControllerDescriptor( configuration, "Orders", controllerType ); var actionDescriptor = new ReflectedHttpActionDescriptor( controllerDescriptor, actionMethod ); var apiDescription = new VersionedApiDescription() diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Asp.Versioning.WebApi.OData.Tests.csproj b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Asp.Versioning.WebApi.OData.Tests.csproj index 592ea6dc..30285b43 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Asp.Versioning.WebApi.OData.Tests.csproj +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Asp.Versioning.WebApi.OData.Tests.csproj @@ -1,9 +1,9 @@ - + - - net452;net472 - Asp.Versioning - + + net452;net472 + Asp.Versioning + diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs index 236fa169..c39e10b4 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Controllers/VersionedMetadataControllerTest.cs @@ -37,6 +37,7 @@ public async Task options_should_return_expected_headers() configuration.AddApiVersioning( options => { + options.ReportApiVersions = true; options.Policies.Sunset( "VersionedMetadata" ) .Link( "policies" ) .Title( "Versioning Policy" ) @@ -81,8 +82,6 @@ public IEnumerable GetServices( Type serviceType ) } } -#pragma warning disable CA1812 - [ApiVersion( "1.0" )] [ApiVersion( "2.0" )] private sealed class Controller1 : ODataController diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs index 7bd0781e..31845519 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/OData/VersionedODataModelBuilderTest.cs @@ -34,7 +34,7 @@ public void get_edm_models_should_return_expected_results() var model = builder.GetEdmModels().Single(); // assert - model.GetAnnotationValue( model ).ApiVersion.Should().Be( apiVersion ); + model.GetApiVersion().Should().Be( apiVersion ); modelCreated.Verify( f => f( It.IsAny(), model ), Times.Once() ); } diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs index 0756895b..f06fc620 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dneutral + namespace Asp.Versioning.Routing; using Microsoft.AspNet.OData; @@ -44,8 +46,6 @@ public void should_map_controller_should_return_expected_result_for_controller_v result.Should().BeTrue(); } -#pragma warning disable CA1812 - [ApiVersionNeutral] private sealed class NeutralController : ODataController { } diff --git a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs index 37fece37..c2e86cbc 100644 --- a/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs +++ b/src/AspNet/OData/test/Asp.Versioning.WebApi.OData.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs @@ -46,7 +46,7 @@ public void map_versioned_odata_route_should_return_expected_result( string rout var selector = GetODataRootContainer( configuration, routeName ).GetRequiredService(); var routingConventions = GetRoutingConventions( configuration, route ); - selector.ApiVersions.Should().Equal( new ApiVersion[] { new( 1, 0 ), new( 2, 0 ) } ); + selector.ApiVersions.Should().Equal( [new( 1, 0 ), new( 2, 0 )] ); routingConventions[0].Should().BeOfType(); routingConventions[1].Should().BeOfType(); routingConventions.OfType().Should().BeEmpty(); @@ -94,8 +94,6 @@ private static IReadOnlyList GetRoutingConventions( Htt return routingConventions.ToArray(); } -#pragma warning disable CA1812 - [ApiVersion( "1.0" )] private sealed class ControllerV1 : ODataController { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/ApiExplorerOptions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/ApiExplorerOptions.cs index 09df17e5..63aaa0ec 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/ApiExplorerOptions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/ApiExplorerOptions.cs @@ -44,4 +44,14 @@ public partial class ApiExplorerOptions /// /// The name associated with the API version route constraint. public string RouteConstraintName => options.Value.RouteConstraintName; + + /// + /// Gets or sets the API version selector. + /// + /// An API version selector object. + public IApiVersionSelector ApiVersionSelector + { + get => apiVersionSelector ?? options.Value.ApiVersionSelector; + set => apiVersionSelector = value; + } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs index 83d99d0e..c7585844 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ApiExplorer/VersionedApiExplorer.cs @@ -101,7 +101,7 @@ public IDocumentationProvider DocumentationProvider /// The configured sunset policy manager. protected ISunsetPolicyManager SunsetPolicyManager { - get => sunsetPolicyManager ??= Configuration.DependencyResolver.GetSunsetPolicyManager(); + get => sunsetPolicyManager ??= Configuration.GetSunsetPolicyManager(); set => sunsetPolicyManager = value; } @@ -227,7 +227,7 @@ protected virtual ApiDescriptionGroupCollection InitializeApiDescriptions() } var routes = FlattenRoutes( Configuration.Routes ).ToArray(); - var policyManager = Configuration.DependencyResolver.GetSunsetPolicyManager(); + var policyManager = Configuration.GetSunsetPolicyManager(); foreach ( var apiVersion in FlattenApiVersions( controllerMappings ) ) { @@ -713,7 +713,7 @@ protected virtual Collection ExploreRouteControllers( var apiDescriptions = new Collection(); var routeTemplate = route.RouteTemplate; - string controllerVariableValue; + string? controllerVariableValue; if ( controllerVariableRegex.IsMatch( routeTemplate ) ) { @@ -734,12 +734,12 @@ protected virtual Collection ExploreRouteControllers( } } else if ( route.Defaults.TryGetValue( RouteValueKeys.Controller, out controllerVariableValue ) && - controllerMappings.TryGetValue( controllerVariableValue, out var controllerDescriptor ) ) + controllerMappings.TryGetValue( controllerVariableValue!, out var controllerDescriptor ) ) { // bound controller variable {controller = "controllerName"} foreach ( var nestedControllerDescriptor in controllerDescriptor.AsEnumerable() ) { - if ( ShouldExploreController( controllerVariableValue, nestedControllerDescriptor, route, apiVersion ) ) + if ( ShouldExploreController( controllerVariableValue!, nestedControllerDescriptor, route, apiVersion ) ) { ExploreRouteActions( route, routeTemplate, nestedControllerDescriptor, apiDescriptions, apiVersion ); } @@ -782,7 +782,7 @@ private void ExploreRouteActions( return; } - string actionVariableValue; + string? actionVariableValue; if ( actionVariableRegex.IsMatch( localPath ) ) { @@ -935,17 +935,8 @@ private static bool ShouldEmitPrefixes( ICollection par parameter.CanConvertPropertiesFromString() ) > 1; } - private static Type GetCollectionElementType( Type collectionType ) - { - var elementType = collectionType.GetElementType(); - - if ( elementType == null ) - { - elementType = typeof( ICollection<> ).GetGenericBinderTypeArgs( collectionType ).First(); - } - - return elementType; - } + private static Type GetCollectionElementType( Type collectionType ) => + collectionType.GetElementType() ?? typeof( ICollection<> ).GetGenericBinderTypeArgs( collectionType ).First(); private static void AddPlaceholderForProperties( Dictionary parameterValuesForRoute, diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj index c04f1fe3..9622bbfd 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Asp.Versioning.WebApi.ApiExplorer.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 7.1.0 + 7.1.0.0 net45;net472 ASP.NET Web API Versioning API Explorer The API Explorer extensions for ASP.NET Web API Versioning. diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs index 596bd634..469c44f5 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiDescriptionGroup.cs @@ -56,5 +56,5 @@ public ApiDescriptionGroup( ApiVersion apiVersion, string name ) /// /// A collection of /// versioned API descriptions. - public virtual Collection ApiDescriptions { get; } = new(); + public virtual Collection ApiDescriptions { get; } = []; } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs index c1e5fdf6..f92c6857 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/ApiVersionParameterDescriptionContext.cs @@ -30,9 +30,9 @@ public class ApiVersionParameterDescriptionContext : IApiVersionParameterDescrip public ApiVersionParameterDescriptionContext( ApiDescription apiDescription, ApiVersion apiVersion, ApiExplorerOptions options ) { Options = options ?? throw new ArgumentNullException( nameof( options ) ); - ApiDescription = apiDescription; - ApiVersion = apiVersion; - optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion; + ApiDescription = apiDescription ?? throw new ArgumentNullException( nameof( apiDescription ) ); + ApiVersion = apiVersion ?? throw new ArgumentNullException( nameof( apiVersion ) ); + optional = FirstParameterIsOptional( apiDescription, apiVersion, options ); } /// @@ -242,4 +242,21 @@ private static void CloneFormattersAndAddMediaTypeParameter( NameValueHeaderValu formatters.Add( formatter ); } } + + private static bool FirstParameterIsOptional( + ApiDescription apiDescription, + ApiVersion apiVersion, + ApiExplorerOptions options ) + { + if ( !options.AssumeDefaultVersionWhenUnspecified ) + { + return false; + } + + var mapping = ApiVersionMapping.Explicit | ApiVersionMapping.Implicit; + var model = apiDescription.ActionDescriptor.GetApiVersionMetadata().Map( mapping ); + var defaultApiVersion = options.ApiVersionSelector.SelectVersion( model ); + + return apiVersion == defaultApiVersion; + } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs index e5ab4a68..da07e7d1 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/Description/VersionedApiDescription.cs @@ -69,7 +69,7 @@ public ApiVersion ApiVersion /// /// A collection of arbitrary metadata properties /// associated with the API description. - public IDictionary Properties => properties ??= new(); + public IDictionary Properties => properties ??= []; private static Action CreateSetResponseDescriptionMutator() { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi.ApiExplorer/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApiVersionRequestProperties.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApiVersionRequestProperties.cs index 98adf3be..0383fb9c 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApiVersionRequestProperties.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApiVersionRequestProperties.cs @@ -55,11 +55,9 @@ public string? RawRequestedApiVersion { 0 => default, 1 => values[0], -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations; existing behavior via IApiVersionReader.Read _ => throw new AmbiguousApiVersionException( string.Format( CultureInfo.CurrentCulture, CommonSR.MultipleDifferentApiVersionsRequested, string.Join( ", ", values ) ), values ), -#pragma warning restore CA1065 }; } set @@ -90,7 +88,7 @@ public ApiVersion? RequestedApiVersion return apiVersion; } - var parser = request.GetConfiguration().DependencyResolver.GetApiVersionParser(); + var parser = request.GetConfiguration().GetApiVersionParser(); try { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj index b2f49ecd..761b386b 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Asp.Versioning.WebApi.csproj @@ -1,8 +1,8 @@  - 6.2.1 - 6.2.0.0 + 7.1.0 + 7.1.0.0 net45;net472 ASP.NET Web API Versioning A service API versioning library for Microsoft ASP.NET Web API. @@ -15,8 +15,20 @@ + + + + + + + + + + + + diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ActionSelectorCacheItem.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ActionSelectorCacheItem.cs index 28f69ac4..4ae17299 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ActionSelectorCacheItem.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ActionSelectorCacheItem.cs @@ -23,10 +23,10 @@ namespace Asp.Versioning.Controllers; /// internal sealed class ActionSelectorCacheItem { - private static readonly HttpMethod[] cacheListMethodKinds = new[] { HttpMethod.Get, HttpMethod.Put, HttpMethod.Post }; + private static readonly HttpMethod[] cacheListMethodKinds = [HttpMethod.Get, HttpMethod.Put, HttpMethod.Post]; private readonly HttpControllerDescriptor controllerDescriptor; private readonly CandidateAction[] combinedCandidateActions; - private readonly Dictionary actionParameterNames = new(); + private readonly Dictionary actionParameterNames = []; private readonly ILookup combinedActionNameMapping; private StandardActionSelectionCache? standardActions; @@ -76,7 +76,7 @@ private void InitializeStandardActions() if ( controllerDescriptor.IsAttributeRouted() ) { - selectionCache.StandardCandidateActions = Array.Empty(); + selectionCache.StandardCandidateActions = []; } else { @@ -94,7 +94,7 @@ private void InitializeStandardActions() } } - selectionCache.StandardCandidateActions = standardCandidateActions.ToArray(); + selectionCache.StandardCandidateActions = [.. standardCandidateActions]; } selectionCache.StandardActionNameMapping = @@ -118,11 +118,7 @@ private void InitializeStandardActions() HttpControllerContext controllerContext, Func, HttpActionDescriptor?> selector ) { - if ( selector == null ) - { - throw new ArgumentNullException( nameof( selector ) ); - } - + ArgumentNullException.ThrowIfNull( selector ); InitializeStandardActions(); var firstAttempt = FindAction( controllerContext, selector, ignoreSubRoutes: false ); @@ -153,8 +149,6 @@ private ActionSelectionResult FindAction( bool ignoreSubRoutes ) { var selectedCandidates = FindMatchingActions( controllerContext, ignoreSubRoutes ); - -#pragma warning disable CA2000 // Dispose objects before losing scope if ( selectedCandidates.Count == 0 ) { return new( new HttpResponseException( CreateSelectionError( controllerContext ) ) ); @@ -170,7 +164,6 @@ private ActionSelectionResult FindAction( { return new( new HttpResponseException( CreateSelectionError( controllerContext ) ) ); } -#pragma warning restore CA2000 // Dispose objects before losing scope var ambiguityList = CreateAmbiguousMatchList( selectedCandidates ); var message = string.Format( CultureInfo.CurrentCulture, SR.ApiControllerActionSelector_AmbiguousMatch, ambiguityList ); @@ -307,7 +300,7 @@ private static List GetInitialCandidateWithParameterL continue; } - subRouteData.Values.TryGetValue( RouteValueKeys.Action, out string actionName ); + subRouteData.Values.TryGetValue( RouteValueKeys.Action, out string? actionName ); for ( var i = 0; i < candidates.Length; i++ ) { @@ -338,9 +331,9 @@ private CandidateAction[] GetInitialCandidateList( HttpControllerContext control var routeData = controllerContext.RouteData; CandidateAction[] candidates; - if ( routeData.Values.TryGetValue( RouteValueKeys.Action, out string actionName ) ) + if ( routeData.Values.TryGetValue( RouteValueKeys.Action, out string? actionName ) ) { - var actionsFoundByName = standardActions!.StandardActionNameMapping![actionName].ToArray(); + var actionsFoundByName = standardActions!.StandardActionNameMapping![actionName!].ToArray(); if ( actionsFoundByName.Length == 0 ) { @@ -415,7 +408,7 @@ private List FindActionMatchRequiredRouteAndQueryPara } private List FindActionMatchMostRouteAndQueryParameters( List candidatesFound ) => - candidatesFound.Count < 2 ? candidatesFound : candidatesFound.GroupBy( c => actionParameterNames[c.ActionDescriptor].Length ).OrderByDescending( g => g.Key ).First().ToList(); + candidatesFound.Count < 2 ? candidatesFound : [.. candidatesFound.GroupBy( c => actionParameterNames[c.ActionDescriptor].Length ).OrderByDescending( g => g.Key ).First()]; private static CandidateActionWithParams[] GetCandidateActionsWithBindings( HttpControllerContext controllerContext, CandidateAction[] candidatesFound ) { @@ -488,7 +481,7 @@ private static CandidateAction[] FindActionsForMethod( HttpMethod method, Candid { var listCandidates = new List(); FindActionsForMethod( method, candidates, listCandidates ); - return listCandidates.ToArray(); + return [.. listCandidates]; } private static void FindActionsForMethod( HttpMethod method, CandidateAction[] candidates, List listCandidates ) diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionActionSelector.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionActionSelector.cs index a8b5a729..e23f72a8 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionActionSelector.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionActionSelector.cs @@ -24,11 +24,7 @@ public class ApiVersionActionSelector : IHttpActionSelector /// controller context. public virtual HttpActionDescriptor? SelectAction( HttpControllerContext controllerContext ) { - if ( controllerContext == null ) - { - throw new ArgumentNullException( nameof( controllerContext ) ); - } - + ArgumentNullException.ThrowIfNull( controllerContext ); var internalSelector = GetInternalSelector( controllerContext.ControllerDescriptor ); return internalSelector.SelectAction( controllerContext, SelectActionVersion ); } @@ -41,10 +37,7 @@ public class ApiVersionActionSelector : IHttpActionSelector /// specified controller descriptor. public virtual ILookup GetActionMapping( HttpControllerDescriptor controllerDescriptor ) { - if ( controllerDescriptor == null ) - { - throw new ArgumentNullException( nameof( controllerDescriptor ) ); - } + ArgumentNullException.ThrowIfNull( controllerDescriptor ); var actionMappings = ( from descriptor in controllerDescriptor.AsEnumerable( includeCandidates: true ) let selector = GetInternalSelector( descriptor ) @@ -65,15 +58,8 @@ public virtual ILookup GetActionMapping( HttpContr /// ambiguous among the provided list of candidate actions. protected virtual HttpActionDescriptor? SelectActionVersion( HttpControllerContext controllerContext, IReadOnlyList candidateActions ) { - if ( controllerContext == null ) - { - throw new ArgumentNullException( nameof( controllerContext ) ); - } - - if ( candidateActions == null ) - { - throw new ArgumentNullException( nameof( candidateActions ) ); - } + ArgumentNullException.ThrowIfNull( controllerContext ); + ArgumentNullException.ThrowIfNull( candidateActions ); if ( candidateActions.Count == 0 ) { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionParameterBinding.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionParameterBinding.cs index cc34b6a8..a9f4d9af 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionParameterBinding.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/ApiVersionParameterBinding.cs @@ -25,11 +25,7 @@ public override Task ExecuteBindingAsync( HttpActionContext actionContext, CancellationToken cancellationToken ) { - if ( actionContext == null ) - { - throw new ArgumentNullException( nameof( actionContext ) ); - } - + ArgumentNullException.ThrowIfNull( actionContext ); var value = actionContext.Request.ApiVersionProperties().RequestedApiVersion; SetValue( actionContext, value ); return CompletedTask; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/HttpControllerDescriptorGroup.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/HttpControllerDescriptorGroup.cs index 4e3aab24..884a1747 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/HttpControllerDescriptorGroup.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Controllers/HttpControllerDescriptorGroup.cs @@ -32,7 +32,7 @@ public class HttpControllerDescriptorGroup : HttpControllerDescriptor, IReadOnly /// HTTP controller descriptors. public HttpControllerDescriptorGroup( HttpConfiguration configuration, string controllerName, params HttpControllerDescriptor[] controllerDescriptors ) : base( configuration, controllerName, controllerDescriptors?[0].ControllerType ) => - descriptors = controllerDescriptors ?? throw new ArgumentNullException( nameof( controllerDescriptors ) ); + descriptors = controllerDescriptors ?? throw new System.ArgumentNullException( nameof( controllerDescriptors ) ); /// /// Initializes a new instance of the class. @@ -50,7 +50,7 @@ public HttpControllerDescriptorGroup( HttpConfiguration configuration, string co /// HTTP controller descriptors. public HttpControllerDescriptorGroup( HttpConfiguration configuration, string controllerName, IReadOnlyList controllerDescriptors ) : base( configuration, controllerName, controllerDescriptors?[0].ControllerType ) => - descriptors = controllerDescriptors ?? throw new ArgumentNullException( nameof( controllerDescriptors ) ); + descriptors = controllerDescriptors ?? throw new System.ArgumentNullException( nameof( controllerDescriptors ) ); /// /// Creates and returns a controller for the specified request. diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ActionApiVersionConventionBuilderBase.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ActionApiVersionConventionBuilderBase.cs index aea7e59b..9dd43313 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ActionApiVersionConventionBuilderBase.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ActionApiVersionConventionBuilderBase.cs @@ -13,10 +13,7 @@ public partial class ActionApiVersionConventionBuilderBase : IApiVersionConventi /// public virtual void ApplyTo( HttpActionDescriptor item ) { - if ( item == null ) - { - throw new ArgumentNullException( nameof( item ) ); - } + ArgumentNullException.ThrowIfNull( item ); var attributes = new List(); diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ControllerApiVersionConventionBuilderBase.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ControllerApiVersionConventionBuilderBase.cs index 0c194afe..1480d3ba 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ControllerApiVersionConventionBuilderBase.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Conventions/ControllerApiVersionConventionBuilderBase.cs @@ -35,10 +35,7 @@ public abstract class ControllerApiVersionConventionBuilderBase : ApiVersionConv /// to apply the conventions to. public virtual void ApplyTo( HttpControllerDescriptor item ) { - if ( item == null ) - { - throw new ArgumentNullException( nameof( item ) ); - } + ArgumentNullException.ThrowIfNull( item ); var attributes = new List(); @@ -54,7 +51,7 @@ public virtual void ApplyTo( HttpControllerDescriptor item ) /// The method representing the action to retrieve the convention for. /// The retrieved convention or null. /// True if the convention was successfully retrieved; otherwise, false. - protected abstract bool TryGetConvention( MethodInfo method, out IApiVersionConvention convention ); + protected abstract bool TryGetConvention( MethodInfo method, [MaybeNullWhen( false )] out IApiVersionConvention convention ); private void ApplyActionConventions( HttpControllerDescriptor controller ) { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DefaultApiVersionReporter.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DefaultApiVersionReporter.cs index 357b5978..caa7f0be 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DefaultApiVersionReporter.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DefaultApiVersionReporter.cs @@ -10,10 +10,6 @@ namespace Asp.Versioning; /// public partial class DefaultApiVersionReporter { - private static DefaultApiVersionReporter? instance; - - internal static IReportApiVersions Instance => instance ??= new(); - private static void AddApiVersionHeader( HttpResponseHeaders headers, string headerName, IReadOnlyList versions ) { if ( versions.Count == 0 || headers.Contains( headerName ) ) diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs new file mode 100644 index 00000000..7725393c --- /dev/null +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dependencies/DefaultContainer.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Dependencies; + +using Asp.Versioning; +using Asp.Versioning.Conventions; +using System.ComponentModel.Design; +using System.Web.Http.Dependencies; + +internal sealed class DefaultContainer : IDependencyResolver, IDependencyScope +{ + private readonly ServiceContainer container = new(); + private bool disposed; + + internal DefaultContainer() + { + container.AddService( typeof( ApiVersioningOptions ), static ( sc, t ) => new ApiVersioningOptions() ); + container.AddService( typeof( IApiVersionParser ), static ( sc, t ) => ApiVersionParser.Default ); + container.AddService( typeof( IControllerNameConvention ), static ( sc, t ) => ControllerNameConvention.Default ); + container.AddService( typeof( IProblemDetailsFactory ), static ( sc, t ) => new ProblemDetailsFactory() ); + container.AddService( typeof( ISunsetPolicyManager ), NewSunsetPolicyManager ); + container.AddService( typeof( IReportApiVersions ), NewApiVersionReporter ); + } + + public ApiVersioningOptions ApiVersioningOptions + { + get => GetApiVersioningOptions( container ); + set + { + container.RemoveService( typeof( ApiVersioningOptions ) ); + container.AddService( typeof( ApiVersioningOptions ), value ); + } + } + + public void Replace( Type serviceType, ServiceCreatorCallback activator ) + { + container.RemoveService( serviceType ); + container.AddService( serviceType, activator ); + } + + public IDependencyScope BeginScope() => this; + + public void Dispose() + { + if ( disposed ) + { + return; + } + + disposed = true; + container.Dispose(); + } + + public object GetService( Type serviceType ) => container.GetService( serviceType ); + + public IEnumerable GetServices( Type serviceType ) + { + var service = container.GetService( serviceType ); + + if ( service is not null ) + { + yield return service; + } + } + + private static ApiVersioningOptions GetApiVersioningOptions( IServiceProvider serviceProvider ) => + (ApiVersioningOptions) serviceProvider.GetService( typeof( ApiVersioningOptions ) ); + + private static ISunsetPolicyManager NewSunsetPolicyManager( IServiceProvider serviceProvider, Type type ) => + new SunsetPolicyManager( GetApiVersioningOptions( serviceProvider ) ); + + private static IReportApiVersions NewApiVersionReporter( IServiceProvider serviceProvider, Type type ) + { + var options = GetApiVersioningOptions( serviceProvider ); + + if ( options.ReportApiVersions ) + { + var sunsetPolicyManager = (ISunsetPolicyManager) serviceProvider.GetService( typeof( ISunsetPolicyManager ) ); + return new DefaultApiVersionReporter( sunsetPolicyManager ); + } + + return new DoNotReportApiVersions(); + } +} \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs index 9e8c119a..6cad7f56 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DependencyResolverExtensions.cs @@ -3,24 +3,39 @@ namespace Asp.Versioning; using Asp.Versioning.Conventions; +using System.Globalization; +using System.Web.Http; using System.Web.Http.Dependencies; internal static class DependencyResolverExtensions { - internal static TService? GetService( this IDependencyResolver resolver ) => (TService) resolver.GetService( typeof( TService ) ); - - internal static IApiVersionParser GetApiVersionParser( this IDependencyResolver resolver ) => - resolver.GetService() ?? ApiVersionParser.Default; - - internal static IReportApiVersions GetApiVersionReporter( this IDependencyResolver resolver ) => - resolver.GetService() ?? DefaultApiVersionReporter.Instance; - - internal static IControllerNameConvention GetControllerNameConvention( this IDependencyResolver resolver ) => - resolver.GetService() ?? ControllerNameConvention.Default; - - internal static IProblemDetailsFactory GetProblemDetailsFactory( this IDependencyResolver resolver ) => - resolver.GetService() ?? ProblemDetailsFactory.Default; - - internal static ISunsetPolicyManager GetSunsetPolicyManager( this IDependencyResolver resolver ) => - resolver.GetService() ?? SunsetPolicyManager.Default; + internal static TService? GetService( this IDependencyResolver resolver ) => + (TService) resolver.GetService( typeof( TService ) ); + + internal static TService GetRequiredService( this IDependencyResolver resolver ) + { + var service = resolver.GetService(); + var message = string.Format( CultureInfo.CurrentCulture, SR.NoServiceRegistered, typeof( TService ) ); + return service ?? throw new InvalidOperationException( message ); + } + + internal static IApiVersionParser GetApiVersionParser( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService() ?? + configuration.ApiVersioningServices().GetRequiredService(); + + internal static IReportApiVersions GetApiVersionReporter( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService() ?? + configuration.ApiVersioningServices().GetRequiredService(); + + internal static IControllerNameConvention GetControllerNameConvention( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService() ?? + configuration.ApiVersioningServices().GetRequiredService(); + + internal static IProblemDetailsFactory GetProblemDetailsFactory( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService() ?? + configuration.ApiVersioningServices().GetRequiredService(); + + internal static ISunsetPolicyManager GetSunsetPolicyManager( this HttpConfiguration configuration ) => + configuration.DependencyResolver.GetService() ?? + configuration.ApiVersioningServices().GetRequiredService(); } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ApiVersionControllerSelector.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ApiVersionControllerSelector.cs index a9473c8a..c889b994 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ApiVersionControllerSelector.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ApiVersionControllerSelector.cs @@ -123,7 +123,7 @@ public virtual IDictionary GetControllerMappin return null; } - if ( routeData.Values.TryGetValue( RouteDataTokenKeys.Controller, out string controller ) ) + if ( routeData.Values.TryGetValue( RouteDataTokenKeys.Controller, out string? controller ) ) { return controller; } @@ -221,7 +221,7 @@ private static void ApplyImplicitConventions( HttpControllerDescriptor controlle } var actions = mapping.SelectMany( g => g ); - var namingConvention = controller.Configuration.DependencyResolver.GetControllerNameConvention(); + var namingConvention = controller.Configuration.GetControllerNameConvention(); var name = namingConvention.GroupName( controller.ControllerName ); var metadata = new ApiVersionMetadata( implicitVersionModel, implicitVersionModel, name ); @@ -258,7 +258,7 @@ private static HttpControllerDescriptor[] ApplyCollatedModels( CollateControllerModels( controllerModels, visitedControllers, CollateActionModels( actionModels, visitedActions ) ); ApplyCollatedModelsToActions( configuration, visitedActions ); - return controllers.ToArray(); + return [.. controllers]; } private static void CollateControllerVersions( @@ -344,7 +344,7 @@ private static void ApplyCollatedModelsToActions( HttpConfiguration configuration, List> visitedActions ) { - var namingConvention = configuration.DependencyResolver.GetControllerNameConvention(); + var namingConvention = configuration.GetControllerNameConvention(); for ( var i = 0; i < visitedActions.Count; i++ ) { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ControllerSelectionContext.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ControllerSelectionContext.cs index 5c8003c1..729390ce 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ControllerSelectionContext.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/ControllerSelectionContext.cs @@ -70,7 +70,7 @@ internal ApiVersion? RequestedVersion foreach ( var action in actions ) { - candidates ??= new(); + candidates ??= []; candidates.Add( new( action ) ); } } diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpControllerTypeCache.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpControllerTypeCache.cs index 473f9811..5c5d3abb 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpControllerTypeCache.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpControllerTypeCache.cs @@ -33,7 +33,7 @@ private Dictionary> InitializeCache() var services = configuration.Services; var assembliesResolver = services.GetAssembliesResolver(); var typeResolver = services.GetHttpControllerTypeResolver(); - var convention = configuration.DependencyResolver.GetControllerNameConvention(); + var convention = configuration.GetControllerNameConvention(); var comparer = StringComparer.OrdinalIgnoreCase; return typeResolver.GetControllerTypes( assembliesResolver ) diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs index 82a9761d..22fac02a 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs @@ -9,8 +9,6 @@ namespace Asp.Versioning.Dispatcher; using System.Web.Http.Tracing; using static System.Net.HttpStatusCode; -#pragma warning disable CA2000 // Dispose objects before losing scope - internal sealed class HttpResponseExceptionFactory { private const string Allow = nameof( Allow ); @@ -38,7 +36,7 @@ internal HttpResponseExceptionFactory( HttpRequestMessage request, ApiVersionMod private ApiVersioningOptions Options => configuration.GetApiVersioningOptions(); - private IProblemDetailsFactory ProblemDetails => configuration.DependencyResolver.GetProblemDetailsFactory(); + private IProblemDetailsFactory ProblemDetails => configuration.GetProblemDetailsFactory(); private ITraceWriter TraceWriter => configuration.Services.GetTraceWriter() ?? NullTraceWriter.Instance; @@ -64,7 +62,8 @@ internal HttpResponseException NewUnmatchedException( } } - var versionsOnlyByMediaType = Options.ApiVersionReader.VersionsByMediaType( allowMultipleLocations: false ); + var options = Options; + var versionsOnlyByMediaType = options.ApiVersionReader.VersionsByMediaType( allowMultipleLocations: false ); if ( versionsOnlyByMediaType ) { @@ -75,9 +74,28 @@ internal HttpResponseException NewUnmatchedException( if ( couldMatch ) { properties ??= request.ApiVersionProperties(); - response = properties.RequestedApiVersion is ApiVersion apiVersion - ? CreateResponseForUnsupportedApiVersion( apiVersion, NotFound ) - : CreateNotFound( conventionRouteResult ); + + if ( properties.RequestedApiVersion is ApiVersion apiVersion ) + { + HttpStatusCode statusCode; + var matchedUrlSegment = !string.IsNullOrEmpty( properties.RouteParameter ); + + if ( matchedUrlSegment ) + { + statusCode = NotFound; + } + else + { + var versionsByUrlOnly = options.ApiVersionReader.VersionsByUrl( allowMultipleLocations: false ); + statusCode = versionsByUrlOnly ? NotFound : options.UnsupportedApiVersionStatusCode; + } + + response = CreateResponseForUnsupportedApiVersion( apiVersion, statusCode ); + } + else + { + response = CreateNotFound( conventionRouteResult ); + } } else { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DoNotReportApiVersions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DoNotReportApiVersions.cs index 722073e5..db2b979d 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DoNotReportApiVersions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DoNotReportApiVersions.cs @@ -6,12 +6,6 @@ namespace Asp.Versioning; internal sealed class DoNotReportApiVersions : IReportApiVersions { - private static DoNotReportApiVersions? instance; - - private DoNotReportApiVersions() { } - - internal static IReportApiVersions Instance => instance ??= new(); - public ApiVersionMapping Mapping => Explicit | Implicit; public void Report( HttpResponseMessage response, ApiVersionModel apiVersionModel ) { } diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ErrorObjectFactory.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ErrorObjectFactory.cs new file mode 100644 index 00000000..79bbe3cc --- /dev/null +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ErrorObjectFactory.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +// REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +namespace Asp.Versioning; + +using Newtonsoft.Json; +using static Asp.Versioning.ProblemDetailsDefaults; +using static Newtonsoft.Json.NullValueHandling; + +internal sealed class ErrorObjectFactory : IProblemDetailsFactory +{ + public ProblemDetails CreateProblemDetails( + HttpRequestMessage request, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null ) + { + var status = statusCode ?? 500; + ErrorObject? problem; + + if ( type == Ambiguous.Type ) + { + problem = NewError( title, instance ); + problem.Error.Code = Ambiguous.Code; + } + else if ( type == Invalid.Type ) + { + problem = NewError( title, instance ); + problem.Error.Code = Invalid.Code; + return ProblemDetailsFactory.AddInvalidExtensions( request, status, problem, ApplyMessage ); + } + else if ( type == Unspecified.Type ) + { + problem = NewError( title, instance ); + problem.Error.Code = Unspecified.Code; + } + else if ( type == Unsupported.Type ) + { + problem = NewError( title, instance ); + problem.Error.Code = Unsupported.Code; + return ProblemDetailsFactory.AddUnsupportedExtensions( request, status, problem, ApplyMessage ); + } + + return ProblemDetailsFactory.NewProblemDetails( + request, + statusCode, + title, + type, + detail, + instance ); + } + + private static ErrorObject NewError( string? message, string? target ) => + new() + { + Error = + { + Message = message, + Target = target, + }, + }; + + private static void ApplyMessage( ErrorObject obj, string message ) => + obj.Error.InnerError = new() { Message = message }; + + private sealed class ErrorObject : ProblemDetails + { + [JsonProperty( "error" )] + public ErrorDetail Error { get; } = new(); + } + + private sealed class ErrorDetail + { + [JsonProperty( "code", NullValueHandling = Ignore )] + public string? Code { get; set; } + + [JsonProperty( "message", NullValueHandling = Ignore )] + public string? Message { get; set; } + + [JsonProperty( "target", NullValueHandling = Ignore )] + public string? Target { get; set; } + + [JsonProperty( "innerError", NullValueHandling = Ignore )] + public InnerError? InnerError { get; set; } + } + + private sealed class InnerError + { + [JsonProperty( "message", NullValueHandling = Ignore )] + public string? Message { get; set; } + } +} \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Formatting/ProblemDetailsMediaTypeFormatter.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Formatting/ProblemDetailsMediaTypeFormatter.cs new file mode 100644 index 00000000..cf44d707 --- /dev/null +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Formatting/ProblemDetailsMediaTypeFormatter.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +// REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs +namespace Asp.Versioning.Formatting; + +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Formatting; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using static Asp.Versioning.ProblemDetailsDefaults; + +/// +/// Represents a media type formatter for problem details based on https://tools.ietf.org/html/rfc7807. +/// +public class ProblemDetailsMediaTypeFormatter : MediaTypeFormatter +{ + private readonly JsonMediaTypeFormatter json; + + /// + /// Initializes a new instance of the class. + /// + public ProblemDetailsMediaTypeFormatter() : this( new() ) { } + + /// + /// Initializes a new instance of the class. + /// + /// The existing instance to derive from. + public ProblemDetailsMediaTypeFormatter( JsonMediaTypeFormatter formatter ) + : base( formatter ) + { + json = formatter; + SupportedEncodings.Add( new UTF8Encoding( encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true ) ); + SupportedEncodings.Add( new UnicodeEncoding( bigEndian: false, byteOrderMark: true, throwOnInvalidBytes: true ) ); + SupportedMediaTypes.Add( DefaultMediaType ); + } + + /// + /// Gets the default media type. + /// + /// Returns the media type for application/problem+json. + public static MediaTypeHeaderValue DefaultMediaType { get; } = MediaTypeHeaderValue.Parse( MediaType.Json ); + + /// + public override bool CanReadType( Type type ) => false; + + /// + public override bool CanWriteType( Type type ) => typeof( ProblemDetails ).IsAssignableFrom( type ); + + /// + public override Task WriteToStreamAsync( + Type type, + object value, + Stream writeStream, + HttpContent content, + TransportContext transportContext, + CancellationToken cancellationToken ) => + json.WriteToStreamAsync( type, value, writeStream, content, transportContext, cancellationToken ); + + /// + public override void SetDefaultContentHeaders( Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType ) + { + mediaType.MediaType = DefaultMediaType.MediaType; + base.SetDefaultContentHeaders( type, headers, mediaType ); + } +} \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/HeaderApiVersionReader.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/HeaderApiVersionReader.cs index b18405aa..58b12d19 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/HeaderApiVersionReader.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/HeaderApiVersionReader.cs @@ -10,10 +10,7 @@ public partial class HeaderApiVersionReader /// public virtual IReadOnlyList Read( HttpRequestMessage request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull( request ); var count = HeaderNames.Count; @@ -68,7 +65,7 @@ public virtual IReadOnlyList Read( HttpRequestMessage request ) if ( versions == null ) { - return version == null ? Array.Empty() : new[] { version }; + return version == null ? [] : [version]; } return versions.ToArray(); diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/IApiVersionSelectorExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/IApiVersionSelectorExtensions.cs new file mode 100644 index 00000000..37d63336 --- /dev/null +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/IApiVersionSelectorExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +/// +/// Provides extension methods for . +/// +public static class IApiVersionSelectorExtensions +{ + /// + /// Selects an API version given the specified API version information. + /// + /// The extended . + /// The model to select the version from. + /// The selected API version. + public static ApiVersion SelectVersion( this IApiVersionSelector selector, ApiVersionModel model ) + { + ArgumentNullException.ThrowIfNull( selector ); + using var request = new HttpRequestMessage(); + return selector.SelectVersion( request, model ); + } +} \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReader.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReader.cs index d748aa0b..d3a96174 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReader.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReader.cs @@ -10,10 +10,7 @@ public partial class MediaTypeApiVersionReader /// public virtual IReadOnlyList Read( HttpRequestMessage request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull( request ); var contentType = request.Content?.Headers.ContentType; var version = contentType is null ? default : ReadContentTypeHeader( contentType ); @@ -21,18 +18,18 @@ public virtual IReadOnlyList Read( HttpRequestMessage request ) if ( accept is null || ReadAcceptHeader( accept ) is not string otherVersion ) { - return version is null ? Array.Empty() : new[] { version }; + return version is null ? [] : [version]; } var comparer = StringComparer.OrdinalIgnoreCase; if ( version is null || comparer.Equals( version, otherVersion ) ) { - return new[] { otherVersion }; + return [otherVersion]; } - return comparer.Compare( version, otherVersion ) <= 0 ? - new[] { version, otherVersion } : - new[] { otherVersion, version }; + return comparer.Compare( version, otherVersion ) <= 0 + ? [version, otherVersion] + : [otherVersion, version]; } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReaderBuilder.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReaderBuilder.cs index 11f0fcb3..413f20e0 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReaderBuilder.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReaderBuilder.cs @@ -20,27 +20,22 @@ public partial class MediaTypeApiVersionReaderBuilder /// If a value is not specified, there is expected to be a single template parameter. /// The current . /// The template syntax is the same used by route templates; however, constraints are not supported. -#pragma warning disable CA1716 // Identifiers should not match keywords public virtual MediaTypeApiVersionReaderBuilder Template( string template, string? parameterName = default ) -#pragma warning restore CA1716 // Identifiers should not match keywords { - if ( string.IsNullOrEmpty( template ) ) - { - throw new ArgumentNullException( nameof( template ) ); - } + ArgumentException.ThrowIfNullOrEmpty( template ); if ( string.IsNullOrEmpty( parameterName ) ) { var parser = new RouteParser(); var parsedRoute = parser.Parse( template ); var segments = from content in parsedRoute.PathSegments.OfType() - from segment in content.Subsegments.OfType() - select segment; + from segment in content.Subsegments.OfType() + select segment; if ( segments.Count() > 1 ) { var message = string.Format( CultureInfo.CurrentCulture, CommonSR.InvalidMediaTypeTemplate, template ); - throw new ArgumentException( message, nameof( template ) ); + throw new System.ArgumentException( message, nameof( template ) ); } } @@ -113,11 +108,6 @@ private static IReadOnlyList ReadMediaTypePattern( } } - if ( version is null ) - { - return Array.Empty(); - } - - return versions is null ? new[] { version } : versions.ToArray(); + return ToArray( ref version, versions ); } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetails.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetails.cs index b6a27396..e3640c0e 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetails.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetails.cs @@ -32,7 +32,7 @@ public class ProblemDetails /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be /// "about:blank". /// - [JsonProperty( "type" )] + [JsonProperty( "type", NullValueHandling = Ignore )] public string? Type { get; set; } /// @@ -72,5 +72,6 @@ public class ProblemDetails /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. /// + [JsonExtensionData] public IDictionary Extensions => extensions; } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs index 3bb429dd..baaad563 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ProblemDetailsFactory.cs @@ -10,11 +10,16 @@ namespace Asp.Versioning; internal sealed class ProblemDetailsFactory : IProblemDetailsFactory { - private static ProblemDetailsFactory? @default; - - public static IProblemDetailsFactory Default => @default ??= new(); - public ProblemDetails CreateProblemDetails( + HttpRequestMessage request, + int? statusCode = null, + string? title = null, + string? type = null, + string? detail = null, + string? instance = null ) => + NewProblemDetails( request, statusCode, title, type, detail, instance ); + + internal static ProblemDetails NewProblemDetails( HttpRequestMessage request, int? statusCode = null, string? title = null, @@ -40,7 +45,7 @@ public ProblemDetails CreateProblemDetails( else if ( type == Invalid.Type ) { problemDetails.Code = Invalid.Code; - return AddInvalidExtensions( request, status, problemDetails ); + return AddInvalidExtensions( request, status, problemDetails, ApplyMessage ); } else if ( type == Unspecified.Type ) { @@ -49,13 +54,17 @@ public ProblemDetails CreateProblemDetails( else if ( type == Unsupported.Type ) { problemDetails.Code = Unsupported.Code; - return AddUnsupportedExtensions( request, status, problemDetails ); + return AddUnsupportedExtensions( request, status, problemDetails, ApplyMessage ); } return problemDetails; } - private static ProblemDetailsEx AddInvalidExtensions( HttpRequestMessage request, int status, ProblemDetailsEx problemDetails ) + internal static T AddInvalidExtensions( + HttpRequestMessage request, + int status, + T problemDetails, + Action applyMessage ) where T : ProblemDetails { if ( status != 400 || !request.ShouldIncludeErrorDetail() ) { @@ -64,14 +73,18 @@ private static ProblemDetailsEx AddInvalidExtensions( HttpRequestMessage request var safeUrl = request.RequestUri.SafeFullPath(); var requestedVersion = request.ApiVersionProperties().RawRequestedApiVersion; - var error = string.Format( CurrentCulture, SR.VersionedControllerNameNotFound, safeUrl, requestedVersion ); + var message = string.Format( CurrentCulture, SR.VersionedControllerNameNotFound, safeUrl, requestedVersion ); - problemDetails.Error = error; + applyMessage( problemDetails, message ); return problemDetails; } - private static ProblemDetailsEx AddUnsupportedExtensions( HttpRequestMessage request, int status, ProblemDetailsEx problemDetails ) + internal static T AddUnsupportedExtensions( + HttpRequestMessage request, + int status, + T problemDetails, + Action applyMessage ) where T : ProblemDetails { if ( !request.ShouldIncludeErrorDetail() ) { @@ -95,14 +108,17 @@ private static ProblemDetailsEx AddUnsupportedExtensions( HttpRequestMessage req var safeUrl = request.RequestUri.SafeFullPath(); var requestedMethod = request.Method; - var version = request.GetRequestedApiVersion()?.ToString() ?? "(null)"; - var error = string.Format( CurrentCulture, messageFormat, safeUrl, version, requestedMethod ); + var version = request.ApiVersionProperties().RawRequestedApiVersion ?? "(null)"; + var message = string.Format( CurrentCulture, messageFormat, safeUrl, version, requestedMethod ); - problemDetails.Error = error; + applyMessage( problemDetails, message ); return problemDetails; } + private static void ApplyMessage( ProblemDetailsEx problemDetails, string message ) => + problemDetails.Error = message; + private sealed class ProblemDetailsEx : ProblemDetails { [JsonProperty( "code", NullValueHandling = Ignore )] diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/QueryStringApiVersionReader.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/QueryStringApiVersionReader.cs index 3f119177..8b88062b 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/QueryStringApiVersionReader.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/QueryStringApiVersionReader.cs @@ -10,10 +10,7 @@ public partial class QueryStringApiVersionReader /// public virtual IReadOnlyList Read( HttpRequestMessage request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull(request); var count = ParameterNames.Count; @@ -62,7 +59,7 @@ public virtual IReadOnlyList Read( HttpRequestMessage request ) if ( versions == null ) { - return version == null ? Array.Empty() : new[] { version }; + return version == null ? [] : [version]; } return versions.ToArray(); diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt index 40001e18..5f282702 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReleaseNotes.txt @@ -1 +1 @@ -Fix MediaTypeApiVersionReaderBuilder.AddParameters (#904) \ No newline at end of file + \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReportApiVersionsAttribute.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReportApiVersionsAttribute.cs index 8208fd7e..782118fa 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReportApiVersionsAttribute.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ReportApiVersionsAttribute.cs @@ -18,10 +18,7 @@ public sealed partial class ReportApiVersionsAttribute /// response provided that there is a response and the executed action was not version-neutral. public override void OnActionExecuted( HttpActionExecutedContext actionExecutedContext ) { - if ( actionExecutedContext == null ) - { - throw new ArgumentNullException( nameof( actionExecutedContext ) ); - } + ArgumentNullException.ThrowIfNull( actionExecutedContext ); var response = actionExecutedContext.Response; @@ -32,14 +29,7 @@ public override void OnActionExecuted( HttpActionExecutedContext actionExecutedC var context = actionExecutedContext.ActionContext; var action = context.ActionDescriptor; - var reporter = reportApiVersions; - - if ( reporter is null ) - { - var dependencyResolver = context.ControllerContext.Configuration.DependencyResolver; - reporter = dependencyResolver.GetApiVersionReporter(); - } - + var reporter = reportApiVersions ?? context.ControllerContext.Configuration.GetApiVersionReporter(); var model = action.GetApiVersionMetadata().Map( reporter.Mapping ); response.RequestMessage ??= actionExecutedContext.Request; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionRouteConstraint.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionRouteConstraint.cs index 3af05991..ad930d65 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionRouteConstraint.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionRouteConstraint.cs @@ -2,7 +2,6 @@ namespace Asp.Versioning.Routing; -using System.Web.Http; using System.Web.Http.Routing; /// @@ -21,17 +20,14 @@ public sealed class ApiVersionRouteConstraint : IHttpRouteConstraint /// True if the route constraint is matched; otherwise, false. public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection ) { - if ( values == null ) - { - throw new ArgumentNullException( nameof( values ) ); - } + ArgumentNullException.ThrowIfNull( values ); if ( string.IsNullOrEmpty( parameterName ) ) { return false; } - if ( !values.TryGetValue( parameterName, out string value ) ) + if ( !values.TryGetValue( parameterName, out string? value ) ) { return false; } @@ -41,7 +37,7 @@ public bool Match( HttpRequestMessage request, IHttpRoute route, string paramete return !string.IsNullOrEmpty( value ); } - var parser = request.GetConfiguration().DependencyResolver.GetApiVersionParser(); + var parser = request.GetConfiguration().GetApiVersionParser(); var properties = request.ApiVersionProperties(); properties.RouteParameter = parameterName; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionUrlHelper.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionUrlHelper.cs index cb414b6e..8a1beea4 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionUrlHelper.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ApiVersionUrlHelper.cs @@ -15,7 +15,7 @@ public class ApiVersionUrlHelper : UrlHelper /// The inner URL helper. public ApiVersionUrlHelper( UrlHelper url ) { - Url = url ?? throw new ArgumentNullException( nameof( url ) ); + Url = url ?? throw new System.ArgumentNullException( nameof( url ) ); if ( url.Request != null ) { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/BoundRouteTemplateAdapter{T}.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/BoundRouteTemplateAdapter{T}.cs index a5c6446b..8d5de8fb 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/BoundRouteTemplateAdapter{T}.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/BoundRouteTemplateAdapter{T}.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1812 - namespace Asp.Versioning.Routing; using System.Web.Http.Routing; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IBoundRouteTemplate.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IBoundRouteTemplate.cs index 6bd7dced..c2ce56f5 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IBoundRouteTemplate.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IBoundRouteTemplate.cs @@ -15,11 +15,9 @@ public interface IBoundRouteTemplate /// The bound template. string BoundTemplate { get; set; } -#pragma warning disable CA2227 // Collection properties should be read only /// /// Gets or sets the template parameter values. /// /// The template route value dictionary. HttpRouteValueDictionary Values { get; set; } -#pragma warning restore CA2227 } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSegment.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSegment.cs index 5a57da6d..d6e6e455 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSegment.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSegment.cs @@ -2,8 +2,6 @@ namespace Asp.Versioning.Routing; -#pragma warning disable CA1040 // Avoid empty interfaces - /// /// Defines the behavior of a path segment. /// diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSeparatorSegment.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSeparatorSegment.cs index d57a4a7b..ca2829d6 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSeparatorSegment.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSeparatorSegment.cs @@ -2,8 +2,6 @@ namespace Asp.Versioning.Routing; -#pragma warning disable CA1040 // Avoid empty interfaces - /// /// Defines the behavior of a path separator. /// diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSubsegment.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSubsegment.cs index 8a180b7c..49eec855 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSubsegment.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/IPathSubsegment.cs @@ -2,8 +2,6 @@ namespace Asp.Versioning.Routing; -#pragma warning disable CA1040 // Avoid empty interfaces - /// /// Defines the behavior of a path subsegment. /// diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ParsedRouteAdapter{T}.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ParsedRouteAdapter{T}.cs index 28148ce3..76ac7302 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ParsedRouteAdapter{T}.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/ParsedRouteAdapter{T}.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1812 - namespace Asp.Versioning.Routing; using System.Reflection; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathContentSegmentAdapter{T}.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathContentSegmentAdapter{T}.cs index 77f350fa..7f62ae53 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathContentSegmentAdapter{T}.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathContentSegmentAdapter{T}.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1812 - namespace Asp.Versioning.Routing; using static System.Linq.Expressions.Expression; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathLiteralSubsegmentAdapter{T}.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathLiteralSubsegmentAdapter{T}.cs index 65e1bd05..a565ba0e 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathLiteralSubsegmentAdapter{T}.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathLiteralSubsegmentAdapter{T}.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1812 - namespace Asp.Versioning.Routing; using static System.Linq.Expressions.Expression; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathParameterSubsegmentAdapter{T}.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathParameterSubsegmentAdapter{T}.cs index f51c711c..5250bfe1 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathParameterSubsegmentAdapter{T}.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathParameterSubsegmentAdapter{T}.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1812 - namespace Asp.Versioning.Routing; using static System.Linq.Expressions.Expression; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathSeparatorSegmentAdapter{T}.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathSeparatorSegmentAdapter{T}.cs index 9c16ab59..4d94a78d 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathSeparatorSegmentAdapter{T}.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/PathSeparatorSegmentAdapter{T}.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1812 - namespace Asp.Versioning.Routing; internal sealed class PathSeparatorSegmentAdapter : IPathSeparatorSegment where T : notnull diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/UrlHelperExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/UrlHelperExtensions.cs index a53f0f3e..121d6e75 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/UrlHelperExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Routing/UrlHelperExtensions.cs @@ -3,6 +3,7 @@ namespace System.Web.Http.Routing; using Asp.Versioning.Routing; +using Backport; /// /// Provides extension methods for . @@ -22,10 +23,7 @@ public static class UrlHelperExtensions /// it would be erroneously added as a query string parameter. public static UrlHelper WithoutApiVersion( this UrlHelper urlHelper ) { - if ( urlHelper == null ) - { - throw new ArgumentNullException( nameof( urlHelper ) ); - } + ArgumentNullException.ThrowIfNull( urlHelper ); if ( urlHelper is WithoutApiVersionUrlHelper ) { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.Designer.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.Designer.cs index 56042d8b..c272abc8 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.Designer.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.Designer.cs @@ -114,6 +114,15 @@ internal static string ApiVersionUnspecified { } } + /// + /// Looks up a localized string similar to The value cannot be an empty string.. + /// + internal static string Argument_EmptyString { + get { + return ResourceManager.GetString("Argument_EmptyString", resourceCulture); + } + } + /// /// Looks up a localized string similar to No route providing a controller name was found to match request URI '{0}'.. /// @@ -159,6 +168,15 @@ internal static string DirectRoute_AmbiguousController { } } + /// + /// Looks up a localized string similar to {0}.{1} is an invalid value for {2}.{3}. Did you mean to apply {4} via attribute or convention instead?. + /// + internal static string InvalidDefaultApiVersion { + get { + return ResourceManager.GetString("InvalidDefaultApiVersion", resourceCulture); + } + } + /// /// Looks up a localized string similar to A controller was not selected for request URI '{0}' and API version '{1}'.. /// @@ -168,6 +186,15 @@ internal static string NoControllerSelected { } } + /// + /// Looks up a localized string similar to No service for type '{0}' has been registered.. + /// + internal static string NoServiceRegistered { + get { + return ResourceManager.GetString("NoServiceRegistered", resourceCulture); + } + } + /// /// Looks up a localized string similar to No HTTP resource was found that matches the request URI '{0}'.. /// diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.resx b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.resx index ed143dc1..dced0e03 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.resx +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SR.resx @@ -135,6 +135,9 @@ An API version is required, but was not specified. + + The value cannot be an empty string. + No route providing a controller name was found to match request URI '{0}'. @@ -150,9 +153,20 @@ Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.{1}{1}The request has found the following matching controller types: {0} + + {0}.{1} is an invalid value for {2}.{3}. Did you mean to apply {4} via attribute or convention instead? + 0 = ApiVersion +1 = Neutral +2 = ApiVersioningOptions +3 = DefaultApiVersion +4 = IApiVersionNeutral + A controller was not selected for request URI '{0}' and API version '{1}'. + + No service for type '{0}' has been registered. + No HTTP resource was found that matches the request URI '{0}'. diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs index b15d2160..78af0cdf 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/SunsetPolicyManager.cs @@ -8,17 +8,10 @@ namespace Asp.Versioning; public partial class SunsetPolicyManager { private readonly ApiVersioningOptions options; - private static ISunsetPolicyManager? @default; /// /// Initializes a new instance of the class. /// /// The associated API versioning options. public SunsetPolicyManager( ApiVersioningOptions options ) => this.options = options; - - internal static ISunsetPolicyManager Default - { - get => @default ?? new SunsetPolicyManager( new ApiVersioningOptions() ); - set => @default = value; - } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpRequestMessageExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpRequestMessageExtensions.cs index 25abca3a..e6163773 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpRequestMessageExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpRequestMessageExtensions.cs @@ -3,6 +3,7 @@ namespace System.Net.Http; using Asp.Versioning; +using Backport; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Web.Http; @@ -22,9 +23,7 @@ private static HttpResponseMessage CreateErrorResponse( this HttpRequestMessage if ( configuration == null ) { -#pragma warning disable CA2000 // Dispose objects before losing scope configuration = new HttpConfiguration(); -#pragma warning restore CA2000 // Dispose objects before losing scope request.RegisterForDispose( configuration ); request.SetConfiguration( configuration ); } @@ -60,9 +59,7 @@ public static ApiVersioningOptions GetApiVersioningOptions( this HttpRequestMess if ( configuration == null ) { -#pragma warning disable CA2000 // Dispose objects before losing scope configuration = new HttpConfiguration(); -#pragma warning restore CA2000 // Dispose objects before losing scope request.RegisterForDispose( configuration ); request.SetConfiguration( configuration ); } @@ -77,14 +74,11 @@ public static ApiVersioningOptions GetApiVersioningOptions( this HttpRequestMess /// The current API versioning properties. public static ApiVersionRequestProperties ApiVersionProperties( this HttpRequestMessage request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull( request ); - if ( request.Properties.TryGetValue( ApiVersionPropertiesKey, out ApiVersionRequestProperties properties ) ) + if ( request.Properties.TryGetValue( ApiVersionPropertiesKey, out ApiVersionRequestProperties? properties ) ) { - return properties; + return properties!; } var forceRouteConstraintEvaluation = !request.Properties.ContainsKey( RoutingContextKey ); @@ -126,14 +120,19 @@ internal static Tuple GetProblemDetail { var configuration = request.GetConfiguration(); var negotiator = configuration.Services.GetContentNegotiator(); - var result = negotiator.Negotiate( typeof( ProblemDetails ), request, configuration.Formatters ) ?? - new( configuration.Formatters.JsonFormatter ?? new(), MediaTypeHeaderValue.Parse( "application/problem+json" ) ); + var result = negotiator.Negotiate( typeof( ProblemDetails ), request, configuration.Formatters ); return result.MediaType.MediaType switch { - "application/json" => Tuple.Create( MediaTypeHeaderValue.Parse( "application/problem+json" ), result.Formatter ), - "application/xml" => Tuple.Create( MediaTypeHeaderValue.Parse( "application/problem+xml" ), result.Formatter ), - _ => Tuple.Create( result.MediaType, result.Formatter ), + null => Tuple.Create( + MediaTypeHeaderValue.Parse( ProblemDetailsDefaults.MediaType.Json ), + (MediaTypeFormatter) ( configuration.Formatters.JsonFormatter ?? new() ) ), + "application/xml" => Tuple.Create( + MediaTypeHeaderValue.Parse( ProblemDetailsDefaults.MediaType.Xml ), + result.Formatter ), + _ => Tuple.Create( + result.MediaType, + result.Formatter ), }; } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs index 4b9d2354..2f9e1510 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Net.Http/HttpResponseMessageExtensions.cs @@ -3,7 +3,7 @@ namespace System.Net.Http; using Asp.Versioning; -using System; +using Backport; using System.Collections.Generic; using System.Net.Http.Headers; @@ -22,15 +22,8 @@ public static class HttpResponseMessageExtensions /// The sunset policy to write. public static void WriteSunsetPolicy( this HttpResponseMessage response, SunsetPolicy sunsetPolicy ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } - - if ( sunsetPolicy == null ) - { - throw new ArgumentNullException( nameof( sunsetPolicy ) ); - } + ArgumentNullException.ThrowIfNull( response ); + ArgumentNullException.ThrowIfNull( sunsetPolicy ); var headers = response.Headers; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpActionDescriptorExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpActionDescriptorExtensions.cs index 0821d758..77a1f230 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpActionDescriptorExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpActionDescriptorExtensions.cs @@ -3,6 +3,7 @@ namespace System.Web.Http; using Asp.Versioning; +using Backport; using System.ComponentModel; using System.Web.Http.Controllers; @@ -20,14 +21,11 @@ public static class HttpActionDescriptorExtensions /// The API version information for the action. public static ApiVersionMetadata GetApiVersionMetadata( this HttpActionDescriptor action ) { - if ( action == null ) - { - throw new ArgumentNullException( nameof( action ) ); - } + ArgumentNullException.ThrowIfNull( action ); - if ( action.Properties.TryGetValue( typeof( ApiVersionMetadata ), out ApiVersionMetadata value ) ) + if ( action.Properties.TryGetValue( typeof( ApiVersionMetadata ), out ApiVersionMetadata? value ) ) { - return value; + return value!; } return ApiVersionMetadata.Empty; @@ -42,11 +40,7 @@ public static ApiVersionMetadata GetApiVersionMetadata( this HttpActionDescripto [EditorBrowsable( EditorBrowsableState.Never )] public static void SetApiVersionMetadata( this HttpActionDescriptor action, ApiVersionMetadata value ) { - if ( action == null ) - { - throw new ArgumentNullException( nameof( action ) ); - } - + ArgumentNullException.ThrowIfNull( action ); action.Properties.AddOrUpdate( typeof( ApiVersionMetadata ), value, ( key, oldValue ) => value ); } diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs index 932639c4..0051de64 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpConfigurationExtensions.cs @@ -4,7 +4,11 @@ namespace System.Web.Http; using Asp.Versioning; using Asp.Versioning.Controllers; +using Asp.Versioning.Dependencies; using Asp.Versioning.Dispatcher; +using Asp.Versioning.Formatting; +using Backport; +using System.Globalization; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using static Asp.Versioning.ApiVersionParameterLocation; @@ -14,7 +18,7 @@ namespace System.Web.Http; /// public static class HttpConfigurationExtensions { - private const string ApiVersioningOptionsKey = "MS_ApiVersioningOptions"; + private const string ApiVersioningServicesKey = "MS_ApiVersioningServices"; /// /// Gets the current API versioning options. @@ -23,12 +27,22 @@ public static class HttpConfigurationExtensions /// The current API versioning options. public static ApiVersioningOptions GetApiVersioningOptions( this HttpConfiguration configuration ) { - if ( configuration == null ) - { - throw new ArgumentNullException( nameof( configuration ) ); - } + ArgumentNullException.ThrowIfNull( configuration ); + return configuration.ApiVersioningServices().ApiVersioningOptions; + } - return (ApiVersioningOptions) configuration.Properties.GetOrAdd( ApiVersioningOptionsKey, key => new ApiVersioningOptions() ); + /// + /// Converts problem details into error objects. + /// + /// The current configuration. + /// This enables backward compatibility by converting into Error Objects that + /// conform to the Error Responses + /// in the Microsoft REST API Guidelines and + /// OData Error Responses. + public static void ConvertProblemDetailsToErrorObject( this HttpConfiguration configuration ) + { + ArgumentNullException.ThrowIfNull( configuration ); + configuration.Initializer += EnableErrorObjectResponses; } /// @@ -37,11 +51,7 @@ public static ApiVersioningOptions GetApiVersioningOptions( this HttpConfigurati /// The configuration that will use service versioning. public static void AddApiVersioning( this HttpConfiguration configuration ) { - if ( configuration == null ) - { - throw new ArgumentNullException( nameof( configuration ) ); - } - + ArgumentNullException.ThrowIfNull( configuration ); configuration.AddApiVersioning( new ApiVersioningOptions() ); } @@ -52,19 +62,13 @@ public static void AddApiVersioning( this HttpConfiguration configuration ) /// An action used to configure the provided options. public static void AddApiVersioning( this HttpConfiguration configuration, Action setupAction ) { - if ( configuration == null ) - { - throw new ArgumentNullException( nameof( configuration ) ); - } - - if ( setupAction == null ) - { - throw new ArgumentNullException( nameof( setupAction ) ); - } + ArgumentNullException.ThrowIfNull( configuration ); + ArgumentNullException.ThrowIfNull( setupAction ); var options = new ApiVersioningOptions(); setupAction( options ); + ValidateApiVersioningOptions( options ); configuration.AddApiVersioning( options ); } @@ -92,20 +96,59 @@ private static void AddApiVersioning( this HttpConfiguration configuration, ApiV } } - configuration.Properties.AddOrUpdate( ApiVersioningOptionsKey, options, ( key, oldValue ) => options ); + configuration.ApiVersioningServices().ApiVersioningOptions = options; configuration.ParameterBindingRules.Add( typeof( ApiVersion ), ApiVersionParameterBinding.Create ); - SunsetPolicyManager.Default = new SunsetPolicyManager( options ); + configuration.Formatters.Insert( 0, new ProblemDetailsMediaTypeFormatter( configuration.Formatters.JsonFormatter ?? new() ) ); } - internal static IReportApiVersions GetApiVersionReporter( this HttpConfiguration configuration ) + // ApiVersion.Neutral does not have the same meaning as IApiVersionNeutral. setting + // ApiVersioningOptions.DefaultApiVersion this value will not make all APIs version-neutral + // and will likely lead to many unexpected side effects. this is a best-effort, one-time + // validation check to help prevent people from going off the rails. if someone bypasses + // this validation by removing the check or updating the value later, then caveat emptor. + // + // REF: https://github.com/dotnet/aspnet-api-versioning/issues/1011 + private static void ValidateApiVersioningOptions( ApiVersioningOptions options ) { - var options = configuration.GetApiVersioningOptions(); - - if ( options.ReportApiVersions ) + if ( options.DefaultApiVersion == ApiVersion.Neutral ) { - return configuration.DependencyResolver.GetApiVersionReporter(); + var message = string.Format( + CultureInfo.CurrentCulture, + SR.InvalidDefaultApiVersion, + nameof( ApiVersion ), + nameof( ApiVersion.Neutral ), + nameof( ApiVersioningOptions ), + nameof( ApiVersioningOptions.DefaultApiVersion ), + nameof( IApiVersionNeutral ) ); + + throw new InvalidOperationException( message ); } + } + + private static void EnableErrorObjectResponses( HttpConfiguration configuration ) + { + configuration.ApiVersioningServices().Replace( + typeof( IProblemDetailsFactory ), + static ( sc, t ) => new ErrorObjectFactory() ); + + var formatters = configuration.Formatters; + var problemDetails = ProblemDetailsMediaTypeFormatter.DefaultMediaType; - return DoNotReportApiVersions.Instance; + for ( var i = 0; i < formatters.Count; i++ ) + { + var mediaTypes = formatters[i].SupportedMediaTypes; + + for ( var j = 0; j < mediaTypes.Count; j++ ) + { + if ( mediaTypes[j].Equals( problemDetails ) ) + { + formatters.RemoveAt( i ); + return; + } + } + } } + + internal static DefaultContainer ApiVersioningServices( this HttpConfiguration configuration ) => + (DefaultContainer) configuration.Properties.GetOrAdd( ApiVersioningServicesKey, key => new DefaultContainer() ); } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpControllerDescriptorExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpControllerDescriptorExtensions.cs index 3e66c57d..8efe2d8c 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpControllerDescriptorExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpControllerDescriptorExtensions.cs @@ -4,6 +4,7 @@ namespace System.Web.Http; using Asp.Versioning; using Asp.Versioning.Controllers; +using Backport; using System.ComponentModel; using System.Web.Http.Controllers; using System.Web.Http.Description; @@ -34,14 +35,11 @@ public static class HttpControllerDescriptorExtensions [EditorBrowsable( EditorBrowsableState.Never )] public static ApiVersionModel GetApiVersionModel( this HttpControllerDescriptor controllerDescriptor ) { - if ( controllerDescriptor == null ) - { - throw new ArgumentNullException( nameof( controllerDescriptor ) ); - } + ArgumentNullException.ThrowIfNull( controllerDescriptor ); - if ( controllerDescriptor.Properties.TryGetValue( typeof( ApiVersionModel ), out ApiVersionModel value ) ) + if ( controllerDescriptor.Properties.TryGetValue( typeof( ApiVersionModel ), out ApiVersionModel? value ) ) { - return value; + return value!; } return ApiVersionModel.Empty; @@ -56,10 +54,7 @@ public static ApiVersionModel GetApiVersionModel( this HttpControllerDescriptor [EditorBrowsable( EditorBrowsableState.Never )] public static void SetApiVersionModel( this HttpControllerDescriptor controllerDescriptor, ApiVersionModel value ) { - if ( controllerDescriptor == null ) - { - throw new ArgumentNullException( nameof( controllerDescriptor ) ); - } + ArgumentNullException.ThrowIfNull( controllerDescriptor ); controllerDescriptor.Properties.AddOrUpdate( typeof( ApiVersionModel ), value, ( key, oldValue ) => value ); @@ -84,10 +79,7 @@ public static IEnumerable AsEnumerable( this HttpContr internal static IEnumerable AsEnumerable( this HttpControllerDescriptor controllerDescriptor, bool includeCandidates ) { - if ( controllerDescriptor == null ) - { - throw new ArgumentNullException( nameof( controllerDescriptor ) ); - } + ArgumentNullException.ThrowIfNull( controllerDescriptor ); var visited = new HashSet(); @@ -107,12 +99,12 @@ internal static IEnumerable AsEnumerable( this HttpCon yield return controllerDescriptor; } - if ( !includeCandidates || !controllerDescriptor.Properties.TryGetValue( PossibleControllerCandidatesKey, out IEnumerable candidates ) ) + if ( !includeCandidates || !controllerDescriptor.Properties.TryGetValue( PossibleControllerCandidatesKey, out IEnumerable? candidates ) ) { yield break; } - foreach ( var candidate in candidates ) + foreach ( var candidate in candidates! ) { if ( visited.Add( candidate ) ) { diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteCollectionExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteCollectionExtensions.cs index d2d3a7c1..64310188 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteCollectionExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteCollectionExtensions.cs @@ -2,6 +2,7 @@ namespace System.Web.Http; +using Backport; using System.Reflection; using System.Web.Http.Routing; using static System.Reflection.BindingFlags; @@ -19,10 +20,7 @@ public static class HttpRouteCollectionExtensions /// routes mapped to their name. public static IReadOnlyDictionary ToDictionary( this HttpRouteCollection routes ) { - if ( routes == null ) - { - throw new ArgumentNullException( nameof( routes ) ); - } + ArgumentNullException.ThrowIfNull( routes ); const string HostedHttpRouteCollection = "System.Web.Http.WebHost.Routing.HostedHttpRouteCollection"; diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteDataExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteDataExtensions.cs index 20d6990e..314c9bc9 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteDataExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteDataExtensions.cs @@ -33,6 +33,6 @@ internal static class HttpRouteDataExtensions } } - return list.ToArray(); + return [.. list]; } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteExtensions.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteExtensions.cs index 5c69c15c..9971270b 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteExtensions.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/System.Web.Http/HttpRouteExtensions.cs @@ -20,7 +20,7 @@ internal static class HttpRouteExtensions var directRouteActions = default( HttpActionDescriptor[] ); - if ( dataTokens.TryGetValue( RouteDataTokenKeys.Actions, out HttpActionDescriptor[] possibleDirectRouteActions ) && + if ( dataTokens.TryGetValue( RouteDataTokenKeys.Actions, out HttpActionDescriptor[]? possibleDirectRouteActions ) && possibleDirectRouteActions != null && possibleDirectRouteActions.Length > 0 ) { @@ -49,6 +49,6 @@ internal static class HttpRouteExtensions candidates.Add( new CandidateAction( directRouteActions[i], order, precedence ) ); } - return candidates.ToArray(); + return [.. candidates]; } } \ No newline at end of file diff --git a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/UrlSegmentApiVersionReader.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/UrlSegmentApiVersionReader.cs index 04e31809..5cfa1e1e 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/UrlSegmentApiVersionReader.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/UrlSegmentApiVersionReader.cs @@ -11,10 +11,7 @@ public partial class UrlSegmentApiVersionReader /// public virtual IReadOnlyList Read( HttpRequestMessage request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull(request); if ( reentrant ) { diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/ApiExplorer/VersionedApiExplorerTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/ApiExplorer/VersionedApiExplorerTest.cs index e42d2651..42c21028 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/ApiExplorer/VersionedApiExplorerTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/ApiExplorer/VersionedApiExplorerTest.cs @@ -1,7 +1,10 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dcase +//// Ignore Spelling: Dinsensitive namespace Asp.Versioning.ApiExplorer; + using Asp.Versioning.Models; using Asp.Versioning.Routing; using Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiDescriptionGroupCollectionTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiDescriptionGroupCollectionTest.cs index 55dc4e1d..bca97b12 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiDescriptionGroupCollectionTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiDescriptionGroupCollectionTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: denormalized + namespace Asp.Versioning.Description; using System.Collections.ObjectModel; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiVersionParameterDescriptionContextTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiVersionParameterDescriptionContextTest.cs index dca4f3b8..d2fb7be2 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiVersionParameterDescriptionContextTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/ApiVersionParameterDescriptionContextTest.cs @@ -4,13 +4,11 @@ namespace Asp.Versioning.Description; using Asp.Versioning.ApiExplorer; using Asp.Versioning.Routing; -using System.Collections.ObjectModel; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Description; -using System.Web.Http.Filters; using System.Web.Http.Routing; using static Asp.Versioning.ApiVersionParameterLocation; using static System.Web.Http.Description.ApiParameterSource; @@ -234,11 +232,19 @@ public void add_parameter_should_add_optional_parameter_when_allowed() var configuration = new HttpConfiguration(); var action = NewActionDescriptor(); var description = new ApiDescription() { ActionDescriptor = action }; - var version = new ApiVersion( 1, 0 ); - var options = new ApiExplorerOptions( configuration ); + var version = new ApiVersion( 2.0 ); + var options = new ApiExplorerOptions( configuration ) + { + ApiVersionSelector = new ConstantApiVersionSelector( version ), + }; action.Configuration = configuration; - configuration.AddApiVersioning( o => o.AssumeDefaultVersionWhenUnspecified = true ); + configuration.AddApiVersioning( + options => + { + options.DefaultApiVersion = ApiVersion.Default; + options.AssumeDefaultVersionWhenUnspecified = true; + } ); var context = new ApiVersionParameterDescriptionContext( description, version, options ); @@ -255,7 +261,7 @@ public void add_parameter_should_add_optional_parameter_when_allowed() ParameterDescriptor = new { ParameterName = "api-version", - DefaultValue = "1.0", + DefaultValue = "2.0", IsOptional = true, Configuration = configuration, ActionDescriptor = action, @@ -291,9 +297,9 @@ private static HttpActionDescriptor NewActionDescriptor() var action = new Mock() { CallBase = true }.Object; var controller = new Mock() { CallBase = true }; - controller.Setup( c => c.GetCustomAttributes( It.IsAny() ) ).Returns( new Collection() ); - controller.Setup( c => c.GetCustomAttributes( It.IsAny() ) ).Returns( new Collection() ); - controller.Setup( c => c.GetFilters() ).Returns( new Collection() ); + controller.Setup( c => c.GetCustomAttributes( It.IsAny() ) ).Returns( [] ); + controller.Setup( c => c.GetCustomAttributes( It.IsAny() ) ).Returns( [] ); + controller.Setup( c => c.GetFilters() ).Returns( [] ); action.ControllerDescriptor = controller.Object; return action; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/InternalTypeExtensions.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/InternalTypeExtensions.cs index b2458e22..4dd18890 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/InternalTypeExtensions.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Description/InternalTypeExtensions.cs @@ -20,9 +20,9 @@ internal static void EnsureInitialized( this IHttpRoute route, Func actions, bool targetIsAction ) diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/GenericMutableObject{T}.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/GenericMutableObject{T}.cs index db85d146..98fc3dbd 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/GenericMutableObject{T}.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/GenericMutableObject{T}.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Foo + namespace Asp.Versioning.Models; public class GenericMutableObject : List diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/MutableObject.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/MutableObject.cs index d5b0daaf..cef08ef1 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/MutableObject.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Models/MutableObject.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Foo + namespace Asp.Versioning.Models; public class MutableObject diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/ApiExplorerValuesController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/ApiExplorerValuesController.cs index 9a0218d1..c75b460b 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/ApiExplorerValuesController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/ApiExplorerValuesController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeApiExplorerValuesController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeApiExplorerValuesController.cs index 7e156d55..62240c77 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeApiExplorerValuesController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeApiExplorerValuesController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues2Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues2Controller.cs index a7bb3f20..709b16db 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues2Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues2Controller.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0060 -#pragma warning disable CA1062 // Validate arguments of public methods -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues3Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues3Controller.cs index d25f7216..1c17d4bf 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues3Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/AttributeValues3Controller.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0060 -#pragma warning disable CA1062 // Validate arguments of public methods namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/DuplicatedIdController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/DuplicatedIdController.cs index f6b40d55..1c401fc1 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/DuplicatedIdController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/DuplicatedIdController.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0060 -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/IgnoreApiValuesController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/IgnoreApiValuesController.cs index 734d9990..2107688b 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/IgnoreApiValuesController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/IgnoreApiValuesController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values2Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values2Controller.cs index a1b8b3b5..796aba7c 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values2Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values2Controller.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods -#pragma warning disable CA1822 // Mark members as static +#pragma warning disable IDE0060 namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values3Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values3Controller.cs index 69e8b09a..04f09f1c 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values3Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.ApiExplorer.Tests/Simulators/Values3Controller.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0060 -#pragma warning disable CA1062 // Validate arguments of public methods namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Controllers/HttpControllerDescriptorGroupTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Controllers/HttpControllerDescriptorGroupTest.cs index 2d4227c3..0b9ba137 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Controllers/HttpControllerDescriptorGroupTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Controllers/HttpControllerDescriptorGroupTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: eq + namespace Asp.Versioning.Controllers; using System.Collections.ObjectModel; @@ -51,12 +53,12 @@ public void get_custom_attributes_should_aggregate_attributes() var configuration = new HttpConfiguration(); descriptor1.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) - .Returns( () => new Collection() { new ApiVersionAttribute( "1.0" ) } ); + .Returns( () => new Collection() { new( "1.0" ) } ); descriptor1.Object.Configuration = configuration; descriptor1.Object.Properties[typeof( ApiVersionModel )] = new ApiVersionModel( new ApiVersion( 1, 0 ) ); descriptor2.Setup( d => d.GetCustomAttributes( It.IsAny() ) ) - .Returns( () => new Collection() { new ApiVersionAttribute( "2.0" ) } ); + .Returns( () => new Collection() { new( "2.0" ) } ); descriptor2.Object.Configuration = configuration; descriptor2.Object.Properties[typeof( ApiVersionModel )] = new ApiVersionModel( new ApiVersion( 2, 0 ) ); diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs index d53ccda7..f99b03cf 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs @@ -2,7 +2,6 @@ namespace Asp.Versioning.Conventions; -using System.Collections.ObjectModel; using System.Web.Http; using System.Web.Http.Controllers; using static Asp.Versioning.ApiVersionMapping; @@ -17,7 +16,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; - actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( [] ); actionDescriptor.Object.ControllerDescriptor = new(); // act @@ -43,7 +42,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; - actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( [] ); actionDescriptor.Object.ControllerDescriptor = new(); actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ); diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs index 12a5f6ee..e7626764 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs @@ -2,7 +2,6 @@ namespace Asp.Versioning.Conventions; -using System.Collections.ObjectModel; using System.Web.Http; using System.Web.Http.Controllers; using static Asp.Versioning.ApiVersionMapping; @@ -17,7 +16,7 @@ public void apply_to_should_assign_empty_model_without_api_versions_from_mapped_ var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; - actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( [] ); actionDescriptor.Object.ControllerDescriptor = new(); // act @@ -43,7 +42,7 @@ public void apply_to_should_assign_model_with_declared_api_versions_from_mapped_ var actionBuilder = new ActionApiVersionConventionBuilder( controllerBuilder ); var actionDescriptor = new Mock() { CallBase = true }; - actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( new Collection() ); + actionDescriptor.Setup( ad => ad.GetCustomAttributes() ).Returns( [] ); actionDescriptor.Object.ControllerDescriptor = new(); actionBuilder.MapToApiVersion( new ApiVersion( 2, 0 ) ); diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs index e2fc1c6c..82164eb5 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs @@ -19,7 +19,7 @@ public void apply_to_should_assign_conventions_to_controller() var controllerDescriptor = mock.Object; var controllerBuilder = default( IControllerConventionBuilder ); - mock.Setup( cd => cd.GetCustomAttributes() ).Returns( new Collection() ); + mock.Setup( cd => cd.GetCustomAttributes() ).Returns( [] ); controllerDescriptor.Configuration = configuration; controllerDescriptor.ControllerType = typeof( UndecoratedController ); configuration.AddApiVersioning( o => controllerBuilder = o.Conventions.Controller() ); @@ -54,7 +54,7 @@ public void apply_to_should_assign_empty_conventions_to_api_version_neutral_cont var controllerDescriptor = mock.Object; var controllerBuilder = default( IControllerConventionBuilder ); - mock.Setup( cd => cd.GetCustomAttributes() ).Returns( new Collection() ); + mock.Setup( cd => cd.GetCustomAttributes() ).Returns( [] ); controllerDescriptor.Configuration = configuration; controllerDescriptor.ControllerType = typeof( UndecoratedController ); configuration.AddApiVersioning( o => controllerBuilder = o.Conventions.Controller() ); diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs index 6990e951..eedc943c 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs @@ -20,7 +20,7 @@ public void apply_to_should_assign_conventions_to_controller() var controllerDescriptor = mock.Object; var controllerBuilder = default( IControllerConventionBuilder ); - mock.Setup( cd => cd.GetCustomAttributes() ).Returns( new Collection() ); + mock.Setup( cd => cd.GetCustomAttributes() ).Returns( [] ); controllerDescriptor.Configuration = configuration; controllerDescriptor.ControllerType = typeof( UndecoratedController ); configuration.AddApiVersioning( o => controllerBuilder = o.Conventions.Controller( typeof( UndecoratedController ) ) ); @@ -55,7 +55,7 @@ public void apply_to_should_assign_empty_conventions_to_api_version_neutral_cont var controllerDescriptor = mock.Object; var controllerBuilder = default( IControllerConventionBuilder ); - mock.Setup( cd => cd.GetCustomAttributes() ).Returns( new Collection() ); + mock.Setup( cd => cd.GetCustomAttributes() ).Returns( [] ); controllerDescriptor.Configuration = configuration; controllerDescriptor.ControllerType = typeof( UndecoratedController ); configuration.AddApiVersioning( o => controllerBuilder = o.Conventions.Controller( typeof( UndecoratedController ) ) ); diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs index dc3f2eb5..23e70e9e 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/DefaultApiVersionReporterTest.cs @@ -5,7 +5,6 @@ namespace Asp.Versioning; using Asp.Versioning.Simulators; using System.Web.Http; using System.Web.Http.Controllers; -using System.Web.Http.Dependencies; using static System.Net.HttpStatusCode; public class DefaultApiVersionReporterTest @@ -14,9 +13,8 @@ public class DefaultApiVersionReporterTest public void report_should_add_expected_headers() { // arrange - var reporter = new DefaultApiVersionReporter(); var sunsetDate = DateTimeOffset.Now; - var dependencyResolver = new Mock(); + var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ) ); var configuration = new HttpConfiguration(); var request = new HttpRequestMessage(); var response = new HttpResponseMessage( OK ) { RequestMessage = request }; @@ -34,9 +32,6 @@ public void report_should_add_expected_headers() deprecatedAdvertisedVersions: Enumerable.Empty() ); var metadata = new ApiVersionMetadata( apiModel, endpointModel, "Test" ); - dependencyResolver.Setup( dr => dr.GetService( typeof( ISunsetPolicyManager ) ) ) - .Returns( new TestSunsetPolicyManager( sunsetDate ) ); - configuration.DependencyResolver = dependencyResolver.Object; request.SetConfiguration( configuration ); request.ApiVersionProperties().RequestedApiVersion = new ApiVersion( 1.0 ); request.Properties["MS_HttpActionDescriptor"] = diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.AmbiguousControllers.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.AmbiguousControllers.cs index 80957cb1..15512777 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.AmbiguousControllers.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.AmbiguousControllers.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1812 -#pragma warning disable CA1822 // Mark members as static #pragma warning disable SA1601 // Partial elements should be documented namespace Asp.Versioning.Dispatcher; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs index 3dc5df0d..e4c523fb 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs @@ -1,5 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dbased +//// Ignore Spelling: Dneutral + namespace Asp.Versioning.Dispatcher; using Asp.Versioning; @@ -163,7 +166,7 @@ public void select_controller_should_return_correct_versionX2DneutralX2C_convent } [Fact] - public async Task select_controller_should_return_404_for_unmatchedX2C_attributeX2Dbased_controller_version() + public async Task select_controller_should_return_400_for_unmatchedX2C_attributeX2Dbased_controller_version() { // arrange var detail = "The HTTP resource that matches the request URI 'http://localhost/api/test' does not support the API version '42.0'."; @@ -190,13 +193,13 @@ public async Task select_controller_should_return_404_for_unmatchedX2C_attribute var content = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0, 4.0" ); response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "3.0-Alpha" ); content.Should().BeEquivalentTo( new ProblemDetails() { - Status = 404, + Status = 400, Title = "Unsupported API version", Type = ProblemDetailsDefaults.Unsupported.Type, Detail = detail, @@ -255,7 +258,7 @@ public async Task select_controller_should_return_400_for_attributeX2Dbased_cont } [Fact] - public async Task select_controller_should_return_404_for_unmatchedX2C_conventionX2Dbased_controller_version() + public async Task select_controller_should_return_400_for_unmatchedX2C_conventionX2Dbased_controller_version() { // arrange var detail = "The HTTP resource that matches the request URI 'http://localhost/api/test' does not support the API version '4.0'."; @@ -283,13 +286,13 @@ public async Task select_controller_should_return_404_for_unmatchedX2C_conventio var content = await response.Content.ReadAsProblemDetailsAsync(); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0" ); response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "1.8, 1.9" ); content.Should().BeEquivalentTo( new ProblemDetails() { - Status = 404, + Status = 400, Title = "Unsupported API version", Type = ProblemDetailsDefaults.Unsupported.Type, Detail = detail, @@ -413,7 +416,7 @@ public void select_controller_should_return_400_when_no_version_is_specified_and } [Fact] - public void select_controller_should_return_404_for_unmatched_action() + public void select_controller_should_return_400_for_unmatched_action() { // arrange var configuration = AttributeRoutingEnabledConfiguration; @@ -433,7 +436,7 @@ public void select_controller_should_return_404_for_unmatched_action() var response = selectController.Should().Throw().Subject.Single().Response; // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0, 3.0, 4.0" ); response.Headers.GetValues( "api-deprecated-versions" ).Single().Should().Be( "3.0-Alpha" ); } diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderBuilderTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderBuilderTest.cs index b980fc31..499d07fe 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderBuilderTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderBuilderTest.cs @@ -125,7 +125,7 @@ public void read_should_retrieve_version_from_content_type_and_accept() var versions = reader.Read( request ); // assert - versions.Should().BeEquivalentTo( new[] { "1.5", "2.0" } ); + versions.Should().BeEquivalentTo( ["1.5", "2.0"] ); } [Fact] @@ -246,7 +246,7 @@ public void read_should_only_retrieve_included_media_types() [InlineData( "application/vnd-v{v}+json", "v", "application/vnd-v2.1+json", "2.1" )] [InlineData( "application/vnd-v{ver}+json", "ver", "application/vnd-v2022-11-01+json", "2022-11-01" )] [InlineData( "application/vnd-{version}+xml", "version", "application/vnd-1.1-beta+xml", "1.1-beta" )] - public void read_should_retreive_version_from_media_type_template( + public void read_should_retrieve_version_from_media_type_template( string template, string parameterName, string mediaType, diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderTest.cs index bc4487b9..ea9ee138 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/MediaTypeApiVersionReaderTest.cs @@ -111,7 +111,7 @@ public void read_should_retrieve_version_from_content_type_and_accept() var versions = reader.Read( request ); // assert - versions.Should().BeEquivalentTo( new[] { "1.5", "2.0" } ); + versions.Should().BeEquivalentTo( ["1.5", "2.0"] ); } [Fact] diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/QueryStringApiVersionReaderTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/QueryStringApiVersionReaderTest.cs index 6355badf..c03525f9 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/QueryStringApiVersionReaderTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/QueryStringApiVersionReaderTest.cs @@ -86,8 +86,8 @@ public void read_should_not_throw_exception_when_duplicate_api_versions_are_requ } [Theory] - [InlineData( new object[] { new string[0] } )] - [InlineData( new object[] { new[] { "api-version" } } )] + [InlineData( [new string[0]] )] + [InlineData( [new[] { "api-version" }] )] public void add_parameters_should_add_single_parameter_from_query_string( string[] parameterNames ) { // arrange diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/ReportApiVersionsAttributeTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/ReportApiVersionsAttributeTest.cs index 57090ed2..ec30300a 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/ReportApiVersionsAttributeTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/ReportApiVersionsAttributeTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dneutral + namespace Asp.Versioning; using Asp.Versioning.Simulators; @@ -29,11 +31,12 @@ public void on_action_executed_should_add_version_headers() var method = controller.GetType().GetMethod( nameof( TestController.Get ) ); var controllerDescriptor = new Mock( configuration, "Test", controller.GetType() ) { CallBase = true }; var routeData = new HttpRouteData( new HttpRoute( "api/tests" ) ); - var controllerContext = new HttpControllerContext( new HttpConfiguration(), routeData, new HttpRequestMessage() ) { Controller = controller }; + var controllerContext = new HttpControllerContext( configuration, routeData, new HttpRequestMessage() ) { Controller = controller }; var actionDescriptor = new ReflectedHttpActionDescriptor( controllerDescriptor.Object, method ); var actionContext = new HttpActionContext( controllerContext, actionDescriptor ) { Response = new HttpResponseMessage() }; var context = new HttpActionExecutedContext( actionContext, null ); + configuration.AddApiVersioning( options => options.ReportApiVersions = true ); controllerContext.Request.SetConfiguration( new() ); controllerContext.Request.Properties["MS_HttpActionDescriptor"] = actionDescriptor; controllerDescriptor.Setup( cd => cd.GetCustomAttributes( It.IsAny() ) ).Returns( attributes ); @@ -69,11 +72,12 @@ public void on_action_executing_should_not_add_headers_for_versionX2Dneutral_con var method = controller.GetType().GetMethod( nameof( TestVersionNeutralController.Get ) ); var controllerDescriptor = new Mock( configuration, "Test", controller.GetType() ) { CallBase = true }; var routeData = new HttpRouteData( new HttpRoute( "api/tests" ) ); - var controllerContext = new HttpControllerContext( new HttpConfiguration(), routeData, new HttpRequestMessage() ) { Controller = new TestVersionNeutralController() }; + var controllerContext = new HttpControllerContext( configuration, routeData, new HttpRequestMessage() ) { Controller = new TestVersionNeutralController() }; var actionDescriptor = new ReflectedHttpActionDescriptor( controllerDescriptor.Object, method ); var actionContext = new HttpActionContext( controllerContext, actionDescriptor ) { Response = new HttpResponseMessage() }; var context = new HttpActionExecutedContext( actionContext, null ); + configuration.AddApiVersioning(); controllerDescriptor.Setup( cd => cd.GetCustomAttributes( It.IsAny() ) ).Returns( attributes ); controllerDescriptor.Object.Properties[typeof( ApiVersionModel )] = ApiVersionModel.Neutral; actionDescriptor.Properties[typeof( ApiVersionMetadata )] = ApiVersionMetadata.Neutral; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AdminController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AdminController.cs index ae3bbbd2..6bcd08cc 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AdminController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AdminController.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Admin + namespace Asp.Versioning.Simulators; using System.Web.Http; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRoute2Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRoute2Controller.cs index 51c53117..34fc582c 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRoute2Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRoute2Controller.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRouteController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRouteController.cs index 39a16df1..1cc6df30 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRouteController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ApiVersionedRouteController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest2Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest2Controller.cs index c0af43b2..257bbc60 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest2Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest2Controller.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest4Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest4Controller.cs index 84b21dae..f31b3581 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest4Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTest4Controller.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTestController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTestController.cs index 11374a40..bf3e867c 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTestController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/AttributeRoutedTestController.cs @@ -1,6 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static +#pragma warning disable IDE0060 namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ConventionsController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ConventionsController.cs index 5053fc3d..c7cd7e91 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ConventionsController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/ConventionsController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/NeutralController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/NeutralController.cs index 18d1a70a..8101ce4f 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/NeutralController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/NeutralController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/OrdersController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/OrdersController.cs index cc482c4a..1d42a805 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/OrdersController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/OrdersController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1707 // Identifiers should not contain underscores namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestController.cs index a141a1e6..3c145fb1 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersion2Controller.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersion2Controller.cs index c6c87bdf..89d77a80 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersion2Controller.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersion2Controller.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersionNeutralController.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersionNeutralController.cs index 89369fe3..d9ceb578 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersionNeutralController.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Simulators/TestVersionNeutralController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.Simulators; diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs index a77f8010..22cec40e 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpConfigurationExtensionsTest.cs @@ -37,4 +37,17 @@ public void add_api_versioning_should_report_api_versions_when_option_is_enabled configuration.Services.GetActionSelector().Should().BeOfType(); configuration.Filters.Single().Instance.Should().BeOfType(); } + + [Fact] + public void add_api_versioning_should_not_allow_default_neutral_api_version() + { + // arrange + var configuration = new HttpConfiguration(); + + // act + Action options = () => configuration.AddApiVersioning( options => options.DefaultApiVersion = ApiVersion.Neutral ); + + // assert + options.Should().Throw(); + } } \ No newline at end of file diff --git a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpRouteCollectionExtensionsTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpRouteCollectionExtensionsTest.cs index 8d270992..eb271b38 100644 --- a/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpRouteCollectionExtensionsTest.cs +++ b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/System.Web.Http/HttpRouteCollectionExtensionsTest.cs @@ -56,7 +56,7 @@ namespace System.Web.Http.WebHost.Routing internal sealed class HostedHttpRouteCollection : HttpRouteCollection { #pragma warning disable SA1309 // Field names should not begin with underscore - private readonly RouteCollection _routeCollection = new(); + private readonly RouteCollection _routeCollection = []; #pragma warning restore SA1309 // Field names should not begin with underscore public override string VirtualPathRoot => throw NotUsedInUnitTest(); @@ -131,7 +131,7 @@ namespace System.Web.Http.WebHost.Routing internal sealed class HttpWebRoute : Route { public HttpWebRoute( IHttpRoute httpRoute ) - : base( httpRoute.RouteTemplate, new(), new(), new(), Mock.Of() ) => HttpRoute = httpRoute; + : base( httpRoute.RouteTemplate, [], [], [], Mock.Of() ) => HttpRoute = httpRoute; public IHttpRoute HttpRoute { get; } } diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Asp.Versioning.Mvc.Acceptance.Tests.csproj b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Asp.Versioning.Mvc.Acceptance.Tests.csproj index 4a88a59d..d891b0a2 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Asp.Versioning.Mvc.Acceptance.Tests.csproj +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Asp.Versioning.Mvc.Acceptance.Tests.csproj @@ -1,13 +1,13 @@  - net7.0 + $(DefaultTargetFramework) Asp.Versioning - - + + diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/FilteredControllerTypes.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/FilteredControllerTypes.cs index 6509a017..f84e913c 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/FilteredControllerTypes.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/FilteredControllerTypes.cs @@ -8,7 +8,7 @@ namespace Asp.Versioning; internal sealed class FilteredControllerTypes : ControllerFeatureProvider, ICollection { - private readonly HashSet controllerTypes = new(); + private readonly HashSet controllerTypes = []; protected override bool IsController( TypeInfo typeInfo ) => base.IsController( typeInfo ) && controllerTypes.Contains( typeInfo ); diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MediaTypeFixture.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MediaTypeFixture.cs new file mode 100644 index 00000000..5736c1bf --- /dev/null +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MediaTypeFixture.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Http; + +public class MediaTypeFixture : MinimalApiFixture +{ + protected override void OnAddApiVersioning( ApiVersioningOptions options ) => + options.ApiVersionReader = new MediaTypeApiVersionReader(); +} \ No newline at end of file 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 208c5130..20789427 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 @@ -1,6 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -namespace Asp.Versioning; +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable ASP0018 // Unused route parameter + +namespace Asp.Versioning.Http; using Asp.Versioning.Conventions; using Microsoft.AspNetCore.Builder; @@ -11,24 +14,16 @@ public class MinimalApiFixture : HttpServerFixture { protected override void OnConfigureEndpoints( IEndpointRouteBuilder endpoints ) { + endpoints.MapGet( "api/ping", () => Results.NoContent() ) + .WithApiVersionSet( endpoints.NewApiVersionSet().Build() ) + .IsApiVersionNeutral(); + var values = endpoints.NewApiVersionSet( "Values" ) .HasApiVersion( 1.0 ) .HasApiVersion( 2.0 ) .ReportApiVersions() .Build(); - var helloWorld = endpoints.NewApiVersionSet( "Hello World" ) - .HasApiVersion( 1.0 ) - .HasApiVersion( 2.0 ) - .ReportApiVersions() - .Build(); - - var orders = endpoints.NewApiVersionSet( "Orders" ).Build(); - - endpoints.MapGet( "api/ping", () => Results.NoContent() ) - .WithApiVersionSet( endpoints.NewApiVersionSet().Build() ) - .IsApiVersionNeutral(); - endpoints.MapGet( "api/values", () => "Value 1" ) .WithApiVersionSet( values ) .MapToApiVersion( 1.0 ); @@ -37,43 +32,34 @@ protected override void OnConfigureEndpoints( IEndpointRouteBuilder endpoints ) .WithApiVersionSet( values ) .MapToApiVersion( 2.0 ); - endpoints.MapGet( "api/v{version:apiVersion}/hello", () => "Hello world!" ) - .WithApiVersionSet( helloWorld ) - .MapToApiVersion( 1.0 ); - - endpoints.MapGet( "api/v{version:apiVersion}/hello/{text}", ( string text ) => text ) - .WithApiVersionSet( helloWorld ) - .MapToApiVersion( 1.0 ); - - endpoints.MapGet( "api/v{version:apiVersion}/hello", () => "Hello world! (v2)" ) - .WithApiVersionSet( helloWorld ) - .MapToApiVersion( 2.0 ); - - endpoints.MapGet( "api/v{version:apiVersion}/hello/{text}", ( string text ) => text + " (v2)" ) - .WithApiVersionSet( helloWorld ) - .MapToApiVersion( 2.0 ); - - endpoints.MapPost( "api/v{version:apiVersion}/hello", () => { } ) - .WithApiVersionSet( helloWorld ); + var orders = endpoints.NewVersionedApi( "Orders" ) + .MapGroup( "api/order" ) + .HasApiVersion( 1.0 ) + .HasApiVersion( 2.0 ); - endpoints.MapGet( "api/order", () => { } ) - .WithApiVersionSet( orders ) - .HasApiVersion( 1.0 ) - .HasApiVersion( 2.0 ); + orders.MapGet( "/", () => Results.Ok() ); + orders.MapGet( "/{id}", ( int id ) => Results.Ok() ).HasDeprecatedApiVersion( 0.9 ); + orders.MapPost( "/", () => Results.Created() ); + orders.MapDelete( "/{id}", ( int id ) => Results.NoContent() ).IsApiVersionNeutral(); - endpoints.MapGet( "api/order/{id}", ( int id ) => { } ) - .WithApiVersionSet( orders ) - .HasDeprecatedApiVersion( 0.9 ) - .HasApiVersion( 1.0 ) - .HasApiVersion( 2.0 ); + var helloWorld = endpoints.NewVersionedApi( "Orders" ) + .MapGroup( "api/v{version:apiVersion}/hello" ) + .HasApiVersion( 1.0 ) + .HasApiVersion( 2.0 ) + .ReportApiVersions(); - endpoints.MapPost( "api/order", () => { } ) - .WithApiVersionSet( orders ) - .HasApiVersion( 1.0 ) - .HasApiVersion( 2.0 ); + helloWorld.MapGet( "/", () => "Hello world!" ).MapToApiVersion( 1.0 ); + helloWorld.MapGet( "/{text}", ( string text ) => text ).MapToApiVersion( 1.0 ); + helloWorld.MapGet( "/", () => "Hello world! (v2)" ).MapToApiVersion( 2.0 ); + helloWorld.MapGet( "/{text}", ( string text ) => text + " (v2)" ).MapToApiVersion( 2.0 ); + helloWorld.MapPost( "/", () => { } ); + } - endpoints.MapDelete( "api/order/{id}", ( int id ) => { } ) - .WithApiVersionSet( orders ) - .IsApiVersionNeutral(); + protected override void OnAddApiVersioning( ApiVersioningOptions options ) + { + options.ApiVersionReader = ApiVersionReader.Combine( + new QueryStringApiVersionReader(), + new UrlSegmentApiVersionReader(), + new MediaTypeApiVersionReader() ); } } \ No newline at end of file diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a media type.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a media type.cs new file mode 100644 index 00000000..ee292d3d --- /dev/null +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a media type.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Http; + +using Asp.Versioning; +using System.Net.Http; +using System.Net.Http.Json; +using static System.Net.Http.Headers.MediaTypeWithQualityHeaderValue; +using static System.Net.Http.HttpMethod; +using static System.Net.HttpStatusCode; + +public class when_using_a_media_type : AcceptanceTest, IClassFixture +{ + [Fact] + public async Task problem_details_should_be_returned_for_accept_header_with_unsupported_api_version() + { + // arrange + using var request = new HttpRequestMessage( Post, "api/values" ) + { + Headers = { Accept = { Parse( "application/json;v=3.0" ) } }, + Content = JsonContent.Create( new { test = true }, Parse( "application/json;v=3.0" ) ), + }; + + // act + var response = await Client.SendAsync( request ); + var problem = await response.Content.ReadAsProblemDetailsAsync(); + + // assert + response.StatusCode.Should().Be( UnsupportedMediaType ); + problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type ); + } + + public when_using_a_media_type( MediaTypeFixture 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/Http/given a versioned minimal API/when using an endpoint.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using an endpoint.cs index 45329292..de479673 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using an endpoint.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using an endpoint.cs @@ -11,7 +11,7 @@ namespace given_a_versioned_minimal_API; public class when_using_an_endpoint : AcceptanceTest { [Theory] - [InlineData( "api/order?api-version=0.9", NotFound )] + [InlineData( "api/order?api-version=0.9", BadRequest )] [InlineData( "api/order?api-version=1.0", OK )] [InlineData( "api/order?api-version=2.0", OK )] [InlineData( "api/order/42?api-version=0.9", OK )] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/HttpServerFixture.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/HttpServerFixture.cs index e04031d1..57c872e1 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/HttpServerFixture.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/HttpServerFixture.cs @@ -1,5 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. + +// Ignore Spelling: app +// Ignore Spelling: Mvc namespace Asp.Versioning; using Microsoft.AspNetCore.Builder; @@ -18,7 +21,7 @@ public abstract partial class HttpServerFixture { private string visualizationUrl; - public string DirectedGraphVisualizationUrl => + internal string DirectedGraphVisualizationUrl => visualizationUrl ??= GenerateEndpointDirectedGraph( Server.Services ); protected virtual void OnConfigurePartManager( ApplicationPartManager partManager ) => diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OrdersController.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OrdersController.cs index 2d94fbaa..5fc09eb7 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OrdersController.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OrdersController.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0060 + namespace Asp.Versioning.Mvc.UsingAttributes.Controllers; using Asp.Versioning.Mvc.UsingAttributes.Models; diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OverlappingRouteTemplateController.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OverlappingRouteTemplateController.cs index da532780..f028c66f 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OverlappingRouteTemplateController.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/Controllers/OverlappingRouteTemplateController.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1822 // Mark members as static +#pragma warning disable CA1822 +#pragma warning disable IDE0060 namespace Asp.Versioning.Mvc.UsingAttributes.Controllers; diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs index f692f92d..7c78efd4 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs @@ -87,7 +87,7 @@ public async Task then_delete_should_return_405( string apiVersion ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -96,7 +96,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/values?api-version=3.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/Controllers/OrdersController.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/Controllers/OrdersController.cs index 8d1d1a34..8c481dc2 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/Controllers/OrdersController.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/Controllers/OrdersController.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0060 // Remove unused parameter + namespace Asp.Versioning.Mvc.UsingConventions.Controllers; using Asp.Versioning.Mvc.UsingConventions.Models; diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs index 069d4c36..081fae3c 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingConventions/given a versioned Controller using conventions/when using a query string and split into two types.cs @@ -29,7 +29,7 @@ public async Task then_get_should_return_200( string controller, string apiVersi } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -38,7 +38,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/values?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V1/OrdersController.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V1/OrdersController.cs index 1006619f..8b935d1e 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V1/OrdersController.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V1/OrdersController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1716 // Identifiers should not match keywords namespace Asp.Versioning.Mvc.UsingNamespace.Controllers.V1; diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V2/OrdersController.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V2/OrdersController.cs index 0d7e007a..9804051b 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V2/OrdersController.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/Controllers/V2/OrdersController.cs @@ -1,6 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1716 // Identifiers should not match keywords namespace Asp.Versioning.Mvc.UsingNamespace.Controllers.V2; diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs index 18669a23..7a1f29a4 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingNamespace/given a versioned Controller per namespace/when using a query string.cs @@ -34,7 +34,7 @@ public async Task then_get_should_return_200( Type controllerType, string apiVer } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -43,7 +43,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/agreements/42?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs index dc1ff8bb..82aced7b 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Advanced/given a versioned ODataController mixed with base controllers/when people is any version.cs @@ -2,14 +2,13 @@ namespace given_a_versioned_ODataController_mixed_with_base_controllers; -using Asp.Versioning; using Asp.Versioning.OData.Advanced; using static System.Net.HttpStatusCode; public class when_people_is_any_version : AdvancedAcceptanceTest { [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { lastName = "Me" }; @@ -18,7 +17,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var response = await PatchAsync( $"api/people/42?api-version=4.0", person ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs index e138f931..0294fe55 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/Controllers/WeatherForecastsController.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. #pragma warning disable IDE0060 // Remove unused parameter +#pragma warning disable IDE0079 #pragma warning disable CA1822 // Mark members as static namespace Asp.Versioning.OData.Basic.Controllers; diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs index 1cdad01e..e8c60654 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -37,7 +37,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/people?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] @@ -84,7 +84,7 @@ public async Task then_delete_should_return_405_for_unmatched_action() } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -93,7 +93,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var response = await PatchAsync( "api/people/42?api-version=4.0", person ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs index 4e8cce19..7cea36c0 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 @@ -17,14 +17,14 @@ public async Task then_get_should_return_200( string requestUrl ) // act - var response = (await GetAsync( requestUrl )).EnsureSuccessStatusCode(); + var response = ( await GetAsync( requestUrl ) ).EnsureSuccessStatusCode(); // assert response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0" ); } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -33,7 +33,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/orders?api-version=2.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given versioned batch middleware/when using a url segment.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given versioned batch middleware/when using a url segment.cs index cb9cc0fa..8d873512 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given versioned batch middleware/when using a url segment.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given versioned batch middleware/when using a url segment.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA2000 // Dispose objects before losing scope namespace given_versioned_batch_middleware; diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs index e9f86835..b6ba5605 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/CustomerModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; @@ -27,6 +25,9 @@ private static EntityTypeConfiguration ConfigureCurrent( ODataModelBui public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( apiVersion ); + switch ( apiVersion.MajorVersion ) { case 1: diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs index 6cb4a514..20cbfd11 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/OrderModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; @@ -24,6 +22,8 @@ private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuilde public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + if ( supportedApiVersion == null || supportedApiVersion == apiVersion ) { ConfigureCurrent( builder ); diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs index 46e67287..0c49a498 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/PersonModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; @@ -27,6 +25,9 @@ private static EntityTypeConfiguration ConfigureCurrent( ODataModelBuild public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( apiVersion ); + switch ( apiVersion.MajorVersion ) { case 1: diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs index 62a23acc..4456fa39 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Configuration/WeatherForecastModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.OData.Configuration; using Asp.Versioning.OData.Models; @@ -24,6 +22,8 @@ private static EntityTypeConfiguration ConfigureCurrent( ODataM public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + if ( supportedApiVersion == null || supportedApiVersion == apiVersion ) { ConfigureCurrent( builder ); diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs index 3bec69f6..4287b9a1 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/ODataAcceptanceTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dspecific + namespace Asp.Versioning.OData; using static System.Net.HttpStatusCode; @@ -37,7 +39,7 @@ public async Task then_the_service_document_should_be_versionX2Dspecific( string } [Fact] - public async Task then_the_service_document_should_return_404_for_an_unsupported_version() + public async Task then_the_service_document_should_return_400_for_an_unsupported_version() { // arrange @@ -46,7 +48,7 @@ public async Task then_the_service_document_should_return_404_for_an_unsupported var response = await GetAsync( "api?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] @@ -79,7 +81,7 @@ public async Task then_X24metadata_should_be_versionX2Dspecific( string apiVersi } [Fact] - public async Task then_X24metadata_should_return_404_for_an_unsupported_version() + public async Task then_X24metadata_should_return_400_for_an_unsupported_version() { // arrange @@ -88,7 +90,7 @@ public async Task then_X24metadata_should_return_404_for_an_unsupported_version( var response = await GetAsync( "api/$metadata?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } protected ODataAcceptanceTest( ODataFixture fixture ) : base( fixture ) { } diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs index 91d65526..cf15f390 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string and split into two types.cs @@ -28,7 +28,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -37,7 +37,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/people?api-version=4.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] @@ -71,7 +71,7 @@ public async Task then_patch_should_return_400_if_supported_in_any_version( stri } [Fact] - public async Task then_patch_should_return_404_for_an_unsupported_version() + public async Task then_patch_should_return_400_for_an_unsupported_version() { // arrange var person = new { id = 42, firstName = "John", lastName = "Doe", email = "john.doe@somewhere.com" }; @@ -80,7 +80,7 @@ public async Task then_patch_should_return_404_for_an_unsupported_version() var response = await PatchAsync( "api/people/42?api-version=4.0", person ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs index d054f0b1..9ae9f4c0 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/UsingConventions/given a versioned ODataController using conventions/when using a query string.cs @@ -24,7 +24,7 @@ public async Task then_get_should_return_200( string requestUrl ) } [Fact] - public async Task then_get_should_return_404_for_an_unsupported_version() + public async Task then_get_should_return_400_for_an_unsupported_version() { // arrange @@ -33,7 +33,7 @@ public async Task then_get_should_return_404_for_an_unsupported_version() var response = await GetAsync( "api/orders?api-version=2.0" ); // assert - response.StatusCode.Should().Be( NotFound ); + response.StatusCode.Should().Be( BadRequest ); } [Fact] diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/TestApplicationPart.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/TestApplicationPart.cs index e107ef34..03c8f591 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/TestApplicationPart.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/TestApplicationPart.cs @@ -1,27 +1,26 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -namespace Asp.Versioning -{ - using Microsoft.AspNetCore.Mvc.ApplicationParts; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; +namespace Asp.Versioning; + +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; - internal sealed class TestApplicationPart : ApplicationPart, IApplicationPartTypeProvider - { - public TestApplicationPart() => Types = Enumerable.Empty(); +internal sealed class TestApplicationPart : ApplicationPart, IApplicationPartTypeProvider +{ + public TestApplicationPart() => Types = Enumerable.Empty(); - public TestApplicationPart( params TypeInfo[] types ) => Types = types; + public TestApplicationPart( params TypeInfo[] types ) => Types = types; - public TestApplicationPart( IEnumerable types ) => Types = types; + public TestApplicationPart( IEnumerable types ) => Types = types; - public TestApplicationPart( IEnumerable types ) : this( types.Select( t => t.GetTypeInfo() ) ) { } + public TestApplicationPart( IEnumerable types ) : this( types.Select( t => t.GetTypeInfo() ) ) { } - public TestApplicationPart( params Type[] types ) : this( types.Select( t => t.GetTypeInfo() ) ) { } + public TestApplicationPart( params Type[] types ) : this( types.Select( t => t.GetTypeInfo() ) ) { } - public override string Name => "Test Part"; + public override string Name => "Test Part"; - public IEnumerable Types { get; } - } + public IEnumerable Types { get; } } \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ApiDescriptionExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ApiDescriptionExtensions.cs new file mode 100644 index 00000000..29520295 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ApiDescriptionExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Microsoft.AspNetCore.Mvc.ApiExplorer; + +internal static class ApiDescriptionExtensions +{ + internal static bool IsODataLike( this ApiDescription description ) + { + var parameters = description.ActionDescriptor.Parameters; + + for ( var i = 0; i < parameters.Count; i++ ) + { + if ( parameters[i].ParameterType.IsODataQueryOptions() ) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs index 0084cd01..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 @@ -92,10 +92,7 @@ public ODataApiDescriptionProvider( /// The default implementation performs no action. public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var results = context.Results; var visited = new HashSet( capacity: results.Count, new ApiDescriptionComparer() ); @@ -145,6 +142,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) else { UpdateModelTypes( result, matched ); + UpdateFunctionCollectionParameters( result, matched ); } } @@ -201,7 +199,7 @@ private static bool IsNavigationPropertyLink( ODataPathTemplate template ) => private static bool TryMatchModelVersion( ApiDescription description, - IReadOnlyList items, + IODataRoutingMetadata[] items, [NotNullWhen( true )] out IODataRoutingMetadata? metadata ) { if ( description.GetApiVersion() is not ApiVersion apiVersion ) @@ -218,10 +216,10 @@ private static bool TryMatchModelVersion( return false; } - for ( var i = 0; i < items.Count; i++ ) + for ( var i = 0; i < items.Length; i++ ) { var item = items[i]; - var otherApiVersion = item.Model.GetAnnotationValue( item.Model )?.ApiVersion; + var otherApiVersion = item.Model.GetApiVersion(); if ( apiVersion.Equals( otherApiVersion ) ) { @@ -459,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; diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs new file mode 100644 index 00000000..5c73744b --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning.OData; + +/// +/// Provides additional implementation specific to ASP.NET Core. +/// +public partial class ODataApiExplorerOptions +{ + /// + /// Initializes a new instance of the class. + /// + /// The associated model builder. + [CLSCompliant( false )] + public ODataApiExplorerOptions( VersionedODataModelBuilder modelBuilder ) => + AdHocModelBuilder = modelBuilder ?? throw new ArgumentNullException( nameof( modelBuilder ) ); +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs new file mode 100644 index 00000000..5e494688 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning.OData; +using Microsoft.Extensions.Options; +using static Asp.Versioning.ApiVersionMapping; + +/// +/// Represents a factory to create OData API explorer options. +/// +[CLSCompliant( false )] +public class ODataApiExplorerOptionsFactory : ApiExplorerOptionsFactory +{ + private readonly IApiVersionMetadataCollationProvider[] providers; + private readonly IEnumerable modelConfigurations; + + /// + /// Initializes a new instance of the class. + /// + /// A sequence of + /// providers used to collate API version metadata. + /// A sequence of + /// configurations used to configure Entity Data Models. + /// The API versioning options + /// used to create API explorer options. + /// The sequence of + /// configuration actions to run. + /// The sequence of + /// initialization actions to run. + public ODataApiExplorerOptionsFactory( + IEnumerable providers, + IEnumerable modelConfigurations, + IOptions options, + IEnumerable> setups, + IEnumerable> postConfigures ) + : base( options, setups, postConfigures ) + { + this.providers = ( providers ?? throw new ArgumentNullException( nameof( providers ) ) ).ToArray(); + this.modelConfigurations = modelConfigurations ?? throw new ArgumentNullException( nameof( modelConfigurations ) ); + } + + /// + /// Initializes a new instance of the class. + /// + /// A sequence of + /// providers used to collate API version metadata. + /// A sequence of + /// configurations used to configure Entity Data Models. + /// The API versioning options + /// used to create API explorer options. + /// The sequence of + /// configuration actions to run. + /// The sequence of + /// initialization actions to run. + /// The sequence of + /// validations to run. + public ODataApiExplorerOptionsFactory( + IEnumerable providers, + IEnumerable modelConfigurations, + IOptions options, + IEnumerable> setups, + IEnumerable> postConfigures, + IEnumerable> validations ) + : base( options, setups, postConfigures, validations ) + { + this.providers = ( providers ?? throw new ArgumentNullException( nameof( providers ) ) ).ToArray(); + this.modelConfigurations = modelConfigurations ?? throw new ArgumentNullException( nameof( modelConfigurations ) ); + } + + /// + protected override ODataApiExplorerOptions CreateInstance( string name ) + { + var options = new ODataApiExplorerOptions( new( CollateApiVersions( providers, Options ), modelConfigurations ) ); + CopyOptions( Options, options ); + return options; + } + + private static ODataApiVersionCollectionProvider CollateApiVersions( + IApiVersionMetadataCollationProvider[] providers, + ApiVersioningOptions options ) + { + var context = new ApiVersionMetadataCollationContext(); + + for ( var i = 0; i < providers.Length; i++ ) + { + providers[i].Execute( context ); + } + + var results = context.Results; + var versions = new SortedSet(); + + for ( var i = 0; i < results.Count; i++ ) + { + var model = results[i].Map( Implicit ); + var declared = model.DeclaredApiVersions; + + for ( var j = 0; j < declared.Count; j++ ) + { + versions.Add( declared[j] ); + } + } + + if ( versions.Count == 0 ) + { + versions.Add( options.DefaultApiVersion ); + } + + return new() { ApiVersions = versions.ToArray() }; + } + + private sealed class ODataApiVersionCollectionProvider : IODataApiVersionCollectionProvider + { + public required IReadOnlyList ApiVersions { get; set; } + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataQueryOptionModelMetadata.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataQueryOptionModelMetadata.cs index 062a95aa..bc2a8825 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataQueryOptionModelMetadata.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataQueryOptionModelMetadata.cs @@ -23,10 +23,7 @@ public sealed class ODataQueryOptionModelMetadata : ModelMetadata public ODataQueryOptionModelMetadata( IModelMetadataProvider modelMetadataProvider, Type modelType, string description ) : base( ModelMetadataIdentity.ForType( modelType ) ) { - if ( modelMetadataProvider == null ) - { - throw new ArgumentNullException( nameof( modelMetadataProvider ) ); - } + ArgumentNullException.ThrowIfNull( modelMetadataProvider ); inner = modelMetadataProvider.GetMetadataForType( modelType ); Description = description; diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs new file mode 100644 index 00000000..47eac901 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs @@ -0,0 +1,233 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Asp.Versioning; +using Asp.Versioning.Conventions; +using Asp.Versioning.OData; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.OData.Routing; +using Microsoft.AspNetCore.OData.Routing.Template; +using Microsoft.Extensions.Options; +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; +using System.Runtime.CompilerServices; +using Opts = Microsoft.Extensions.Options.Options; + +/// +/// Represents an API description provider for partial OData support. +/// +[CLSCompliant( false )] +public class PartialODataDescriptionProvider : IApiDescriptionProvider +{ + private static readonly int BeforeOData = ODataOrder() + 10; + private readonly IOptionsFactory odataOptionsFactory; + private readonly IOptions options; + private bool markedAdHoc; + private IODataQueryOptionsConvention[]? conventions; + + /// + /// Initializes a new instance of the class. + /// + /// The factory used to create + /// OData options. + /// The container of configured + /// API explorer options. + public PartialODataDescriptionProvider( + IOptionsFactory odataOptionsFactory, + IOptions options ) + { + this.odataOptionsFactory = odataOptionsFactory ?? throw new ArgumentNullException( nameof( odataOptionsFactory ) ); + this.options = options ?? throw new ArgumentNullException( nameof( options ) ); + } + + /// + /// Gets the associated OData API explorer options. + /// + /// The current OData API explorer options. + protected ODataApiExplorerOptions Options + { + get + { + var value = options.Value; + + if ( !markedAdHoc ) + { + value.AdHocModelBuilder.OnModelCreated += MarkAsAdHoc; + markedAdHoc = true; + } + + return value; + } + } + + /// + /// Gets the builder used to create ad hoc Entity Data Models (EDMs). + /// + /// The associated model builder. + protected VersionedODataModelBuilder ModelBuilder => Options.AdHocModelBuilder; + + /// + /// Gets associated the OData query option conventions. + /// + /// A read-only list of + /// OData query option conventions. + protected IReadOnlyList Conventions => + conventions ??= Options.AdHocModelBuilder.ModelConfigurations.OfType().ToArray(); + + /// + /// Gets or sets the order precedence of the current API description provider. + /// + /// The order precedence of the current API description provider. + public int Order { get; protected set; } = BeforeOData; + + /// + public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context ) + { + ArgumentNullException.ThrowIfNull( context ); + + var results = FilterResults( context.Results, Conventions ); + + if ( results.Length == 0 ) + { + return; + } + + var models = ModelBuilder.GetEdmModels(); + + for ( var i = 0; i < models.Count; i++ ) + { + var model = models[i]; + var version = model.GetApiVersion(); + var odata = odataOptionsFactory.Create( Opts.DefaultName ); + + odata.AddRouteComponents( model ); + + for ( var j = 0; j < results.Length; j++ ) + { + var result = results[j]; + var metadata = result.ActionDescriptor.GetApiVersionMetadata(); + + if ( metadata.IsMappedTo( version ) ) + { + result.ActionDescriptor.EndpointMetadata.Add( ODataMetadata.New( model ) ); + } + } + } + } + + /// + public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) + { + ArgumentNullException.ThrowIfNull( context ); + + var actions = context.Actions; + + for ( var i = 0; i < actions.Count; i++ ) + { + var metadata = actions[i].EndpointMetadata; + + for ( var j = metadata.Count - 1; j >= 0; j-- ) + { + if ( metadata[j] is IODataRoutingMetadata routing && routing.Model.IsAdHoc() ) + { + metadata.Remove( j ); + } + } + } + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static int ODataOrder() => + new ODataApiDescriptionProvider( + new StubModelMetadataProvider(), + new StubModelTypeBuilder(), + new OptionsFactory( [], [] ), + Opts.Create( + new ODataApiExplorerOptions( + new( new StubODataApiVersionCollectionProvider(), [] ) ) ) ).Order; + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) => + model.SetAnnotationValue( model, AdHocAnnotation.Instance ); + + private static ApiDescription[] FilterResults( + IList results, + IReadOnlyList conventions ) + { + var filtered = default( List ); + + for ( var i = 0; i < results.Count; i++ ) + { + var result = results[i]; + var metadata = result.ActionDescriptor.EndpointMetadata; + var odata = false; + + for ( var j = 0; j < metadata.Count; j++ ) + { + if ( metadata[j] is IODataRoutingMetadata ) + { + odata = true; + break; + } + } + + if ( odata || !result.IsODataLike() ) + { + continue; + } + + filtered ??= new( capacity: results.Count ); + filtered.Add( result ); + + for ( var j = 0; j < conventions.Count; j++ ) + { + conventions[j].ApplyTo( result ); + } + } + + return filtered?.ToArray() ?? []; + } + + private sealed class StubModelMetadataProvider : IModelMetadataProvider + { + public IEnumerable GetMetadataForProperties( Type modelType ) => + throw new NotImplementedException(); + + public ModelMetadata GetMetadataForType( Type modelType ) => + throw new NotImplementedException(); + } + + private sealed class StubModelTypeBuilder : IModelTypeBuilder + { + public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ) => + throw new NotImplementedException(); + + public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) => + throw new NotImplementedException(); + } + + private sealed class StubODataApiVersionCollectionProvider : IODataApiVersionCollectionProvider + { + public IReadOnlyList ApiVersions + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + } + + private static class ODataMetadata + { + private const string ArbitrarySegment = "52459ff8-bca1-4a26-b7f2-08c7da04472d"; + + // metadata (~/$metadata) and service (~/) doc have special handling. + // make sure we don't match the service doc + private static readonly ODataPathTemplate AdHocODataTemplate = + new( new DynamicSegmentTemplate( new( ArbitrarySegment ) ) ); + + public static ODataRoutingMetadata New( IEdmModel model ) => new( string.Empty, model, AdHocODataTemplate ); + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/SubstitutedModelMetadata.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/SubstitutedModelMetadata.cs index ec670cde..5ee9b575 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/SubstitutedModelMetadata.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/SubstitutedModelMetadata.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA1812 namespace Asp.Versioning.ApiExplorer; 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 36b8c467..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,9 +1,9 @@  - 7.0.0 - 7.0.0.0 - net7.0 + 8.2.0 + 8.2.0.0 + $(DefaultTargetFramework) Asp.Versioning 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. diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs new file mode 100644 index 00000000..6ca75048 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Conventions; + +using Microsoft.AspNetCore.Mvc.ApiExplorer; + +/// +/// Provides additional implementation specific to ASP.NET Core. +/// +[CLSCompliant( false )] +public partial class ImplicitModelBoundSettingsConvention +{ + /// + public void ApplyTo( ApiDescription apiDescription ) + { + ArgumentNullException.ThrowIfNull( apiDescription ); + + var responses = apiDescription.SupportedResponseTypes; + + for ( var j = 0; j < responses.Count; j++ ) + { + var response = responses[j]; + var notForSuccess = response.StatusCode < 200 || response.StatusCode >= 300; + + if ( notForSuccess ) + { + continue; + } + + var model = response.ModelMetadata; + var type = model == null + ? response.Type + : model.IsEnumerableType + ? model.ElementType + : model.UnderlyingOrModelType; + + if ( type != null ) + { + types.Add( type ); + } + } + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs index aef2fdbf..ae3ad97b 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs @@ -34,7 +34,7 @@ public partial class ODataQueryOptionDescriptionContext foreach ( var item in items ) { var model = item.Model; - var otherVersion = model.GetAnnotationValue( model )?.ApiVersion; + var otherVersion = model.GetApiVersion(); if ( version.Equals( otherVersion ) ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs index 9a91c9fa..3c876e07 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs @@ -4,6 +4,7 @@ namespace Asp.Versioning.Conventions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; +using System.Reflection; /// /// Provides additional implementation specific to Microsoft ASP.NET Core. @@ -11,28 +12,13 @@ namespace Asp.Versioning.Conventions; [CLSCompliant( false )] public partial class ODataQueryOptionsConventionBuilder { - private static Type GetController( ApiDescription apiDescription ) + private static TypeInfo GetController( ApiDescription apiDescription ) { if ( apiDescription.ActionDescriptor is ControllerActionDescriptor action ) { return action.ControllerTypeInfo; } - return typeof( object ); - } - - private static bool IsODataLike( ApiDescription description ) - { - var parameters = description.ActionDescriptor.Parameters; - - for ( var i = 0; i < parameters.Count; i++ ) - { - if ( parameters[i].ParameterType.IsODataQueryOptions() ) - { - return true; - } - } - - return false; + return typeof( object ).GetTypeInfo(); } } \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs index cef80319..bfab96ce 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs @@ -16,10 +16,7 @@ public partial class ODataValidationSettingsConvention /// public virtual void ApplyTo( ApiDescription apiDescription ) { - if ( apiDescription == null ) - { - throw new ArgumentNullException( nameof( apiDescription ) ); - } + ArgumentNullException.ThrowIfNull( apiDescription ); if ( !IsSupported( apiDescription.HttpMethod ) ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs index 27283ec7..9304fe5d 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -4,6 +4,7 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; using Asp.Versioning.ApiExplorer; +using Asp.Versioning.Conventions; using Asp.Versioning.OData; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -23,11 +24,7 @@ public static class IApiVersioningBuilderExtensions /// The original . public static IApiVersioningBuilder AddODataApiExplorer( this IApiVersioningBuilder builder ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); AddApiExplorerServices( builder ); return builder; } @@ -40,11 +37,7 @@ public static IApiVersioningBuilder AddODataApiExplorer( this IApiVersioningBuil /// The original . public static IApiVersioningBuilder AddODataApiExplorer( this IApiVersioningBuilder builder, Action setupAction ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); AddApiExplorerServices( builder ); builder.Services.Configure( setupAction ); return builder; @@ -55,21 +48,21 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) var services = builder.Services; builder.AddApiExplorer(); + builder.Services.AddModelConfigurationsAsServices(); services.TryAddSingleton(); - services.TryAddSingleton, ApiExplorerOptionsFactory>(); + services.TryAddSingleton, ODataApiExplorerOptionsFactory>(); + services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Transient() ); + services.TryAddEnumerable( Transient() ); services.Replace( Singleton, ODataApiExplorerOptionsAdapter>() ); } +#pragma warning disable IDE0079 #pragma warning disable CA1812 - private sealed class ODataApiExplorerOptionsAdapter : IOptionsFactory + private sealed class ODataApiExplorerOptionsAdapter( IOptionsFactory factory ) + : IOptionsFactory { - private readonly IOptionsFactory factory; - - public ODataApiExplorerOptionsAdapter( IOptionsFactory factory ) => - this.factory = factory; - public ApiExplorerOptions Create( string name ) => factory.Create( name ); } } \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Format.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Format.cs new file mode 100644 index 00000000..11126284 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Format.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Text; + +internal static class Format +{ + internal static readonly CompositeFormat UnsupportedQueryOption = CompositeFormat.Parse( ODataExpSR.UnsupportedQueryOption ); + internal static readonly CompositeFormat MaxExpressionDesc = CompositeFormat.Parse( ODataExpSR.MaxExpressionDesc ); + internal static readonly CompositeFormat AllowedPropertiesDesc = CompositeFormat.Parse( ODataExpSR.AllowedPropertiesDesc ); + internal static readonly CompositeFormat MaxDepthDesc = CompositeFormat.Parse( ODataExpSR.MaxDepthDesc ); + internal static readonly CompositeFormat MaxValueDesc = CompositeFormat.Parse( ODataExpSR.MaxValueDesc ); + internal static readonly CompositeFormat AllowedLogicalOperatorsDesc = CompositeFormat.Parse( ODataExpSR.AllowedLogicalOperatorsDesc ); + internal static readonly CompositeFormat AllowedArithmeticOperatorsDesc = CompositeFormat.Parse( ODataExpSR.AllowedArithmeticOperatorsDesc ); + internal static readonly CompositeFormat AmbiguousActionMethod = CompositeFormat.Parse( ODataExpSR.AmbiguousActionMethod ); + internal static readonly CompositeFormat InvalidActionMethodExpression = CompositeFormat.Parse( ODataExpSR.InvalidActionMethodExpression ); + internal static readonly CompositeFormat ActionMethodNotFound = CompositeFormat.Parse( ODataExpSR.ActionMethodNotFound ); + internal static readonly CompositeFormat AllowedFunctionsDesc = CompositeFormat.Parse( ODataExpSR.AllowedFunctionsDesc ); + internal static readonly CompositeFormat RequiredInterfaceNotImplemented = CompositeFormat.Parse( ODataExpSR.RequiredInterfaceNotImplemented ); + internal static readonly CompositeFormat ConventionStyleMismatch = CompositeFormat.Parse( ODataExpSR.ConventionStyleMismatch ); +} \ 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/ApplicationModels/ODataControllerSpecification.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/ApplicationModels/ODataControllerSpecification.cs index 2a778ee8..eb1cebd0 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/ApplicationModels/ODataControllerSpecification.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/ApplicationModels/ODataControllerSpecification.cs @@ -14,10 +14,7 @@ public sealed class ODataControllerSpecification : IApiControllerSpecification /// public bool IsSatisfiedBy( ControllerModel controller ) { - if ( controller == null ) - { - throw new ArgumentNullException( nameof( controller ) ); - } + ArgumentNullException.ThrowIfNull( controller ); if ( ODataControllerSpecification.IsSatisfiedBy( controller ) ) { 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 55e90b91..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 @@ -1,9 +1,9 @@  - 7.0.0 - 7.0.0.0 - net7.0 + 8.2.0 + 8.2.0.0 + $(DefaultTargetFramework) Asp.Versioning ASP.NET Core API Versioning with OData v4.0 A service API versioning library for Microsoft ASP.NET Core with OData v4.0. @@ -15,7 +15,7 @@ - + diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Builder/IApplicationBuilderExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/Builder/IApplicationBuilderExtensions.cs index 0ce54373..c6248429 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Builder/IApplicationBuilderExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Builder/IApplicationBuilderExtensions.cs @@ -17,11 +17,7 @@ public static class IApplicationBuilderExtensions /// The original . public static IApplicationBuilder UseVersionedODataBatching( this IApplicationBuilder app ) { - if ( app == null ) - { - throw new ArgumentNullException( nameof( app ) ); - } - + ArgumentNullException.ThrowIfNull( app ); return app.UseMiddleware(); } } \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Controllers/VersionedMetadataController.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/Controllers/VersionedMetadataController.cs index e29e2ede..1f4b0266 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Controllers/VersionedMetadataController.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Controllers/VersionedMetadataController.cs @@ -4,7 +4,6 @@ namespace Asp.Versioning.Controllers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Routing.Controllers; -using Microsoft.Extensions.Primitives; using static Microsoft.OData.ODataConstants; using static Microsoft.OData.ODataUtils; using static Microsoft.OData.ODataVersion; @@ -17,6 +16,8 @@ namespace Asp.Versioning.Controllers; [ControllerName( "OData" )] public class VersionedMetadataController : MetadataController { + private static readonly string[] values = ["GET", "OPTIONS"]; + /// /// Handles a request for the HTTP OPTIONS method. /// @@ -55,8 +56,8 @@ public virtual IActionResult GetOptions() { var headers = Response.Headers; - headers.Add( "Allow", new StringValues( new[] { "GET", "OPTIONS" } ) ); - headers.Add( ODataVersionHeader, ODataVersionToString( V4 ) ); + headers.Allow = new( values ); + headers[ODataVersionHeader] = ODataVersionToString( V4 ); return Ok(); } diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs index 3af0b775..8d90b695 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -9,7 +9,6 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.AspNetCore.Routing; @@ -32,11 +31,7 @@ public static class IApiVersioningBuilderExtensions /// The original . public static IApiVersioningBuilder AddOData( this IApiVersioningBuilder builder ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); AddServices( builder.AddMvc().Services ); return builder; } @@ -50,10 +45,7 @@ public static IApiVersioningBuilder AddOData( this IApiVersioningBuilder builder [CLSCompliant( false )] public static IApiVersioningBuilder AddOData( this IApiVersioningBuilder builder, Action setupAction ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } + ArgumentNullException.ThrowIfNull( builder ); var services = builder.AddMvc().Services; AddServices( services ); @@ -66,8 +58,7 @@ private static void AddServices( IServiceCollection services ) services.TryRemoveODataService( typeof( IApplicationModelProvider ), ODataRoutingApplicationModelProviderType ); var partManager = services.GetOrCreateApplicationPartManager(); - - ConfigureDefaultFeatureProviders( partManager ); + var configured = partManager.ConfigureDefaultFeatureProviders(); services.AddHttpContextAccessor(); services.TryAddSingleton(); @@ -84,24 +75,11 @@ private static void AddServices( IServiceCollection services ) services.TryAddEnumerable( Singleton() ); services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Transient() ); - services.AddModelConfigurationsAsServices( partManager ); - } - private static T GetService( this IServiceCollection services ) => - (T) services.LastOrDefault( d => d.ServiceType == typeof( T ) )?.ImplementationInstance!; - - private static ApplicationPartManager GetOrCreateApplicationPartManager( this IServiceCollection services ) - { - var partManager = services.GetService(); - - if ( partManager == null ) + if ( configured ) { - partManager = new ApplicationPartManager(); - services.TryAddSingleton( partManager ); + services.AddModelConfigurationsAsServices( partManager ); } - - partManager.ApplicationParts.Add( new AssemblyPart( typeof( ODataApiVersioningOptions ).Assembly ) ); - return partManager; } [MethodImpl( MethodImplOptions.AggressiveInlining )] @@ -124,7 +102,13 @@ private static void TryRemoveODataService( this IServiceCollection services, Typ } } - var message = string.Format( CultureInfo.CurrentCulture, SR.UnableToFindServices, nameof( IMvcBuilder ), "AddOData", "ConfigureServices(...)" ); + var message = string.Format( + CultureInfo.CurrentCulture, + Format.UnableToFindServices, + nameof( IMvcBuilder ), + "AddOData", + "ConfigureServices(...)" ); + throw new InvalidOperationException( message ); } @@ -148,27 +132,6 @@ private static void TryReplaceODataService( } } - private static void AddModelConfigurationsAsServices( this IServiceCollection services, ApplicationPartManager partManager ) - { - var feature = new ModelConfigurationFeature(); - var modelConfigurationType = typeof( IModelConfiguration ); - - partManager.PopulateFeature( feature ); - - foreach ( var modelConfiguration in feature.ModelConfigurations ) - { - services.TryAddEnumerable( Transient( modelConfigurationType, modelConfiguration ) ); - } - } - - private static void ConfigureDefaultFeatureProviders( ApplicationPartManager partManager ) - { - if ( !partManager.FeatureProviders.OfType().Any() ) - { - partManager.FeatureProviders.Add( new ModelConfigurationFeatureProvider() ); - } - } - private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor ) { if ( descriptor.ImplementationInstance != null ) @@ -198,12 +161,8 @@ IHttpContextFactory NewFactory( IServiceProvider serviceProvider ) return Describe( typeof( IHttpContextFactory ), NewFactory, lifetime ); } - private sealed class HttpContextFactoryDecorator : IHttpContextFactory + private sealed class HttpContextFactoryDecorator( IHttpContextFactory decorated ) : IHttpContextFactory { - private readonly IHttpContextFactory decorated; - - public HttpContextFactoryDecorator( IHttpContextFactory decorated ) => this.decorated = decorated; - public HttpContext Create( IFeatureCollection featureCollection ) { // features do not support cloning or DI, which is precisely why ASP.NET Core no longer supports diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs new file mode 100644 index 00000000..16432665 --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs @@ -0,0 +1,73 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Microsoft.Extensions.DependencyInjection; + +using Asp.Versioning.OData; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Runtime.CompilerServices; +using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; + +/// +/// Provides extension methods for . +/// +public static class IServiceCollectionExtensions +{ + [MethodImpl( MethodImplOptions.AggressiveInlining )] + internal static T GetService( this IServiceCollection services ) => + (T) services.LastOrDefault( d => d.ServiceType == typeof( T ) )?.ImplementationInstance!; + + internal static ApplicationPartManager GetOrCreateApplicationPartManager( this IServiceCollection services ) + { + var partManager = services.GetService(); + + if ( partManager == null ) + { + partManager = new ApplicationPartManager(); + services.TryAddSingleton( partManager ); + } + + partManager.ApplicationParts.Add( new AssemblyPart( typeof( ODataApiVersioningOptions ).Assembly ) ); + return partManager; + } + + internal static void AddModelConfigurationsAsServices( this IServiceCollection services, ApplicationPartManager partManager ) + { + var feature = new ModelConfigurationFeature(); + var modelConfigurationType = typeof( IModelConfiguration ); + + partManager.PopulateFeature( feature ); + + foreach ( var modelConfiguration in feature.ModelConfigurations ) + { + services.TryAddEnumerable( Transient( modelConfigurationType, modelConfiguration ) ); + } + } + + internal static bool ConfigureDefaultFeatureProviders( this ApplicationPartManager partManager ) + { + if ( partManager.FeatureProviders.OfType().Any() ) + { + return false; + } + + partManager.FeatureProviders.Add( new ModelConfigurationFeatureProvider() ); + return true; + } + + /// + /// Registers discovered model configurations as services in the . + /// + /// The extended . + public static void AddModelConfigurationsAsServices( this IServiceCollection services ) + { + ArgumentNullException.ThrowIfNull( services ); + + var partManager = services.GetOrCreateApplicationPartManager(); + + if ( ConfigureDefaultFeatureProviders( partManager ) ) + { + services.AddModelConfigurationsAsServices( partManager ); + } + } +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Format.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/Format.cs new file mode 100644 index 00000000..c4202f8c --- /dev/null +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Format.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Text; + +internal static class Format +{ + internal static readonly CompositeFormat UnableToFindServices = CompositeFormat.Parse( SR.UnableToFindServices ); +} \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/ODataBatchPathMapping.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/ODataBatchPathMapping.cs index dede3b74..8cf589be 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/ODataBatchPathMapping.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/ODataBatchPathMapping.cs @@ -26,7 +26,7 @@ public void Add( string prefixName, string routeTemplate, ODataBatchHandler hand Debug.Assert( count < mappings.Length, "The capacity has been exceeded." ); var template = TemplateParser.Parse( routeTemplate.TrimStart( '/' ) ); - var matcher = new TemplateMatcher( template, new() ); + var matcher = new TemplateMatcher( template, [] ); handler.PrefixName = prefixName; mappings[count++] = (matcher, handler, version); @@ -40,11 +40,56 @@ public bool TryGetHandler( HttpContext context, [NotNullWhen( true )] out ODataB return false; } + var routeData = new RouteValueDictionary(); + var candidates = new Dictionary( capacity: mappings.Length ); + + batchHandler = SelectExactMatch( context, routeData, candidates ) ?? + SelectBestCandidate( context, candidates, routeData ); + + return batchHandler is not null; + } + + public ValueTask TryGetHandlerAsync( HttpContext context, CancellationToken cancellationToken ) + { + if ( count == 0 ) + { + return ValueTask.FromResult( default( ODataBatchHandler ) ); + } + + var routeData = new RouteValueDictionary(); + var candidates = new Dictionary( capacity: mappings.Length ); + + if ( SelectExactMatch( context, routeData, candidates ) is var handler ) + { + return ValueTask.FromResult( handler ); + } + + return SelectBestCandidateAsync( context, candidates, routeData, cancellationToken ); + } + + private static void MergeRouteData( HttpContext context, RouteValueDictionary routeData ) + { + if ( routeData.Count == 0 ) + { + return; + } + + var batchRouteData = context.ODataFeature().BatchRouteData; + + foreach ( var (key, value) in routeData ) + { + batchRouteData.Add( key, value ); + } + } + + private ODataBatchHandler? SelectExactMatch( + HttpContext context, + RouteValueDictionary routeData, + Dictionary candidates ) + { var path = context.Request.Path; var feature = context.ApiVersioningFeature(); var unspecified = feature.RawRequestedApiVersions.Count == 0; - var routeData = new RouteValueDictionary(); - var candidates = new Dictionary( capacity: mappings.Length ); for ( var i = 0; i < count; i++ ) { @@ -73,33 +118,40 @@ public bool TryGetHandler( HttpContext context, [NotNullWhen( true )] out ODataB } MergeRouteData( context, routeData ); - batchHandler = handler; - return true; + return handler; } - batchHandler = SelectBestCandidate( context, ref path, candidates, routeData ); - return batchHandler is not null; + return default; } - private static void MergeRouteData( HttpContext context, RouteValueDictionary routeData ) + private ODataBatchHandler? SelectBestCandidate( + HttpContext context, + Dictionary candidates, + RouteValueDictionary routeData, + ApiVersion version ) { - if ( routeData.Count == 0 ) + if ( version is null || !candidates.TryGetValue( version, out var index ) ) { - return; + return default; } - var batchRouteData = context.ODataFeature().BatchRouteData; + ref readonly var mapping = ref mappings[index]; + var (matcher, handler, _) = mapping; - foreach ( var (key, value) in routeData ) - { - batchRouteData.Add( key, value ); - } + routeData.Clear(); + matcher.TryMatch( context.Request.Path, routeData ); + MergeRouteData( context, routeData ); + + // it's important that the resolved api version be set here to ensure the correct + // ODataOptions are resolved by ODataBatchHandler when executed + context.ApiVersioningFeature().RequestedApiVersion = version; + + return handler; } private ODataBatchHandler? SelectBestCandidate( HttpContext context, - ref PathString path, - IReadOnlyDictionary candidates, + Dictionary candidates, RouteValueDictionary routeData ) { if ( candidates.Count == 0 ) @@ -114,22 +166,27 @@ private static void MergeRouteData( HttpContext context, RouteValueDictionary ro var model = new ApiVersionModel( candidates.Keys, Enumerable.Empty() ); var version = selector.SelectVersion( context.Request, model ); - if ( version is null || !candidates.TryGetValue( version, out var index ) ) + return SelectBestCandidate( context, candidates, routeData, version ); + } + + private async ValueTask SelectBestCandidateAsync( + HttpContext context, + Dictionary candidates, + RouteValueDictionary routeData, + CancellationToken cancellationToken ) + { + if ( candidates.Count == 0 ) { return default; } - ref readonly var mapping = ref mappings[index]; - var (matcher, handler, _) = mapping; - - routeData.Clear(); - matcher.TryMatch( path, routeData ); - MergeRouteData( context, routeData ); - - // it's important that the resolved api version be set here to ensure the correct - // ODataOptions are resolved by ODataBatchHandler when executed - context.ApiVersioningFeature().RequestedApiVersion = version; + // ~/$batch is always version-neutral so there is no need to check + // ApiVersioningOptions.AllowDefaultVersionWhenUnspecified. use the + // configured IApiVersionSelector to provide a chance to select the + // most appropriate version. + var model = new ApiVersionModel( candidates.Keys, Enumerable.Empty() ); + var version = await selector.SelectVersionAsync( context.Request, model, cancellationToken ).ConfigureAwait( false ); - return handler; + return SelectBestCandidate( context, candidates, routeData, version ); } } \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/VersionedODataBatchMiddleware.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/VersionedODataBatchMiddleware.cs index 1eff0220..df89490d 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/VersionedODataBatchMiddleware.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/Batch/VersionedODataBatchMiddleware.cs @@ -31,10 +31,7 @@ public VersionedODataBatchMiddleware( RequestDelegate next, VersionedODataOption /// A task representing the asynchronous operation. public Task Invoke( HttpContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); if ( HttpMethods.IsPost( context.Request.Method ) && options.TryGetBatchHandler( context, out var handler ) ) diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeature.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeature.cs index 95a2cdc7..5ab6b579 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeature.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeature.cs @@ -21,5 +21,5 @@ public class ModelConfigurationFeature /// Gets the collection of model configurations in an application. /// /// The collection of model configurations in an application. - public ICollection ModelConfigurations => modelConfigurations ??= new(); + public ICollection ModelConfigurations => modelConfigurations ??= []; } \ No newline at end of file diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeatureProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeatureProvider.cs index 7f6254b3..66cf48e9 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeatureProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ModelConfigurationFeatureProvider.cs @@ -15,15 +15,8 @@ public class ModelConfigurationFeatureProvider : IApplicationFeatureProvider public void PopulateFeature( IEnumerable parts, ModelConfigurationFeature feature ) { - if ( parts == null ) - { - throw new ArgumentNullException( nameof( parts ) ); - } - - if ( feature == null ) - { - throw new ArgumentNullException( nameof( feature ) ); - } + ArgumentNullException.ThrowIfNull( parts ); + ArgumentNullException.ThrowIfNull( feature ); var types = from part in parts.OfType() from type in part.Types @@ -43,10 +36,7 @@ where IsModelConfiguration( type ) /// True if the type is a model configuration; otherwise false. protected virtual bool IsModelConfiguration( Type type ) { - if ( type == null ) - { - throw new ArgumentNullException( nameof( type ) ); - } + ArgumentNullException.ThrowIfNull( type ); if ( !type.IsClass || type.IsAbstract || !type.IsPublic || type.ContainsGenericParameters ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersionCollectionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersionCollectionProvider.cs index 76cada9f..0aa16562 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersionCollectionProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersionCollectionProvider.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA1812 namespace Asp.Versioning.OData; 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 0d9cdcc0..9ef71b7b 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs @@ -62,10 +62,7 @@ public virtual void OnProvidersExecuted( ApplicationModelProviderContext context /// public virtual void OnProvidersExecuting( ApplicationModelProviderContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var (metadataControllers, supported, deprecated) = CollateApiVersions( context.Result ); @@ -92,7 +89,7 @@ private static if ( controller.ControllerType.IsMetadataController() ) { - metadataControllers ??= new(); + metadataControllers ??= []; metadataControllers.Add( controller ); continue; } @@ -119,7 +116,7 @@ private static if ( supported == null && versions.Count > 0 ) { - supported = new(); + supported = []; } for ( var k = 0; k < versions.Count; k++ ) @@ -131,7 +128,7 @@ private static if ( deprecated == null && versions.Count > 0 ) { - deprecated = new(); + deprecated = []; } for ( var k = 0; k < versions.Count; k++ ) @@ -144,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 @@ -223,7 +220,7 @@ private void ApplyMetadataControllerConventions( builder.ApplyTo( metadataController ); } - private IReadOnlyList MergeApiVersions( + private ApiVersion[] MergeApiVersions( SortedSet? supported, SortedSet? deprecated ) { @@ -231,14 +228,14 @@ private IReadOnlyList MergeApiVersions( { if ( supported == null ) { - return new[] { Options.DefaultApiVersion }; + return [Options.DefaultApiVersion]; } - return supported.ToArray(); + return [.. supported]; } else if ( supported == null ) { - return deprecated.ToArray(); + return [.. deprecated]; } return supported.Union( deprecated ).ToArray(); diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs index c8fc3fe0..cc798280 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs @@ -1,10 +1,10 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA1812 namespace Asp.Versioning.OData; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Routing.Conventions; @@ -153,7 +153,11 @@ private void AddRouteComponents( for ( var i = 0; i < models.Count; i++ ) { var model = models[i]; - var version = model.GetAnnotationValue( model ).ApiVersion; + + if ( model.GetApiVersion() is not ApiVersion version ) + { + continue; + } if ( !mappings.TryGetValue( version, out var options ) ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataOptionsPostSetup.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataOptionsPostSetup.cs index a8e4f1ff..dfe7314f 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataOptionsPostSetup.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataOptionsPostSetup.cs @@ -35,10 +35,7 @@ public ODataOptionsPostSetup( /// public void PostConfigure( string? name, ODataOptions options ) { - if ( options == null ) - { - throw new ArgumentNullException( nameof( options ) ); - } + ArgumentNullException.ThrowIfNull( options ); var conventions = options.Conventions; var replacements = 0; diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataModelBuilder.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataModelBuilder.cs index 8f560845..9b576129 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataModelBuilder.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataModelBuilder.cs @@ -20,12 +20,10 @@ public VersionedODataModelBuilder( IODataApiVersionCollectionProvider apiVersionCollectionProvider, IEnumerable modelConfigurations ) { - if ( modelConfigurations == null ) - { - throw new ArgumentNullException( nameof( modelConfigurations ) ); - } + ArgumentNullException.ThrowIfNull( apiVersionCollectionProvider ); + ArgumentNullException.ThrowIfNull( modelConfigurations ); - this.apiVersionCollectionProvider = apiVersionCollectionProvider ?? throw new ArgumentNullException( nameof( apiVersionCollectionProvider ) ); + this.apiVersionCollectionProvider = apiVersionCollectionProvider; foreach ( var configuration in modelConfigurations ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataOptions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataOptions.cs index 7319d4a7..5c08429e 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataOptions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataOptions.cs @@ -85,12 +85,11 @@ public IReadOnlyDictionary Mapping /// The current HTTP context. /// The retrieved OData batch handler or null. /// True if the was successfully retrieved; otherwise, false. + /// Prefer the asynchronous version of this method + /// . public virtual bool TryGetBatchHandler( HttpContext context, [NotNullWhen( true )] out ODataBatchHandler? handler ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); if ( batchMapping is null ) { @@ -101,12 +100,33 @@ public virtual bool TryGetBatchHandler( HttpContext context, [NotNullWhen( true return batchMapping.TryGetHandler( context, out handler ); } + /// + /// Attempts to retrieve the configured batch handler for the current context. + /// + /// The current HTTP context. + /// The token that can be used to cancel the operation. + /// A task containing the matched + /// or null if the no match was found. + public virtual ValueTask TryGetBatchHandlerAsync( HttpContext context, CancellationToken cancellationToken ) + { + ArgumentNullException.ThrowIfNull( context ); + + if ( batchMapping is null ) + { + return ValueTask.FromResult( default( ODataBatchHandler? ) ); + } + + return batchMapping.TryGetHandlerAsync( context, cancellationToken ); + } + /// /// Attempts to get the current OData options. /// /// The current HTTP context. /// The resolved OData options or null. /// True if the current OData were successfully resolved; otherwise, false. + /// Prefer the asynchronous version of this method + /// . public virtual bool TryGetValue( HttpContext? context, [NotNullWhen( true )] out ODataOptions? options ) { if ( context == null || mapping == null || mapping.Count == 0 ) @@ -132,6 +152,36 @@ public virtual bool TryGetValue( HttpContext? context, [NotNullWhen( true )] out return mapping.TryGetValue( apiVersion, out options ); } + /// + /// Attempts to get the current OData options. + /// + /// The current HTTP context. + /// The token that can be used to cancel the operation. + /// A task containing the matched + /// or null if the no match was found. + public virtual async ValueTask TryGetValueAsync( HttpContext? context, CancellationToken cancellationToken ) + { + if ( context == null || mapping == null || mapping.Count == 0 ) + { + return default; + } + + var apiVersion = context.GetRequestedApiVersion(); + + if ( apiVersion == null ) + { + var model = new ApiVersionModel( mapping.Keys, Array.Empty() ); + apiVersion = await ApiVersionSelector.SelectVersionAsync( context.Request, model, cancellationToken ).ConfigureAwait( false ); + + if ( apiVersion == null ) + { + return default; + } + } + + return mapping.TryGetValue( apiVersion, out var options ) ? options : default; + } + /// /// Attempts to resolve the current OData options. /// diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs index 84ef5d76..93d92b9c 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs @@ -6,6 +6,7 @@ namespace Asp.Versioning.OData; using Microsoft.AspNetCore.OData.Routing.Template; using Microsoft.OData.Edm; using Microsoft.OData.UriParser; +using System.Runtime.CompilerServices; /// /// Represents a versioned OData template translator. @@ -16,23 +17,14 @@ public sealed class VersionedODataTemplateTranslator : IODataTemplateTranslator /// public ODataPath? Translate( ODataPathTemplate path, ODataTemplateTranslateContext context ) { - if ( path == null ) - { - throw new ArgumentNullException( nameof( path ) ); - } - - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( path ); + ArgumentNullException.ThrowIfNull( context ); var apiVersion = context.HttpContext.GetRequestedApiVersion(); if ( apiVersion == null ) { - var metadata = context.Endpoint.Metadata.GetMetadata(); - - if ( metadata == null || !metadata.IsApiVersionNeutral ) + if ( !IsVersionNeutral( context ) ) { return default; } @@ -40,9 +32,15 @@ public sealed class VersionedODataTemplateTranslator : IODataTemplateTranslator else { var model = context.Model; - var otherApiVersion = model.GetAnnotationValue( model )?.ApiVersion; + var otherApiVersion = model.GetApiVersion(); - if ( !apiVersion.Equals( otherApiVersion ) ) + // HACK: a version-neutral endpoint can fail to match here because odata tries to match the + // first endpoint metadata when there could be multiple. such an endpoint is expected to be + // the same in all versions so allow it to flow through. revisit if/when odata fixes this. + // + // REF: https://github.com/OData/AspNetCoreOData/issues/753 + // REF: https://github.com/OData/AspNetCoreOData/blob/main/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMatcherPolicy.cs#L86 + if ( !apiVersion.Equals( otherApiVersion ) && !IsVersionNeutral( context ) ) { return default; } @@ -58,4 +56,9 @@ public sealed class VersionedODataTemplateTranslator : IODataTemplateTranslator return new( context.Segments ); } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool IsVersionNeutral( ODataTemplateTranslateContext context ) => + context.Endpoint.Metadata.GetMetadata() is ApiVersionMetadata metadata + && metadata.IsApiVersionNeutral; } \ No newline at end of file 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 diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/DefaultMetadataMatcherPolicy.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/DefaultMetadataMatcherPolicy.cs index bc58fc89..a4b7bfd3 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/DefaultMetadataMatcherPolicy.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/DefaultMetadataMatcherPolicy.cs @@ -3,6 +3,7 @@ namespace Asp.Versioning.Routing; using Asp.Versioning; +using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.OData.Routing.Template; @@ -35,11 +36,7 @@ public DefaultMetadataMatcherPolicy( IApiVersionParameterSource parameterSource, IOptions options ) { - if ( parameterSource == null ) - { - throw new ArgumentNullException( nameof( parameterSource ) ); - } - + ArgumentNullException.ThrowIfNull( parameterSource ); versionsByUrl = parameterSource.VersionsByUrl(); this.options = options; } @@ -50,10 +47,7 @@ public DefaultMetadataMatcherPolicy( /// public virtual bool AppliesToEndpoints( IReadOnlyList endpoints ) { - if ( endpoints == null ) - { - throw new ArgumentNullException( nameof( endpoints ) ); - } + ArgumentNullException.ThrowIfNull( endpoints ); for ( var i = 0; i < endpoints.Count; i++ ) { @@ -69,10 +63,7 @@ public virtual bool AppliesToEndpoints( IReadOnlyList endpoints ) /// public IReadOnlyList GetEdges( IReadOnlyList endpoints ) { - if ( endpoints == null ) - { - throw new ArgumentNullException( nameof( endpoints ) ); - } + ArgumentNullException.ThrowIfNull( endpoints ); var edges = default( List ); var lowestApiVersion = default( ApiVersion ); @@ -88,7 +79,7 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints continue; } - edges ??= new(); + edges ??= []; edges.Add( endpoint ); var model = endpoint.Metadata.GetMetadata()!.Map( Explicit | Implicit ); @@ -130,17 +121,14 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints return Array.Empty(); } - var state = (lowestApiVersion, routePatterns?.ToArray() ?? Array.Empty()); + var state = (lowestApiVersion, routePatterns?.ToArray() ?? []); return new PolicyNodeEdge[] { new( state, edges ) }; } /// public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList edges ) { - if ( edges == null ) - { - throw new ArgumentNullException( nameof( edges ) ); - } + ArgumentNullException.ThrowIfNull( edges ); Debug.Assert( edges.Count == 1, $"Only a single edge was expected, but {edges.Count} edges were provided" ); @@ -159,6 +147,7 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList new ApiVersionMatcherPolicy( ApiVersionParser.Default, + Enumerable.Empty(), Options.Create( new ApiVersioningOptions() ), new NullLogger() ).Order; diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs index 5dd11990..230017ed 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedAttributeRoutingConvention.cs @@ -3,7 +3,6 @@ namespace Asp.Versioning.Routing; using Asp.Versioning.ApplicationModels; -using Asp.Versioning.OData; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.OData.Routing.Conventions; using Microsoft.AspNetCore.OData.Routing.Parser; @@ -29,10 +28,7 @@ public VersionedAttributeRoutingConvention( /// public override bool AppliesToAction( ODataControllerActionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var metadata = context.Action .Selectors @@ -55,7 +51,7 @@ public override bool AppliesToAction( ODataControllerActionContext context ) return false; } - var apiVersion = edm.GetAnnotationValue( edm )?.ApiVersion; + var apiVersion = edm.GetApiVersion(); if ( apiVersion == null || !metadata.IsMappedTo( apiVersion ) ) { diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedMetadataRoutingConvention.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedMetadataRoutingConvention.cs index 77ff8f71..c506e50f 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedMetadataRoutingConvention.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/VersionedMetadataRoutingConvention.cs @@ -19,11 +19,7 @@ public class VersionedMetadataRoutingConvention : MetadataRoutingConvention /// public override bool AppliesToController( ODataControllerActionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } - + ArgumentNullException.ThrowIfNull( context ); metadataController ??= typeof( VersionedMetadataController ); return metadataController.IsAssignableFrom( context.Controller.ControllerType ); } @@ -31,10 +27,7 @@ public override bool AppliesToController( ODataControllerActionContext context ) /// public override bool AppliesToAction( ODataControllerActionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var action = context.Action; var actionName = action.ActionMethod.Name; diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs index 3c3f3136..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 @@ -9,6 +9,7 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.OData; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Buffers; public class ODataApiDescriptionProviderTest { @@ -152,6 +153,8 @@ private void AssertVersion1( ApiDescriptionGroup group ) new { HttpMethod = "GET", GroupName, RelativePath = "api/People/{key}" }, }, options => options.ExcludingMissingMembers() ); + + AssertQueryOptionWithoutOData( items[0], "filter", "author", "published" ); } private void AssertVersion2( ApiDescriptionGroup group ) @@ -229,9 +232,33 @@ private void AssertVersion3( ApiDescriptionGroup group ) items.Should().BeEquivalentTo( expected, options => options.ExcludingMissingMembers() ); } - private void PrintGroup( IReadOnlyList items ) + private static void AssertQueryOptionWithoutOData( ApiDescription description, string name, string property, params string[] otherProperties ) + { + var parameter = description.ParameterDescriptions.Single( p => p.Name == name ); + var count = otherProperties.Length + 1; + string suffix; + + if ( count == 1 ) + { + suffix = property; + } + else + { + var pool = ArrayPool.Shared; + var properties = pool.Rent( count ); + + properties[0] = property; + Array.Copy( otherProperties, 0, properties, 1, count - 1 ); + + suffix = string.Join( ", ", properties, 0, count ); + } + + parameter.ModelMetadata.Description.Should().EndWith( suffix + '.' ); + } + + private void PrintGroup( 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/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Asp.Versioning.OData.ApiExplorer.Tests.csproj b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Asp.Versioning.OData.ApiExplorer.Tests.csproj index afcd5741..00feabbc 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Asp.Versioning.OData.ApiExplorer.Tests.csproj +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Asp.Versioning.OData.ApiExplorer.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + $(DefaultTargetFramework) Asp.Versioning diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs index e5cd95ce..fd3717ba 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Conventions/ODataValidationSettingsConventionTest.cs @@ -1,5 +1,10 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dlike +//// Ignore Spelling: Multipart +//// Ignore Spelling: nonaction +//// Ignore Spelling: nonquery + namespace Asp.Versioning.Conventions; using Asp.Versioning.OData; @@ -19,6 +24,7 @@ namespace Asp.Versioning.Conventions; using Microsoft.OData.ModelBuilder; using Microsoft.OData.ModelBuilder.Config; using System.Reflection; +using Xunit; using static Microsoft.AspNetCore.Http.StatusCodes; using static Microsoft.AspNetCore.Mvc.ModelBinding.BindingSource; using static Microsoft.AspNetCore.OData.Query.AllowedArithmeticOperators; @@ -469,6 +475,50 @@ public void apply_to_should_use_model_bound_query_attributes() options => options.ExcludingMissingMembers() ); } + [Fact] + public void apply_should_override_model_bound_settings_with_enable_query_attribute() + { + // arrange + var builder = new ODataConventionModelBuilder().EnableLowerCamelCase(); + + builder.EntitySet( "Customers" ); + + var validationSettings = new ODataValidationSettings() + { + AllowedQueryOptions = AllowedQueryOptions.None, + AllowedArithmeticOperators = AllowedArithmeticOperators.None, + AllowedLogicalOperators = AllowedLogicalOperators.None, + AllowedFunctions = AllowedFunctions.None, + }; + var settings = new TestODataQueryOptionSettings( typeof( Customer ) ); + var convention = new ODataValidationSettingsConvention( validationSettings, settings ); + var model = builder.GetEdmModel(); + var description = NewApiDescription( typeof( CustomersController ), typeof( IEnumerable ), model ); + + // act + convention.ApplyTo( description ); + + // assert + var parameter = description.ParameterDescriptions.Single(); + + parameter.Should().BeEquivalentTo( + new + { + Name = "$filter", + Source = Query, + Type = typeof( string ), + DefaultValue = default( object ), + IsRequired = false, + ModelMetadata = new { Description = "Test" }, + ParameterDescriptor = new + { + Name = "$filter", + ParameterType = typeof( string ), + }, + }, + options => options.ExcludingMissingMembers() ); + } + [Fact] public void apply_to_should_process_odataX2Dlike_api_description() { @@ -589,7 +639,7 @@ public static IEnumerable EnableQueryAttributeData MethodInfo = typeof( ControllerBase ).GetRuntimeMethod( nameof( ControllerBase.Ok ), Type.EmptyTypes ), EndpointMetadata = new object[] { - new ODataRoutingMetadata( string.Empty, model, new() ), + new ODataRoutingMetadata( string.Empty, model, [] ), }, }, HttpMethod = method, @@ -620,7 +670,7 @@ private static ApiDescription NewApiDescription( Type controllerType, Type respo MethodInfo = controllerType.GetRuntimeMethods().Single( m => m.Name == "Get" ), EndpointMetadata = new object[] { - new ODataRoutingMetadata( string.Empty, model, new() ), + new ODataRoutingMetadata( string.Empty, model, [] ), }, }, HttpMethod = "GET", @@ -678,6 +728,13 @@ public class OrdersController : ODataController public IActionResult Get( ODataQueryOptions options ) => Ok(); } + public class CustomersController : ODataController + { + [EnableQuery( AllowedQueryOptions = Filter )] + [ProducesResponseType( typeof( IEnumerable ), Status200OK )] + public IActionResult Get( ODataQueryOptions options ) => Ok(); + } + [Select] [Filter] [Count] @@ -693,6 +750,12 @@ public class Order public int Quantity { get; set; } } + [Page( MaxTop = 25, PageSize = 25 )] + public class Customer + { + public int CustomerId { get; set; } + } + private sealed class TestODataQueryOptionSettings : ODataQueryOptionSettings { internal TestODataQueryOptionSettings( Type type, bool dollarPrefix = true ) : diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/AllConfigurations.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/AllConfigurations.cs index d29fe190..a70422b7 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/AllConfigurations.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/AllConfigurations.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.Simulators.Configuration; using Asp.Versioning.OData; @@ -15,6 +13,8 @@ public class AllConfigurations : IModelConfiguration /// public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + builder.Function( "GetSalesTaxRate" ).Returns().Parameter( "PostalCode" ); } } \ No newline at end of file diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs index bfbe387d..573b67a0 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/OrderModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.Simulators.Configuration; using Asp.Versioning.OData; @@ -16,6 +14,8 @@ public class OrderModelConfiguration : IModelConfiguration /// public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + var order = builder.EntitySet( "Orders" ).EntityType.HasKey( o => o.Id ); if ( apiVersion < ApiVersions.V2 ) diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs index bd869bff..662bf0ba 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/PersonModelConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.Simulators.Configuration; using Asp.Versioning.OData; @@ -16,6 +14,8 @@ public class PersonModelConfiguration : IModelConfiguration /// public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + var person = builder.EntitySet( "People" ).EntityType.HasKey( p => p.Id ); if ( apiVersion < ApiVersions.V3 ) diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/ProductConfiguration.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/ProductConfiguration.cs index b210dbdb..eb71daf6 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/ProductConfiguration.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/ProductConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.Simulators.Configuration; using Asp.Versioning.OData; @@ -16,6 +14,8 @@ public class ProductConfiguration : IModelConfiguration /// public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + if ( apiVersion < ApiVersions.V3 ) { return; diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/SupplierConfiguration.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/SupplierConfiguration.cs index 7ce101b7..9bcd5bbc 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/SupplierConfiguration.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Configuration/SupplierConfiguration.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA1062 // Validate arguments of public methods - namespace Asp.Versioning.Simulators.Configuration; using Asp.Versioning.OData; @@ -16,6 +14,8 @@ public class SupplierConfiguration : IModelConfiguration /// public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { + ArgumentNullException.ThrowIfNull( builder ); + if ( apiVersion < ApiVersions.V3 ) { return; diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs index 00fec7f1..97610da6 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Book.cs @@ -2,9 +2,12 @@ namespace Asp.Versioning.Simulators.Models; +using Microsoft.OData.ModelBuilder; + /// /// Represents a book. /// +[Filter( "author", "published" )] public class Book { /// diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs index 83a08d66..7b503c6b 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs @@ -1,7 +1,5 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable CA2227 // Collection properties should be read only - namespace Asp.Versioning.Simulators.Models; public class Supplier @@ -10,5 +8,7 @@ public class Supplier public string Name { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only public ICollection Products { get; set; } +#pragma warning restore CA2227 // Collection properties should be read only } \ No newline at end of file diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs index 438a0099..d6b7e96a 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/V1/BooksController.cs @@ -16,15 +16,15 @@ namespace Asp.Versioning.Simulators.V1; [Route( "api/[controller]" )] public class BooksController : ControllerBase { - private static readonly Book[] books = new Book[] - { + private static readonly Book[] books = + [ new() { Id = "9781847490599", Title = "Anna Karenina", Author = "Leo Tolstoy", Published = 1878 }, new() { Id = "9780198800545", Title = "War and Peace", Author = "Leo Tolstoy", Published = 1869 }, new() { Id = "9780684801520", Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", Published = 1925 }, new() { Id = "9780486280615", Title = "The Adventures of Huckleberry Finn", Author = "Mark Twain", Published = 1884 }, new() { Id = "9780140430820", Title = "Moby Dick", Author = "Herman Melville", Published = 1851 }, new() { Id = "9780060934347", Title = "Don Quixote", Author = "Miguel de Cervantes", Published = 1605 }, - }; + ]; /// /// Gets all books. diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/ApplicationModels/ODataControllerSpecificationTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/ApplicationModels/ODataControllerSpecificationTest.cs index 7855beca..889d3b34 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/ApplicationModels/ODataControllerSpecificationTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/ApplicationModels/ODataControllerSpecificationTest.cs @@ -29,25 +29,26 @@ public void is_satisfied_by_should_return_expected_value( Type controllerType, b result.Should().Be( expected ); } +#pragma warning disable IDE0079 #pragma warning disable CA1812 private sealed class NormalODataController : ODataController { [EnableQuery] - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } [ODataAttributeRouting] private sealed class CustomODataController : ControllerBase { [EnableQuery] - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } [Route( "api/test" )] private sealed class NonODataController : ControllerBase { [HttpGet] - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } } \ No newline at end of file diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Asp.Versioning.OData.Tests.csproj b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Asp.Versioning.OData.Tests.csproj index e72dee9a..37771eff 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Asp.Versioning.OData.Tests.csproj +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Asp.Versioning.OData.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + $(DefaultTargetFramework) Asp.Versioning diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Controllers/VersionedMetadataControllerTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Controllers/VersionedMetadataControllerTest.cs index 516ef02c..e363fa4c 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Controllers/VersionedMetadataControllerTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Controllers/VersionedMetadataControllerTest.cs @@ -34,6 +34,7 @@ public async Task options_should_return_expected_headers() response.Content.Headers.Allow.Should().BeEquivalentTo( "GET", "OPTIONS" ); } +#pragma warning disable IDE0079 #pragma warning disable CA1812 #pragma warning disable CA1822 // Mark members as static diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/ModelConfigurationFeatureProviderTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/ModelConfigurationFeatureProviderTest.cs index e344a34d..0987219f 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/ModelConfigurationFeatureProviderTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/ModelConfigurationFeatureProviderTest.cs @@ -28,10 +28,11 @@ public void populate_feature_should_discover_valid_model_configurations() provider.PopulateFeature( partManager.ApplicationParts, feature ); // assert - feature.ModelConfigurations.Should().Equal( new[] { typeof( PublicModelConfiguration ) } ); + feature.ModelConfigurations.Should().Equal( [typeof( PublicModelConfiguration )] ); } } +#pragma warning disable IDE0079 #pragma warning disable CA1812 #pragma warning disable SA1402 // File may only contain a single type #pragma warning disable SA1403 // File may only contain a single namespace @@ -40,10 +41,10 @@ namespace ModelConfigurations { internal struct ValueTypeModelConfiguration : IModelConfiguration { - public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } + public readonly void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } - internal class PrivateModelConfiguration : IModelConfiguration + internal sealed class PrivateModelConfiguration : IModelConfiguration { public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } @@ -53,12 +54,12 @@ public abstract class AbstractModelConfiguration : IModelConfiguration public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } - public class GenericModelConfiguration : IModelConfiguration + public sealed class GenericModelConfiguration : IModelConfiguration { public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } - public class PublicModelConfiguration : IModelConfiguration + public sealed class PublicModelConfiguration : IModelConfiguration { public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { } } diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs index 29be43a2..b40917a9 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/OData/VersionedODataModelBuilderTest.cs @@ -29,7 +29,7 @@ public void get_edm_models_should_return_expected_results() var model = builder.GetEdmModels().Single(); // assert - model.GetAnnotationValue( model ).ApiVersion.Should().Be( apiVersion ); + model.GetApiVersion().Should().Be( apiVersion ); modelCreated.Verify( f => f( It.IsAny(), model ), Once() ); } diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/DefaultMetadataMatcherPolicyTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/DefaultMetadataMatcherPolicyTest.cs index 2e3273c3..0d2bd095 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/DefaultMetadataMatcherPolicyTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/DefaultMetadataMatcherPolicyTest.cs @@ -17,7 +17,7 @@ public void applies_to_endpoints_should_return_true_for_service_document() var paramSource = Mock.Of(); var options = Options.Create( new ApiVersioningOptions() ); var policy = new DefaultMetadataMatcherPolicy( paramSource, options ); - var metadata = new ODataRoutingMetadata( string.Empty, EdmCoreModel.Instance, new ODataPathTemplate() ); + var metadata = new ODataRoutingMetadata( string.Empty, EdmCoreModel.Instance, [] ); var items = new object[] { metadata }; var endpoints = new Endpoint[] { new( Limbo, new( items ), default ) }; diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs index 8f39cd8d..9181574e 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedAttributeRoutingConventionTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dneutral + namespace Asp.Versioning.Routing; using Asp.Versioning.OData; diff --git a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedMetadataRoutingConventionTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedMetadataRoutingConventionTest.cs index 8559374e..0346cac2 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedMetadataRoutingConventionTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.Tests/Routing/VersionedMetadataRoutingConventionTest.cs @@ -52,6 +52,7 @@ public void applied_to_action_should_return_true() action.Selectors.Should().HaveCount( 1 ); } +#pragma warning disable IDE0079 #pragma warning disable CA1812 private sealed class AnotherVersionedMetadataController : VersionedMetadataController diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs new file mode 100644 index 00000000..85427ba8 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using System.Collections; + +/// +/// Represents a collection of collated API version metadata. +/// +public class ApiVersionMetadataCollationCollection : IList, IReadOnlyList +{ + private readonly List items; + private readonly List groups; + + /// + /// Initializes a new instance of the class. + /// + public ApiVersionMetadataCollationCollection() + { + items = []; + groups = []; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial capacity of the collection. + public ApiVersionMetadataCollationCollection( int capacity ) + { + items = new( capacity ); + groups = new( capacity ); + } + + /// + /// Gets the item in the list at the specified index. + /// + /// The zero-based index of the item to retrieve. + /// The item at the specified index. + public ApiVersionMetadata this[int index] => items[index]; + + ApiVersionMetadata IList.this[int index] + { + get => items[index]; + set => throw new NotSupportedException(); + } + + /// + public int Count => items.Count; + +#pragma warning disable IDE0079 +#pragma warning disable CA1033 // Interface methods should be callable by child types + bool ICollection.IsReadOnly => ( (ICollection) items ).IsReadOnly; +#pragma warning restore CA1033 // Interface methods should be callable by child types +#pragma warning restore IDE0079 + + /// + public void Add( ApiVersionMetadata item ) => Insert( Count, item, default ); + + /// + /// Adds an item to the collection. + /// + /// The item to add. + /// The associated group name, if any. + public void Add( ApiVersionMetadata item, string? groupName ) => Insert( Count, item, groupName ); + + /// + public void Clear() + { + items.Clear(); + groups.Clear(); + } + + /// + public bool Contains( ApiVersionMetadata item ) => item != null && items.Contains( item ); + + /// + public void CopyTo( ApiVersionMetadata[] array, int arrayIndex ) => items.CopyTo( array, arrayIndex ); + + /// + public IEnumerator GetEnumerator() => items.GetEnumerator(); + + /// + public int IndexOf( ApiVersionMetadata item ) => item == null ? -1 : items.IndexOf( item ); + + /// + public void Insert( int index, ApiVersionMetadata item ) => Insert( index, item, default ); + + /// + /// Inserts an item into the collection. + /// + /// The zero-based index where insertion takes place. + /// The item to insert. + /// The associated group name, if any. + public void Insert( int index, ApiVersionMetadata item, string? groupName ) + { + items.Insert( index, item ?? throw new ArgumentNullException( nameof( item ) ) ); + groups.Insert( index, groupName ); + } + + /// + public bool Remove( ApiVersionMetadata item ) + { + if ( item == null ) + { + return false; + } + + var index = items.IndexOf( item ); + + if ( index < 0 ) + { + return false; + } + + RemoveAt( index ); + return true; + } + + /// + public void RemoveAt( int index ) + { + items.RemoveAt( index ); + groups.RemoveAt( index ); + } + + IEnumerator IEnumerable.GetEnumerator() => ( (IEnumerable) items ).GetEnumerator(); + + /// + /// Gets the group name for the item at the specified index. + /// + /// The zero-based index of the item to get the group name for. + /// The associated group name or null. + /// If the specified is out of range, null + /// is returned. + public string? GroupName( int index ) => + index < 0 || index >= groups.Count ? default : groups[index]; +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs new file mode 100644 index 00000000..313c2f26 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +/// +/// Represents the context used during API version metadata collation. +/// +public class ApiVersionMetadataCollationContext +{ + /// + /// Gets the read-only list of collation results. + /// + /// The read-only list of collation results. + public ApiVersionMetadataCollationCollection Results { get; } = []; +} \ No newline at end of file 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 new file mode 100644 index 00000000..e5ce20fb --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Primitives; + +/// +/// Represents the API version metadata collection provider for endpoints. +/// +[CLSCompliant( false )] +public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider +{ + private readonly EndpointDataSource endpointDataSource; + private 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 ) + { + ArgumentNullException.ThrowIfNull( endpointDataSource ); + ArgumentNullException.ThrowIfNull( endpointInspector ); + + this.endpointDataSource = endpointDataSource; + this.endpointInspector = endpointInspector; + ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version ); + } + + /// + public int Version => version; + + /// + public void Execute( ApiVersionMetadataCollationContext context ) + { + ArgumentNullException.ThrowIfNull( context ); + + var endpoints = endpointDataSource.Endpoints; + + for ( var i = 0; i < endpoints.Count; i++ ) + { + var endpoint = endpoints[i]; + + if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item || + endpointInspector.IsControllerAction( endpoint ) ) + { + continue; + } + + var groupName = endpoint.Metadata.OfType().LastOrDefault()?.EndpointGroupName; + context.Results.Add( item, groupName ); + } + } +} \ No newline at end of file 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 diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionMetadataCollationProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionMetadataCollationProvider.cs new file mode 100644 index 00000000..04a386a6 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/IApiVersionMetadataCollationProvider.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +/// +/// Defines the behavior of an API version metadata collation provider. +/// +public interface IApiVersionMetadataCollationProvider +{ + /// + /// Gets version of the underlying provider results. + /// + /// The version of the provider results. This can be used to detect changes. + int Version { get; } + + /// + /// Executes the provider using the given context. + /// + /// The collation context. + void Execute( ApiVersionMetadataCollationContext context ); +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/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/ApiVersioningFeature.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiVersioningFeature.cs index 5c515096..8437429a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiVersioningFeature.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiVersioningFeature.cs @@ -5,6 +5,7 @@ namespace Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using System.Globalization; +using System.Runtime.CompilerServices; /// /// Represents the API versioning feature. @@ -33,12 +34,7 @@ public IReadOnlyList RawRequestedApiVersions { if ( rawApiVersions is null ) { - var reader = - context.RequestServices.GetService() ?? - ApiVersionReader.Combine( - new QueryStringApiVersionReader(), - new UrlSegmentApiVersionReader() ); - + var reader = context.RequestServices.GetService() ?? ApiVersionReader.Default; rawApiVersions = reader.Read( context.Request ); } @@ -58,16 +54,16 @@ public string? RawRequestedApiVersion { 0 => default, 1 => values[0], -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations; existing behavior via IApiVersionReader.Read - _ => throw new AmbiguousApiVersionException( - string.Format( CultureInfo.CurrentCulture, CommonSR.MultipleDifferentApiVersionsRequested, string.Join( ", ", values ) ), - values ), -#pragma warning restore CA1065 +#pragma warning disable IDE0079 +#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations + _ => throw NewAmbiguousApiVersionException( values ), +#pragma warning restore CA1065 // Do not raise exceptions in unexpected locations +#pragma warning restore IDE0079 }; } set { - rawApiVersions = string.IsNullOrEmpty( value ) ? default : new[] { value }; + rawApiVersions = string.IsNullOrEmpty( value ) ? default : [value]; } } @@ -88,7 +84,8 @@ public ApiVersion? RequestedApiVersion return apiVersion; } - var parser = context.RequestServices.GetRequiredService(); + var parser = context.RequestServices.GetService() + ?? ApiVersionParser.Default; try { @@ -105,10 +102,20 @@ public ApiVersion? RequestedApiVersion { apiVersion = value; - if ( apiVersion is not null && ( rawApiVersions is null || rawApiVersions.Count == 0 ) ) + if ( apiVersion is not null && + ( rawApiVersions is null || rawApiVersions.Count == 0 ) ) { - rawApiVersions = new[] { apiVersion.ToString() }; + rawApiVersions = [apiVersion.ToString()]; } } } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static AmbiguousApiVersionException NewAmbiguousApiVersionException( IReadOnlyList values ) => + new( + string.Format( + CultureInfo.CurrentCulture, + Format.MultipleDifferentApiVersionsRequested, + string.Join( ", ", [.. values], 0, values.Count ) ), + values ); } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj index 4dc9d40d..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,13 +1,14 @@  - 7.0.0 - 7.0.0.0 - net7.0 + 8.1.0 + 8.1.0.0 + $(DefaultTargetFramework) Asp.Versioning ASP.NET Core API Versioning A service API versioning library for Microsoft ASP.NET Core. Asp;AspNet;AspNetCore;Versioning + true diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/ApiVersionSetBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/ApiVersionSetBuilder.cs index 3a0303f6..e24853fc 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/ApiVersionSetBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/ApiVersionSetBuilder.cs @@ -110,10 +110,7 @@ public virtual ApiVersionSetBuilder AdvertisesDeprecatedApiVersion( ApiVersion a /// A new API version model. protected internal virtual ApiVersionModel BuildApiVersionModel( ApiVersioningOptions options ) { - if ( options == null ) - { - throw new ArgumentNullException( nameof( options ) ); - } + ArgumentNullException.ThrowIfNull( options ); if ( VersionNeutral ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/ApiVersionSetBuilderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/ApiVersionSetBuilderFactory.cs new file mode 100644 index 00000000..7d5960dd --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/ApiVersionSetBuilderFactory.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Builder; + +/// +/// Creates and returns a new API version set builder. +/// +/// The name of the API associated with the builder, if any. +/// A new API version set builder. +public delegate ApiVersionSetBuilder ApiVersionSetBuilderFactory( string? name = default ); \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/DefaultApiVersionSetBuilderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/DefaultApiVersionSetBuilderFactory.cs deleted file mode 100644 index c8f879a1..00000000 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/DefaultApiVersionSetBuilderFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning.Builder; - -/// -/// Represents the default API version set builder factory. -/// -public class DefaultApiVersionSetBuilderFactory : IApiVersionSetBuilderFactory -{ - /// - public ApiVersionSetBuilder Create( string? name = default ) => CreateInstance( name ); - - /// - /// Creates and returns a new builder instance. - /// - /// The optional name associated with the builder. - /// A new API version set builder. - protected virtual ApiVersionSetBuilder CreateInstance( string? name ) => new( name ); -} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/EndpointBuilderFinalizer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/EndpointBuilderFinalizer.cs new file mode 100644 index 00000000..250083cd --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/EndpointBuilderFinalizer.cs @@ -0,0 +1,287 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Builder; + +using Asp.Versioning; +using Asp.Versioning.Routing; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System.Globalization; +using System.Runtime.CompilerServices; +using static Asp.Versioning.ApiVersionParameterLocation; +using static Asp.Versioning.ApiVersionProviderOptions; + +internal static class EndpointBuilderFinalizer +{ + internal static void FinalizeEndpoints( EndpointBuilder endpointBuilder ) + { + var versionSet = GetApiVersionSet( endpointBuilder.Metadata ); + Finialize( endpointBuilder, versionSet ); + } + + internal static void FinalizeRoutes( EndpointBuilder endpointBuilder ) + { + var versionSet = endpointBuilder.ApplicationServices.GetService(); + Finialize( endpointBuilder, versionSet ); + } + + private static void Finialize( EndpointBuilder endpointBuilder, ApiVersionSet? versionSet ) + { + if ( versionSet is null ) + { + // this could only happen if the ApiVersionSet was removed elsewhere from the metadata + endpointBuilder.Metadata.Add( ApiVersionMetadata.Empty ); + return; + } + + var services = endpointBuilder.ApplicationServices; + var endpointMetadata = endpointBuilder.Metadata; + var options = services.GetRequiredService>().Value; + var metadata = Build( endpointMetadata, versionSet, options ); + var reportApiVersions = ReportApiVersions( endpointMetadata ) || + options.ReportApiVersions || + versionSet.ReportApiVersions; + + endpointBuilder.Metadata.Add( metadata ); + + var requestDelegate = default( RequestDelegate ); + + if ( reportApiVersions ) + { + requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate ); + + var reporter = services.GetRequiredService(); + + requestDelegate = new ReportApiVersionsDecorator( requestDelegate, reporter, metadata ); + endpointBuilder.RequestDelegate = requestDelegate; + } + + var parameterSource = services.GetRequiredService(); + + if ( parameterSource.VersionsByMediaType() ) + { + var parameterName = parameterSource.GetParameterName( MediaTypeParameter ); + + if ( !string.IsNullOrEmpty( parameterName ) ) + { + requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate ); + requestDelegate = new ContentTypeApiVersionDecorator( requestDelegate, parameterName ); + endpointBuilder.RequestDelegate = requestDelegate; + } + } + } + + private static bool IsApiVersionNeutral( IList metadata ) + { + var versionNeutral = false; + + for ( var i = metadata.Count - 1; i >= 0; i-- ) + { + if ( metadata[i] is IApiVersionNeutral ) + { + versionNeutral = true; + metadata.RemoveAt( i ); + break; + } + } + + if ( versionNeutral ) + { + for ( var i = metadata.Count - 1; i >= 0; i-- ) + { + switch ( metadata[i] ) + { + case IApiVersionProvider: + case IApiVersionNeutral: + metadata.RemoveAt( i ); + break; + } + } + } + + return versionNeutral; + } + + private static bool ReportApiVersions( IList metadata ) + { + var result = false; + + for ( var i = metadata.Count - 1; i >= 0; i-- ) + { + if ( metadata[i] is IReportApiVersions ) + { + result = true; + metadata.RemoveAt( i ); + } + } + + return result; + } + + private static ApiVersionSet? GetApiVersionSet( IList metadata ) + { + for ( var i = metadata.Count - 1; i >= 0; i-- ) + { + if ( metadata[i] is ApiVersionSet versionSet ) + { + metadata.RemoveAt( i ); + return versionSet; + } + } + + return default; + } + + private static bool TryGetApiVersions( IList metadata, out ApiVersionBuckets buckets ) + { + if ( IsApiVersionNeutral( metadata ) ) + { + buckets = default; + return false; + } + + var mapped = default( SortedSet ); + var supported = default( SortedSet ); + var deprecated = default( SortedSet ); + var advertised = default( SortedSet ); + var deprecatedAdvertised = default( SortedSet ); + + for ( var i = metadata.Count - 1; i >= 0; i-- ) + { + var item = metadata[i]; + + if ( item is not IApiVersionProvider provider ) + { + continue; + } + + metadata.RemoveAt( i ); + + var versions = provider.Versions; + var target = provider.Options switch + { + None => supported ??= [], + Mapped => mapped ??= [], + Deprecated => deprecated ??= [], + Advertised => advertised ??= [], + Advertised | Deprecated => deprecatedAdvertised ??= [], + _ => default, + }; + + if ( target is null ) + { + continue; + } + + for ( var j = 0; j < versions.Count; j++ ) + { + target.Add( versions[j] ); + } + } + + buckets = new( + mapped?.ToArray() ?? [], + supported?.ToArray() ?? [], + deprecated?.ToArray() ?? [], + advertised?.ToArray() ?? [], + deprecatedAdvertised?.ToArray() ?? [] ); + + return true; + } + + private static ApiVersionMetadata Build( IList metadata, ApiVersionSet versionSet, ApiVersioningOptions options ) + { + var name = versionSet.Name; + ApiVersionModel? apiModel; + + if ( !TryGetApiVersions( metadata, out var buckets ) || + ( apiModel = versionSet.Build( options ) ).IsApiVersionNeutral ) + { + if ( string.IsNullOrEmpty( name ) ) + { + return ApiVersionMetadata.Neutral; + } + + return new( ApiVersionModel.Neutral, ApiVersionModel.Neutral, name ); + } + + ApiVersionModel endpointModel; + ApiVersion[] emptyVersions; + var inheritedSupported = apiModel.SupportedApiVersions; + var inheritedDeprecated = apiModel.DeprecatedApiVersions; + + if ( buckets.AreEmpty ) + { + var noInheritedApiVersions = inheritedSupported.Count == 0 && + inheritedDeprecated.Count == 0; + + if ( noInheritedApiVersions ) + { + endpointModel = ApiVersionModel.Empty; + } + else + { + emptyVersions = []; + endpointModel = new( + declaredVersions: emptyVersions, + inheritedSupported, + inheritedDeprecated, + emptyVersions, + emptyVersions ); + } + } + else + { + var (mapped, supported, deprecated, advertised, advertisedDeprecated) = buckets; + + if ( mapped.Count == 0 ) + { + endpointModel = new( + declaredVersions: supported.Union( deprecated ), + supported.Union( inheritedSupported ), + deprecated.Union( inheritedDeprecated ), + advertised, + advertisedDeprecated ); + } + else + { + emptyVersions = []; + endpointModel = new( + declaredVersions: mapped, + supportedVersions: inheritedSupported, + deprecatedVersions: inheritedDeprecated, + advertisedVersions: emptyVersions, + deprecatedAdvertisedVersions: emptyVersions ); + } + } + + return new( apiModel, endpointModel, name ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static RequestDelegate EnsureRequestDelegate( RequestDelegate? current, RequestDelegate? original ) => + ( current ?? original ) ?? + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Format.UnsetRequestDelegate, + nameof( RequestDelegate ), + nameof( RouteEndpoint ) ) ); + + private record struct ApiVersionBuckets( + IReadOnlyList Mapped, + IReadOnlyList Supported, + IReadOnlyList Deprecated, + IReadOnlyList Advertised, + IReadOnlyList AdvertisedDeprecated ) + { + internal readonly bool AreEmpty = Mapped.Count == 0 + && Supported.Count == 0 + && Deprecated.Count == 0 + && Advertised.Count == 0 + && AdvertisedDeprecated.Count == 0; + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IApiVersionSetBuilderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IApiVersionSetBuilderFactory.cs deleted file mode 100644 index 2b96db7e..00000000 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IApiVersionSetBuilderFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning.Builder; - -/// -/// Defines the behavior of a factory to create API version set builders. -/// -public interface IApiVersionSetBuilderFactory -{ - /// - /// Creates and returns a new API version set builder. - /// - /// The name of the API associated with the builder, if any. - /// A new API version set builder. - ApiVersionSetBuilder Create( string? name = default ); -} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointConventionBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointConventionBuilderExtensions.cs index a37fac92..15525d66 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointConventionBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointConventionBuilderExtensions.cs @@ -4,14 +4,10 @@ namespace Microsoft.AspNetCore.Builder; using Asp.Versioning; using Asp.Versioning.Builder; -using Asp.Versioning.Routing; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; +using System.Collections; using System.Globalization; -using System.Runtime.CompilerServices; -using static Asp.Versioning.ApiVersionParameterLocation; +using System.Runtime.Serialization; using static Asp.Versioning.ApiVersionProviderOptions; /// @@ -32,298 +28,501 @@ public static TBuilder WithApiVersionSet( ApiVersionSet apiVersionSet ) where TBuilder : notnull, IEndpointConventionBuilder { - if ( apiVersionSet == null ) - { - throw new ArgumentNullException( nameof( apiVersionSet ) ); - } + ArgumentNullException.ThrowIfNull( apiVersionSet ); - builder.Add( endpoint => endpoint.Metadata.Add( apiVersionSet ) ); - builder.Finally( FinalizeEndpoints ); + builder.Add( endpoint => AddMetadata( endpoint, apiVersionSet ) ); + builder.Finally( EndpointBuilderFinalizer.FinalizeEndpoints ); return builder; } /// - /// Applies the specified API version set to the endpoint group. + /// Indicates that the specified API version is mapped to the configured endpoint. /// - /// The type of builder. - /// The extended builder. - /// The optional name associated with the builder. - /// A new instance. - public static IVersionedEndpointRouteBuilder WithApiVersionSet( this TBuilder builder, string? name = default ) - where TBuilder : notnull, IEndpointRouteBuilder, IEndpointConventionBuilder - { - if ( builder is IVersionedEndpointRouteBuilder versionedBuilder ) - { - return versionedBuilder; - } + /// The extended type. + /// The extended endpoint convention builder. + /// The major version number. + /// The optional minor version number. + /// The optional version status. + /// The original . + public static TBuilder MapToApiVersion( this TBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.MapToApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); - var factory = builder.ServiceProvider.GetRequiredService(); + /// + /// Indicates that the specified API version is mapped to the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version number. + /// The optional version status. + /// The original . + public static TBuilder MapToApiVersion( this TBuilder builder, double version, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.MapToApiVersion( new ApiVersion( version, status ) ); - versionedBuilder = new VersionedEndpointRouteBuilder( builder, builder, factory.Create( name ) ); - builder.Finally( FinalizeRoutes ); + /// + /// Indicates that the specified API version is mapped to the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version year. + /// The version month. + /// The version day. + /// The optional version status. + /// The original . + public static TBuilder MapToApiVersion( this TBuilder builder, int year, int month, int day, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.MapToApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); + + /// + /// Indicates that the specified API version is mapped to the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The group version. + /// The optional version status. + /// The original . + public static TBuilder MapToApiVersion( this TBuilder builder, DateOnly groupVersion, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.MapToApiVersion( new ApiVersion( groupVersion, status ) ); - return versionedBuilder; + /// + /// Maps the specified API version to the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The API version to map to the endpoint. + /// The original . + public static TBuilder MapToApiVersion( this TBuilder builder, ApiVersion apiVersion ) + where TBuilder : notnull, IEndpointConventionBuilder + { + builder.Add( endpoint => AddMetadata( endpoint, Convention.MapToApiVersion( apiVersion ) ) ); + return builder; } - private static void FinalizeEndpoints( EndpointBuilder endpointBuilder ) + /// + /// Indicates that the endpoint is API version-neutral. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The original . + public static TBuilder IsApiVersionNeutral( this TBuilder builder ) + where TBuilder : notnull, IEndpointConventionBuilder { - var versionSet = GetApiVersionSet( endpointBuilder.Metadata ); - Finialize( endpointBuilder, versionSet ); + builder.Add( endpoint => AddMetadata( endpoint, new ApiVersionNeutralAttribute() ) ); + return builder; } - private static void FinalizeRoutes( EndpointBuilder endpointBuilder ) + /// + /// Indicates that the specified API version is supported by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The major version number. + /// The optional minor version number. + /// The optional version status. + /// The original . + public static TBuilder HasApiVersion( this TBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); + + /// + /// Indicates that the specified API version is supported by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version number. + /// The optional version status. + /// The original . + public static TBuilder HasApiVersion( this TBuilder builder, double version, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasApiVersion( new ApiVersion( version, status ) ); + + /// + /// Indicates that the specified API version is supported by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version year. + /// The version month. + /// The version day. + /// The optional version status. + /// The original . + public static TBuilder HasApiVersion( this TBuilder builder, int year, int month, int day, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); + + /// + /// Indicates that the specified API version is supported by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The group version. + /// The optional version status. + /// The original . + public static TBuilder HasApiVersion( this TBuilder builder, DateOnly groupVersion, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasApiVersion( new ApiVersion( groupVersion, status ) ); + + /// + /// Indicates that the specified API version is supported by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The supported API version implemented by the endpoint. + /// The original . + public static TBuilder HasApiVersion( this TBuilder builder, ApiVersion apiVersion ) + where TBuilder : notnull, IEndpointConventionBuilder { - var versionSet = endpointBuilder.ApplicationServices.GetService(); - Finialize( endpointBuilder, versionSet ); + builder.Add( + endpoint => + { + AddMetadata( endpoint, Convention.HasApiVersion( apiVersion ) ); + AdvertiseInApiVersionSet( endpoint.Metadata, apiVersion ); + } ); + + return builder; } - private static void Finialize( EndpointBuilder endpointBuilder, ApiVersionSet? versionSet ) + /// + /// Indicates that the specified API version is deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The major version number. + /// The optional minor version number. + /// The optional version status. + /// The original . + public static TBuilder HasDeprecatedApiVersion( this TBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasDeprecatedApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); + + /// + /// Indicates that the specified API version is deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version number. + /// The optional version status. + /// The original . + public static TBuilder HasDeprecatedApiVersion( this TBuilder builder, double version, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasDeprecatedApiVersion( new ApiVersion( version, status ) ); + + /// + /// Indicates that the specified API version is deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version year. + /// The version month. + /// The version day. + /// The optional version status. + /// The original . + public static TBuilder HasDeprecatedApiVersion( this TBuilder builder, int year, int month, int day, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasDeprecatedApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); + + /// + /// Indicates that the specified API version is deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The group version. + /// The optional version status. + /// The original . + public static TBuilder HasDeprecatedApiVersion( this TBuilder builder, DateOnly groupVersion, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.HasDeprecatedApiVersion( new ApiVersion( groupVersion, status ) ); + + /// + /// Indicates that the specified API version is deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The deprecated API version implemented by the endpoint. + /// The original . + public static TBuilder HasDeprecatedApiVersion( this TBuilder builder, ApiVersion apiVersion ) + where TBuilder : notnull, IEndpointConventionBuilder { - if ( versionSet is null ) - { - // this should be impossible because WithApiVersionSet had to be called to get here - endpointBuilder.Metadata.Add( ApiVersionMetadata.Empty ); - return; - } + builder.Add( + endpoint => + { + AddMetadata( endpoint, Convention.HasDeprecatedApiVersion( apiVersion ) ); + AdvertiseDeprecatedInApiVersionSet( endpoint.Metadata, apiVersion ); + } ); - var services = endpointBuilder.ApplicationServices; - var endpointMetadata = endpointBuilder.Metadata; - var options = services.GetRequiredService>().Value; - var metadata = Build( endpointMetadata, versionSet, options ); - var reportApiVersions = ReportApiVersions( endpointMetadata ) || - options.ReportApiVersions || - versionSet.ReportApiVersions; + return builder; + } - endpointBuilder.Metadata.Add( metadata ); + /// + /// Indicates that the specified API version is advertised by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The major version number. + /// The optional minor version number. + /// The optional version status. + /// The original . + public static TBuilder AdvertisesApiVersion( this TBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); - var requestDelegate = default( RequestDelegate ); + /// + /// Indicates that the specified API version is advertised by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version number. + /// The optional version status. + /// The original . + public static TBuilder AdvertisesApiVersion( this TBuilder builder, double version, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesApiVersion( new ApiVersion( version, status ) ); - if ( reportApiVersions ) - { - requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate ); - requestDelegate = new ReportApiVersionsDecorator( requestDelegate, metadata ); - endpointBuilder.RequestDelegate = requestDelegate; - } + /// + /// Indicates that the specified API version is advertised by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version year. + /// The version month. + /// The version day. + /// The optional version status. + /// The original . + public static TBuilder AdvertisesApiVersion( this TBuilder builder, int year, int month, int day, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); - var parameterSource = endpointBuilder.ApplicationServices.GetRequiredService(); + /// + /// Indicates that the specified API version is advertised by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The group version. + /// The optional version status. + /// The original . + public static TBuilder AdvertisesApiVersion( this TBuilder builder, DateOnly groupVersion, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesApiVersion( new ApiVersion( groupVersion, status ) ); - if ( parameterSource.VersionsByMediaType() ) - { - var parameterName = parameterSource.GetParameterName( MediaTypeParameter ); + /// + /// Indicates that the specified API version is advertised by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The advertised API version not directly implemented by the endpoint. + /// The original . + public static TBuilder AdvertisesApiVersion( this TBuilder builder, ApiVersion apiVersion ) + where TBuilder : notnull, IEndpointConventionBuilder + { + builder.Add( + endpoint => + { + AddMetadata( endpoint, Convention.AdvertisesApiVersion( apiVersion ) ); + AdvertiseInApiVersionSet( endpoint.Metadata, apiVersion ); + } ); + + return builder; + } - if ( !string.IsNullOrEmpty( parameterName ) ) + /// + /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The major version number. + /// The optional minor version number. + /// The optional version status. + /// The original . + public static TBuilder AdvertisesDeprecatedApiVersion( this TBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesDeprecatedApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); + + /// + /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version number. + /// The optional version status. + /// The original . + public static TBuilder AdvertisesDeprecatedApiVersion( this TBuilder builder, double version, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesDeprecatedApiVersion( new ApiVersion( version, status ) ); + + /// + /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The version year. + /// The version month. + /// The version day. + /// The version status. + /// The original . + public static TBuilder AdvertisesDeprecatedApiVersion( this TBuilder builder, int year, int month, int day, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesDeprecatedApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); + + /// + /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The group version. + /// The optional version status. + /// The original . + public static TBuilder AdvertisesDeprecatedApiVersion( this TBuilder builder, DateOnly groupVersion, string? status = default ) + where TBuilder : notnull, IEndpointConventionBuilder => builder.AdvertisesDeprecatedApiVersion( new ApiVersion( groupVersion, status ) ); + + /// + /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The advertised, but deprecated API version not directly implemented by the endpoint. + /// The original . + public static TBuilder AdvertisesDeprecatedApiVersion( this TBuilder builder, ApiVersion apiVersion ) + where TBuilder : notnull, IEndpointConventionBuilder + { + builder.Add( + endpoint => { - requestDelegate = EnsureRequestDelegate( requestDelegate, endpointBuilder.RequestDelegate ); - requestDelegate = new ContentTypeApiVersionDecorator( requestDelegate, parameterName ); - endpointBuilder.RequestDelegate = requestDelegate; - } - } + AddMetadata( endpoint, Convention.AdvertisesDeprecatedApiVersion( apiVersion ) ); + AdvertiseDeprecatedInApiVersionSet( endpoint.Metadata, apiVersion ); + } ); + + return builder; } - private static bool IsApiVersionNeutral( IList metadata ) + /// + /// Indicates that the endpoint will report its API versions. + /// + /// The extended type. + /// The extended endpoint convention builder. + /// The original . + public static TBuilder ReportApiVersions( this TBuilder builder ) + where TBuilder : notnull, IEndpointConventionBuilder { - var versionNeutral = false; + builder.Add( endpoint => AddMetadata( endpoint, Convention.ReportApiVersions ) ); + return builder; + } - for ( var i = metadata.Count - 1; i >= 0; i-- ) + private static void AddMetadata( EndpointBuilder builder, ApiVersionSet versionSet ) + { + var metadata = builder.Metadata; + var grouped = builder.ApplicationServices.GetService( typeof( ApiVersionSetBuilder ) ) is not null; + + if ( grouped ) { - if ( metadata[i] is IApiVersionNeutral ) - { - versionNeutral = true; - metadata.RemoveAt( i ); - break; - } + throw new InvalidOperationException( SR.MultipleVersionSets ); } - if ( versionNeutral ) + for ( var i = 0; i < metadata.Count; i++ ) { - for ( var i = metadata.Count - 1; i >= 0; i-- ) + if ( metadata[i] is ApiVersionSet ) { - switch ( metadata[i] ) - { - case IApiVersionProvider: - case IApiVersionNeutral: - metadata.RemoveAt( i ); - break; - } + throw new InvalidOperationException( SR.MultipleVersionSets ); } } - return versionNeutral; + metadata.Add( versionSet ); + + if ( !string.IsNullOrEmpty( versionSet.Name ) ) + { + metadata.Insert( 0, new TagsAttribute( versionSet.Name ) ); + } } - private static bool ReportApiVersions( IList metadata ) + private static void AddMetadata( EndpointBuilder builder, object item ) { - var result = false; + var metadata = builder.Metadata; + var grouped = builder.ApplicationServices.GetService( typeof( ApiVersionSetBuilder ) ) is not null; + + metadata.Add( item ); + + if ( grouped ) + { + return; + } for ( var i = metadata.Count - 1; i >= 0; i-- ) { - if ( metadata[i] is IReportApiVersions ) + if ( metadata[i] is ApiVersionSet ) { - result = true; - metadata.RemoveAt( i ); + return; } } - return result; + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + Format.NoVersionSet, + builder.DisplayName, + nameof( IEndpointRouteBuilderExtensions.NewVersionedApi ), + nameof( IEndpointRouteBuilderExtensions.WithApiVersionSet ) ) ); } - private static ApiVersionSet? GetApiVersionSet( IList metadata ) + private static void AdvertiseInApiVersionSet( IList metadata, ApiVersion apiVersion ) { - var result = default( ApiVersionSet ); - for ( var i = metadata.Count - 1; i >= 0; i-- ) { - if ( metadata[i] is ApiVersionSet set ) + if ( metadata[i] is ApiVersionSet versionSet ) { - result ??= set; - metadata.RemoveAt( i ); + versionSet.AdvertisesApiVersion( apiVersion ); + break; } } - - return result; } - private static bool TryGetApiVersions( IList metadata, out ApiVersionBuckets buckets ) + private static void AdvertiseDeprecatedInApiVersionSet( IList metadata, ApiVersion apiVersion ) { - if ( IsApiVersionNeutral( metadata ) ) - { - buckets = default; - return false; - } - - var mapped = default( SortedSet ); - var supported = default( SortedSet ); - var deprecated = default( SortedSet ); - var advertised = default( SortedSet ); - var deprecatedAdvertised = default( SortedSet ); - for ( var i = metadata.Count - 1; i >= 0; i-- ) { - var item = metadata[i]; - - if ( item is not IApiVersionProvider provider ) + if ( metadata[i] is ApiVersionSet versionSet ) { - continue; + versionSet.AdvertisesDeprecatedApiVersion( apiVersion ); + break; } + } + } - metadata.RemoveAt( i ); + private sealed class SingleItemReadOnlyList : IReadOnlyList + { + private readonly ApiVersion item; - var versions = provider.Versions; - var target = provider.Options switch - { - None => supported ??= new(), - Mapped => mapped ??= new(), - Deprecated => deprecated ??= new(), - Advertised => advertised ??= new(), - Advertised | Deprecated => deprecatedAdvertised ??= new(), - _ => default, - }; - - if ( target is null ) - { - continue; - } + internal SingleItemReadOnlyList( ApiVersion item ) => this.item = item; - for ( var j = 0; j < versions.Count; j++ ) - { - target.Add( versions[j] ); - } +#pragma warning disable IDE0079 +#pragma warning disable CA2201 // Do not raise reserved exception types + public ApiVersion this[int index] => index == 0 ? item : throw new IndexOutOfRangeException(); +#pragma warning restore CA2201 // Do not raise reserved exception types +#pragma warning restore IDE0079 + + public int Count => 1; + + public IEnumerator GetEnumerator() + { + yield return item; } - buckets = new( - mapped?.ToArray() ?? Array.Empty(), - supported?.ToArray() ?? Array.Empty(), - deprecated?.ToArray() ?? Array.Empty(), - advertised?.ToArray() ?? Array.Empty(), - deprecatedAdvertised?.ToArray() ?? Array.Empty() ); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + private sealed class ReportApiVersionsConvention : IReportApiVersions + { + public ApiVersionMapping Mapping => ApiVersionMapping.None; - return true; + public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) { } } - private static ApiVersionMetadata Build( IList metadata, ApiVersionSet versionSet, ApiVersioningOptions options ) + private sealed class Convention : IApiVersionProvider { - var name = versionSet.Name; - ApiVersionModel? apiModel; + private static ReportApiVersionsConvention? reportApiVersions; - if ( !TryGetApiVersions( metadata, out var buckets ) || - ( apiModel = versionSet.Build( options ) ).IsApiVersionNeutral ) + private Convention( ApiVersion version, ApiVersionProviderOptions options ) { - if ( string.IsNullOrEmpty( name ) ) - { - return ApiVersionMetadata.Neutral; - } - - return new( ApiVersionModel.Neutral, ApiVersionModel.Neutral, name ); + Versions = new SingleItemReadOnlyList( version ); + Options = options; } - ApiVersionModel endpointModel; - ApiVersion[] emptyVersions; - var inheritedSupported = apiModel.SupportedApiVersions; - var inheritedDeprecated = apiModel.DeprecatedApiVersions; - var (mapped, supported, deprecated, advertised, advertisedDeprecated) = buckets; - var isEmpty = mapped.Count == 0 && - supported.Count == 0 && - deprecated.Count == 0 && - advertised.Count == 0 && - advertisedDeprecated.Count == 0; - - if ( isEmpty ) - { - var noInheritedApiVersions = inheritedSupported.Count == 0 && - inheritedDeprecated.Count == 0; + public ApiVersionProviderOptions Options { get; } - if ( noInheritedApiVersions ) - { - endpointModel = ApiVersionModel.Empty; - } - else - { - emptyVersions = Array.Empty(); - endpointModel = new( - declaredVersions: emptyVersions, - inheritedSupported, - inheritedDeprecated, - emptyVersions, - emptyVersions ); - } - } - else if ( mapped.Count == 0 ) - { - endpointModel = new( - declaredVersions: supported.Union( deprecated ), - supported.Union( inheritedSupported ), - deprecated.Union( inheritedDeprecated ), - advertised, - advertisedDeprecated ); - } - else - { - emptyVersions = Array.Empty(); - endpointModel = new( - declaredVersions: mapped, - supportedVersions: inheritedSupported, - deprecatedVersions: inheritedDeprecated, - advertisedVersions: emptyVersions, - deprecatedAdvertisedVersions: emptyVersions ); - } + public IReadOnlyList Versions { get; } - return new( apiModel, endpointModel, name ); - } + internal static IReportApiVersions ReportApiVersions => reportApiVersions ??= new(); - private static RequestDelegate EnsureRequestDelegate( RequestDelegate? current, RequestDelegate? original ) => - ( current ?? original ) ?? - throw new InvalidOperationException( - string.Format( - CultureInfo.CurrentCulture, - SR.UnsetRequestDelegate, - nameof( RequestDelegate ), - nameof( RouteEndpoint ) ) ); - - private record struct ApiVersionBuckets( - IReadOnlyList Mapped, - IReadOnlyList Supported, - IReadOnlyList Deprecated, - IReadOnlyList Advertised, - IReadOnlyList AdvertisedDeprecated ); + internal static Convention HasApiVersion( ApiVersion version ) => new( version, None ); + + internal static Convention HasDeprecatedApiVersion( ApiVersion version ) => new( version, Deprecated ); + + internal static Convention MapToApiVersion( ApiVersion version ) => new( version, Mapped ); + + internal static Convention AdvertisesApiVersion( ApiVersion version ) => new( version, Advertised ); + + internal static Convention AdvertisesDeprecatedApiVersion( ApiVersion version ) => new( version, Advertised | Deprecated ); + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointRouteBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointRouteBuilderExtensions.cs index 65664447..e6054e34 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointRouteBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointRouteBuilderExtensions.cs @@ -2,9 +2,12 @@ namespace Microsoft.AspNetCore.Builder; +using Asp.Versioning; using Asp.Versioning.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using System.Runtime.CompilerServices; /// /// Provides extension methods for . @@ -20,13 +23,86 @@ public static class IEndpointRouteBuilderExtensions /// A new API version set builder. public static ApiVersionSetBuilder NewApiVersionSet( this IEndpointRouteBuilder endpoints, string? name = default ) { - if ( endpoints == null ) + ArgumentNullException.ThrowIfNull( endpoints ); + var create = endpoints.ServiceProvider.GetService(); + return create is null ? new( name ) : create( name ); + } + + /// + /// Applies the specified API version set to the endpoint group. + /// + /// The type of builder. + /// The extended builder. + /// The optional name associated with the builder. + /// A new instance. + public static IVersionedEndpointRouteBuilder WithApiVersionSet( this TBuilder builder, string? name = default ) + where TBuilder : notnull, IEndpointRouteBuilder, IEndpointConventionBuilder + { + ArgumentNullException.ThrowIfNull( builder ); + + if ( builder.HasMetadata() ) { - throw new ArgumentNullException( nameof( endpoints ) ); + throw new InvalidOperationException( SR.CannotNestVersionSet ); } - var factory = endpoints.ServiceProvider.GetRequiredService(); + if ( !string.IsNullOrEmpty( name ) ) + { + builder.Add( endpoint => endpoint.Metadata.Insert( 0, new TagsAttribute( name ) ) ); + } - return factory.Create( name ); + builder.Finally( EndpointBuilderFinalizer.FinalizeRoutes ); + + return builder.NewVersionedEndpointRouteBuilder( builder, builder, name ); } + + /// + /// Creates a route group builder for defining all versioned endpoints in an API. + /// + /// The extended . + /// The optional name associated with the builder. + /// A new instance. + public static IVersionedEndpointRouteBuilder NewVersionedApi( this IEndpointRouteBuilder builder, string? name = default ) + { + ArgumentNullException.ThrowIfNull( builder ); + + if ( builder.IsNestedGroup() ) + { + throw new InvalidOperationException( SR.CannotNestApiGroup ); + } + + var group = builder.MapGroup( string.Empty ); + IEndpointConventionBuilder convention = group; + + if ( !string.IsNullOrEmpty( name ) ) + { + convention.Add( endpoint => endpoint.Metadata.Insert( 0, new TagsAttribute( name ) ) ); + } + + convention.Finally( EndpointBuilderFinalizer.FinalizeRoutes ); + + return builder.NewVersionedEndpointRouteBuilder( group, group, name ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static IVersionedEndpointRouteBuilder NewVersionedEndpointRouteBuilder( + this IEndpointRouteBuilder builder, + IEndpointRouteBuilder routeBuilder, + IEndpointConventionBuilder conventionBuilder, + string? name ) + { + var create = builder.ServiceProvider.GetService(); + var versionSet = builder.NewApiVersionSet( name ); + + return create is null ? + new VersionedEndpointRouteBuilder( routeBuilder, conventionBuilder, versionSet ) : + create( routeBuilder, conventionBuilder, versionSet ); + } + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool HasMetadata( this IEndpointRouteBuilder builder ) => + builder.ServiceProvider.GetService() is not null; + + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool IsNestedGroup( this IEndpointRouteBuilder builder ) => + builder is RouteGroupBuilder || builder.HasMetadata(); } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/RouteHandlerBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/RouteHandlerBuilderExtensions.cs deleted file mode 100644 index c8038c68..00000000 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/RouteHandlerBuilderExtensions.cs +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Microsoft.AspNetCore.Builder; - -using Asp.Versioning; -using Asp.Versioning.Builder; -using Microsoft.AspNetCore.Http; -using System.Collections; -using static Asp.Versioning.ApiVersionProviderOptions; - -/// -/// Provides extension methods for . -/// -[CLSCompliant( false )] -public static class RouteHandlerBuilderExtensions -{ - /// - /// Indicates that the specified API version is mapped to the configured endpoint. - /// - /// The extended route handler builder. - /// The major version number. - /// The optional minor version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder MapToApiVersion( this RouteHandlerBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) => - builder.MapToApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); - - /// - /// Indicates that the specified API version is mapped to the configured endpoint. - /// - /// The extended route handler builder. - /// The version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder MapToApiVersion( this RouteHandlerBuilder builder, double version, string? status = default ) => - builder.MapToApiVersion( new ApiVersion( version, status ) ); - - /// - /// Indicates that the specified API version is mapped to the configured endpoint. - /// - /// The extended route handler builder. - /// The version year. - /// The version month. - /// The version day. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder MapToApiVersion( this RouteHandlerBuilder builder, int year, int month, int day, string? status = default ) => - builder.MapToApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); - - /// - /// Indicates that the specified API version is mapped to the configured endpoint. - /// - /// The extended route handler builder. - /// The group version. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder MapToApiVersion( this RouteHandlerBuilder builder, DateOnly groupVersion, string? status = default ) => - builder.MapToApiVersion( new ApiVersion( groupVersion, status ) ); - - /// - /// Maps the specified API version to the configured endpoint. - /// - /// The extended route handler builder. - /// The API version to map to the endpoint. - /// The original . - public static RouteHandlerBuilder MapToApiVersion( this RouteHandlerBuilder builder, ApiVersion apiVersion ) - { - builder.Add( endpoint => endpoint.Metadata.Add( Convention.MapToApiVersion( apiVersion ) ) ); - return builder; - } - - /// - /// Indicates that the endpoint is API version-neutral. - /// - /// The extended route handler builder. - /// The original . - public static RouteHandlerBuilder IsApiVersionNeutral( this RouteHandlerBuilder builder ) - { - builder.Add( endpoint => endpoint.Metadata.Add( new ApiVersionNeutralAttribute() ) ); - return builder; - } - - /// - /// Indicates that the specified API version is supported by the configured endpoint. - /// - /// The extended route handler builder. - /// The major version number. - /// The optional minor version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasApiVersion( this RouteHandlerBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) => - builder.HasApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); - - /// - /// Indicates that the specified API version is supported by the configured endpoint. - /// - /// The extended route handler builder. - /// The version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasApiVersion( this RouteHandlerBuilder builder, double version, string? status = default ) => - builder.HasApiVersion( new ApiVersion( version, status ) ); - - /// - /// Indicates that the specified API version is supported by the configured endpoint. - /// - /// The extended route handler builder. - /// The version year. - /// The version month. - /// The version day. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasApiVersion( this RouteHandlerBuilder builder, int year, int month, int day, string? status = default ) => - builder.HasApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); - - /// - /// Indicates that the specified API version is supported by the configured endpoint. - /// - /// The extended route handler builder. - /// The group version. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasApiVersion( this RouteHandlerBuilder builder, DateOnly groupVersion, string? status = default ) => - builder.HasApiVersion( new ApiVersion( groupVersion, status ) ); - - /// - /// Indicates that the specified API version is supported by the configured endpoint. - /// - /// The extended route handler builder. - /// The supported API version implemented by the endpoint. - /// The original . - public static RouteHandlerBuilder HasApiVersion( this RouteHandlerBuilder builder, ApiVersion apiVersion ) - { - builder.Add( - endpoint => - { - var metadata = endpoint.Metadata; - metadata.Add( Convention.HasApiVersion( apiVersion ) ); - AdvertiseInApiVersionSet( metadata, apiVersion ); - } ); - - return builder; - } - - /// - /// Indicates that the specified API version is deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The major version number. - /// The optional minor version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasDeprecatedApiVersion( this RouteHandlerBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) => - builder.HasDeprecatedApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); - - /// - /// Indicates that the specified API version is deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasDeprecatedApiVersion( this RouteHandlerBuilder builder, double version, string? status = default ) => - builder.HasDeprecatedApiVersion( new ApiVersion( version, status ) ); - - /// - /// Indicates that the specified API version is deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The version year. - /// The version month. - /// The version day. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasDeprecatedApiVersion( this RouteHandlerBuilder builder, int year, int month, int day, string? status = default ) => - builder.HasDeprecatedApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); - - /// - /// Indicates that the specified API version is deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The group version. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder HasDeprecatedApiVersion( this RouteHandlerBuilder builder, DateOnly groupVersion, string? status = default ) => - builder.HasDeprecatedApiVersion( new ApiVersion( groupVersion, status ) ); - - /// - /// Indicates that the specified API version is deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The deprecated API version implemented by the endpoint. - /// The original . - public static RouteHandlerBuilder HasDeprecatedApiVersion( this RouteHandlerBuilder builder, ApiVersion apiVersion ) - { - builder.Add( - endpoint => - { - var metadata = endpoint.Metadata; - metadata.Add( Convention.HasDeprecatedApiVersion( apiVersion ) ); - AdvertiseDeprecatedInApiVersionSet( metadata, apiVersion ); - } ); - - return builder; - } - - /// - /// Indicates that the specified API version is advertised by the configured endpoint. - /// - /// The extended route handler builder. - /// The major version number. - /// The optional minor version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder AdvertisesApiVersion( this RouteHandlerBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) => - builder.AdvertisesApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); - - /// - /// Indicates that the specified API version is advertised by the configured endpoint. - /// - /// The extended route handler builder. - /// The version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder AdvertisesApiVersion( this RouteHandlerBuilder builder, double version, string? status = default ) => - builder.AdvertisesApiVersion( new ApiVersion( version, status ) ); - - /// - /// Indicates that the specified API version is advertised by the configured endpoint. - /// - /// The extended route handler builder. - /// The version year. - /// The version month. - /// The version day. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder AdvertisesApiVersion( this RouteHandlerBuilder builder, int year, int month, int day, string? status = default ) => - builder.AdvertisesApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); - - /// - /// Indicates that the specified API version is advertised by the configured endpoint. - /// - /// The extended route handler builder. - /// The group version. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder AdvertisesApiVersion( this RouteHandlerBuilder builder, DateOnly groupVersion, string? status = default ) => - builder.AdvertisesApiVersion( new ApiVersion( groupVersion, status ) ); - - /// - /// Indicates that the specified API version is advertised by the configured endpoint. - /// - /// The extended route handler builder. - /// The advertised API version not directly implemented by the endpoint. - /// The original . - public static RouteHandlerBuilder AdvertisesApiVersion( this RouteHandlerBuilder builder, ApiVersion apiVersion ) - { - builder.Add( - endpoint => - { - var metadata = endpoint.Metadata; - metadata.Add( Convention.AdvertisesApiVersion( apiVersion ) ); - AdvertiseInApiVersionSet( metadata, apiVersion ); - } ); - - return builder; - } - - /// - /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The major version number. - /// The optional minor version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder AdvertisesDeprecatedApiVersion( this RouteHandlerBuilder builder, int majorVersion, int? minorVersion = default, string? status = default ) => - builder.AdvertisesDeprecatedApiVersion( new ApiVersion( majorVersion, minorVersion, status ) ); - - /// - /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The version number. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder AdvertisesDeprecatedApiVersion( this RouteHandlerBuilder builder, double version, string? status = default ) => - builder.AdvertisesDeprecatedApiVersion( new ApiVersion( version, status ) ); - - /// - /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The version year. - /// The version month. - /// The version day. - /// The version status. - /// The original . - public static RouteHandlerBuilder AdvertisesDeprecatedApiVersion( this RouteHandlerBuilder builder, int year, int month, int day, string? status = default ) => - builder.AdvertisesDeprecatedApiVersion( new ApiVersion( new DateOnly( year, month, day ), status ) ); - - /// - /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The group version. - /// The optional version status. - /// The original . - public static RouteHandlerBuilder AdvertisesDeprecatedApiVersion( this RouteHandlerBuilder builder, DateOnly groupVersion, string? status = default ) => - builder.AdvertisesDeprecatedApiVersion( new ApiVersion( groupVersion, status ) ); - - /// - /// Indicates that the specified API version is advertised and deprecated by the configured endpoint. - /// - /// The extended route handler builder. - /// The advertised, but deprecated API version not directly implemented by the endpoint. - /// The original . - public static RouteHandlerBuilder AdvertisesDeprecatedApiVersion( this RouteHandlerBuilder builder, ApiVersion apiVersion ) - { - builder.Add( - endpoint => - { - var metadata = endpoint.Metadata; - metadata.Add( Convention.AdvertisesDeprecatedApiVersion( apiVersion ) ); - AdvertiseDeprecatedInApiVersionSet( metadata, apiVersion ); - } ); - - return builder; - } - - /// - /// Indicates that the endpoint will report its API versions. - /// - /// The extended route handler builder. - /// The original . - public static RouteHandlerBuilder ReportApiVersions( this RouteHandlerBuilder builder ) - { - builder.Add( endpoint => endpoint.Metadata.Add( Convention.ReportApiVersions ) ); - return builder; - } - - private static void AdvertiseInApiVersionSet( IList metadata, ApiVersion apiVersion ) - { - for ( var i = metadata.Count - 1; i >= 0; i-- ) - { - if ( metadata[i] is ApiVersionSet versionSet ) - { - versionSet.AdvertisesApiVersion( apiVersion ); - break; - } - } - } - - private static void AdvertiseDeprecatedInApiVersionSet( IList metadata, ApiVersion apiVersion ) - { - for ( var i = metadata.Count - 1; i >= 0; i-- ) - { - if ( metadata[i] is ApiVersionSet versionSet ) - { - versionSet.AdvertisesDeprecatedApiVersion( apiVersion ); - break; - } - } - } - - private sealed class SingleItemReadOnlyList : IReadOnlyList - { - private readonly ApiVersion item; - - internal SingleItemReadOnlyList( ApiVersion item ) => this.item = item; - - public ApiVersion this[int index] => index == 0 ? item : throw new IndexOutOfRangeException(); - - public int Count => 1; - - public IEnumerator GetEnumerator() - { - yield return item; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private sealed class ReportApiVersionsConvention : IReportApiVersions - { - public ApiVersionMapping Mapping => ApiVersionMapping.None; - - public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) { } - } - - private sealed class Convention : IApiVersionProvider - { - private static ReportApiVersionsConvention? reportApiVersions; - - private Convention( ApiVersion version, ApiVersionProviderOptions options ) - { - Versions = new SingleItemReadOnlyList( version ); - Options = options; - } - - public ApiVersionProviderOptions Options { get; } - - public IReadOnlyList Versions { get; } - - internal static IReportApiVersions ReportApiVersions => reportApiVersions ??= new(); - - internal static Convention HasApiVersion( ApiVersion version ) => new( version, None ); - - internal static Convention HasDeprecatedApiVersion( ApiVersion version ) => new( version, Deprecated ); - - internal static Convention MapToApiVersion( ApiVersion version ) => new( version, Mapped ); - - internal static Convention AdvertisesApiVersion( ApiVersion version ) => new( version, Advertised ); - - internal static Convention AdvertisesDeprecatedApiVersion( ApiVersion version ) => new( version, Advertised | Deprecated ); - } -} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilder.cs index 5971fe68..36af4a92 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilder.cs @@ -46,7 +46,8 @@ public VersionedEndpointRouteBuilder( protected ApiVersionSetBuilder VersionSetBuilder { get; } /// - public virtual IApplicationBuilder CreateApplicationBuilder() => routeBuilder.CreateApplicationBuilder(); + public virtual IApplicationBuilder CreateApplicationBuilder() => + routeBuilder.CreateApplicationBuilder(); /// public virtual IServiceProvider ServiceProvider => serviceProvider; @@ -55,22 +56,22 @@ public VersionedEndpointRouteBuilder( public virtual ICollection DataSources => dataSources; /// - public virtual void Add( Action convention ) => conventionBuilder.Add( convention ); + public virtual void Add( Action convention ) => + conventionBuilder.Add( convention ); - private sealed class ServiceProviderDecorator : IServiceProvider + private sealed class ServiceProviderDecorator( + IServiceProvider decorated, + ApiVersionSetBuilder versionSetBuilder ) : IServiceProvider { - private readonly IServiceProvider decorated; - private readonly ApiVersionSetBuilder versionSetBuilder; private ApiVersionSet? versionSet; - internal ServiceProviderDecorator( IServiceProvider decorated, ApiVersionSetBuilder versionSetBuilder ) - { - this.decorated = decorated; - this.versionSetBuilder = versionSetBuilder; - } - public object? GetService( Type serviceType ) { + if ( typeof( ApiVersionSetBuilder ).Equals( serviceType ) ) + { + return versionSetBuilder; + } + if ( typeof( ApiVersionSet ).Equals( serviceType ) ) { return versionSet ??= versionSetBuilder.Build(); @@ -80,17 +81,10 @@ internal ServiceProviderDecorator( IServiceProvider decorated, ApiVersionSetBuil } } - private sealed class EndpointDataSourceDecorator : EndpointDataSource + private sealed class EndpointDataSourceDecorator( + EndpointDataSource decorated, + ApiVersionSetBuilder versionSetBuilder ) : EndpointDataSource { - private readonly EndpointDataSource decorated; - private readonly ApiVersionSetBuilder versionSetBuilder; - - internal EndpointDataSourceDecorator( EndpointDataSource decorated, ApiVersionSetBuilder versionSetBuilder ) - { - this.decorated = decorated; - this.versionSetBuilder = versionSetBuilder; - } - public override IReadOnlyList Endpoints => decorated.Endpoints; public override IChangeToken GetChangeToken() => decorated.GetChangeToken(); @@ -99,14 +93,16 @@ public override IReadOnlyList GetGroupedEndpoints( RouteGroupContext c { CollateGroupApiVersions(); - // HACK: we don't have a way to pass the version set for the group down to each convention so - // decorate the service provider to allow it to be resolved. this requires rebuilding the - // current context as well. + // HACK: we don't have a way to pass the version set for the group down + // to each convention so decorate the service provider to allow it to + // be resolved. this requires rebuilding the current context as well. if ( context.ApplicationServices is not ServiceProviderDecorator ) { context = new() { - ApplicationServices = new ServiceProviderDecorator( context.ApplicationServices, versionSetBuilder ), + ApplicationServices = new ServiceProviderDecorator( + context.ApplicationServices, + versionSetBuilder ), Conventions = context.Conventions, FinallyConventions = context.FinallyConventions, Prefix = context.Prefix, @@ -116,7 +112,8 @@ public override IReadOnlyList GetGroupedEndpoints( RouteGroupContext c return decorated.GetGroupedEndpoints( context ); } - public override bool Equals( object? obj ) => ReferenceEquals( this, obj ) || ReferenceEquals( decorated, obj ); + public override bool Equals( object? obj ) => + ReferenceEquals( this, obj ) || ReferenceEquals( decorated, obj ); public override int GetHashCode() => decorated.GetHashCode(); @@ -152,7 +149,7 @@ private void CollateGroupApiVersions() for ( var k = 0; k < versions.Count; k++ ) { - add( versions[i] ); + add( versions[k] ); } } } @@ -161,19 +158,10 @@ private void CollateGroupApiVersions() } } - private sealed class EndpointDataSourceCollectionAdapter : ICollection - { - private readonly ICollection adapted; - private readonly ApiVersionSetBuilder versionSetBuilder; - - internal EndpointDataSourceCollectionAdapter( + private sealed class EndpointDataSourceCollectionAdapter( ICollection adapted, - ApiVersionSetBuilder versionSetBuilder ) - { - this.adapted = adapted; - this.versionSetBuilder = versionSetBuilder; - } - + ApiVersionSetBuilder versionSetBuilder ) : ICollection + { public int Count => adapted.Count; public bool IsReadOnly => adapted.IsReadOnly; @@ -185,7 +173,8 @@ public void Add( EndpointDataSource item ) => public bool Contains( EndpointDataSource item ) => adapted.Contains( item ); - public void CopyTo( EndpointDataSource[] array, int arrayIndex ) => adapted.CopyTo( array, arrayIndex ); + public void CopyTo( EndpointDataSource[] array, int arrayIndex ) => + adapted.CopyTo( array, arrayIndex ); public IEnumerator GetEnumerator() => adapted.GetEnumerator(); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilderFactory.cs new file mode 100644 index 00000000..04aaca1c --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilderFactory.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Builder; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +/// +/// Creates and returns a new versioned endpoint route builder. +/// +/// The inner the new instance decorates. +/// The inner the new instance decorates. +/// The associated API version set builder. +/// A new instance. +[CLSCompliant( false )] +public delegate IVersionedEndpointRouteBuilder VersionedEndpointRouteBuilderFactory( + IEndpointRouteBuilder routeBuilder, + IEndpointConventionBuilder conventionBuilder, + ApiVersionSetBuilder apiVersionSetBuilder ); \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultApiVersionReporter.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultApiVersionReporter.cs index d495c8f6..94ecb388 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultApiVersionReporter.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DefaultApiVersionReporter.cs @@ -21,7 +21,7 @@ private static void AddApiVersionHeader( IHeaderDictionary headers, string heade if ( versions.Count == 1 ) { - headers.Add( headerName, versions[0].ToString() ); + headers[headerName] = versions[0].ToString(); return; } @@ -55,7 +55,7 @@ private static void AddApiVersionHeader( IHeaderDictionary headers, string heade } } - headers.Add( headerName, headerValue.ToString() ); + headers[headerName] = headerValue.ToString(); pool.Return( array ); } } \ 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 4e29d063..89d6d51c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -3,14 +3,15 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; -using Asp.Versioning.Builder; +using Asp.Versioning.ApiExplorer; using Asp.Versioning.Routing; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using System; -using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; +using static ServiceDescriptor; +using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; /// /// Provides extension methods for the interface. @@ -49,10 +50,7 @@ public static IApiVersioningBuilder AddApiVersioning( this IServiceCollection se /// The original . public static IApiVersioningBuilder EnableApiVersionBinding( this IApiVersioningBuilder builder ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } + ArgumentNullException.ThrowIfNull( builder ); // currently required because there is no other hook. // 1. TryParse does not work because: @@ -77,44 +75,83 @@ 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 ) { - if ( services == null ) - { - throw new ArgumentNullException( nameof( services ) ); - } + ArgumentNullException.ThrowIfNull( services ); - services.TryAddSingleton(); services.TryAddSingleton(); - services.Add( Singleton( sp => sp.GetRequiredService>().Value.ApiVersionReader ) ); - services.Add( Singleton( sp => (IApiVersionParameterSource) sp.GetRequiredService>().Value.ApiVersionReader ) ); - services.Add( Singleton( 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>() ); services.TryAddEnumerable( Transient, ApiVersioningRouteOptionsSetup>() ); services.TryAddEnumerable( Singleton() ); + services.TryAddEnumerable( Singleton() ); + services.TryAddTransient(); services.Replace( WithLinkGeneratorDecorator( services ) ); TryAddProblemDetailsRfc7231Compliance( services ); - } - - // REF: https://github.com/dotnet/runtime/blob/master/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceDescriptor.cs#L125 - private static Type GetImplementationType( this ServiceDescriptor descriptor ) - { - if ( descriptor.ImplementationType != null ) - { - return descriptor.ImplementationType; - } - else if ( descriptor.ImplementationInstance != null ) - { - return descriptor.ImplementationInstance.GetType(); - } - else if ( descriptor.ImplementationFactory != null ) - { - var typeArguments = descriptor.ImplementationFactory.GetType().GenericTypeArguments; - return typeArguments[1]; - } - - throw new InvalidOperationException(); + TryAddErrorObjectJsonOptions( services ); } private static ServiceDescriptor WithLinkGeneratorDecorator( IServiceCollection services ) @@ -132,12 +169,31 @@ private static ServiceDescriptor WithLinkGeneratorDecorator( IServiceCollection if ( factory == null ) { - var decoratedType = descriptor.GetImplementationType(); - var decoratorType = typeof( ApiVersionLinkGenerator<> ).MakeGenericType( decoratedType ); + // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs#L96 + // REF: https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceDescriptor.cs#L292 + var decoratedType = descriptor switch + { + { ImplementationType: var type } when type is not null => type, + { ImplementationInstance: var instance } when instance is not null => instance.GetType(), + _ => throw new InvalidOperationException(), + }; services.Replace( Describe( decoratedType, decoratedType, lifetime ) ); - return Describe( typeof( LinkGenerator ), decoratorType, lifetime ); + LinkGenerator NewFactory( IServiceProvider serviceProvider ) + { + var instance = (LinkGenerator) serviceProvider.GetRequiredService( decoratedType! ); + var source = serviceProvider.GetRequiredService(); + + if ( source.VersionsByUrl() ) + { + instance = new ApiVersionLinkGenerator( instance ); + } + + return instance; + } + + return Describe( typeof( LinkGenerator ), NewFactory, lifetime ); } else { @@ -158,6 +214,8 @@ LinkGenerator NewFactory( IServiceProvider serviceProvider ) } } + // TODO: Remove in .NET 9.0 or .NET 8.0 patch + // BUG: https://github.com/dotnet/aspnetcore/issues/52577 private static void TryAddProblemDetailsRfc7231Compliance( IServiceCollection services ) { var descriptor = services.FirstOrDefault( IsDefaultProblemDetailsWriter ); @@ -180,4 +238,49 @@ static bool IsDefaultProblemDetailsWriter( ServiceDescriptor serviceDescriptor ) static Rfc7231ProblemDetailsWriter NewProblemDetailsWriter( IServiceProvider serviceProvider, Type decoratedType ) => 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]; + + if ( !hasErrorObjects && + service.ServiceType == serviceType && + implementationType.IsAssignableFrom( service.ImplementationType ) ) + { + hasErrorObjects = true; + + if ( hasErrorObjectsJsonConfig ) + { + break; + } + } + else if ( service.ServiceType == markerType ) + { + hasErrorObjectsJsonConfig = true; + + if ( hasErrorObjects ) + { + break; + } + } + } + + if ( hasErrorObjects && !hasErrorObjectsJsonConfig ) + { + services.Configure( DefaultErrorObjectJsonConfig ); + } + } + +// 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 diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs new file mode 100644 index 00000000..b02d1260 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs @@ -0,0 +1,215 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +// Ignore Spelling: Serializer +namespace Asp.Versioning; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System.Text.Json; +using System.Text.Json.Serialization; +using static System.Text.Json.Serialization.JsonIgnoreCondition; +using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; + +/// +/// Represents a problem details writer that outputs error objects in responses. +/// +/// This enables backward compatibility by converting into Error Objects that +/// conform to the Error Responses +/// in the Microsoft REST API Guidelines and +/// OData Error Responses. +[CLSCompliant( false )] +public partial class ErrorObjectWriter : IProblemDetailsWriter +{ + private readonly JsonSerializerOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The current JSON options. + /// is null. + 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 ) + { + ArgumentNullException.ThrowIfNull( context ); + + var type = context.ProblemDetails.Type; + + return type == ProblemDetailsDefaults.Unsupported.Type || + type == ProblemDetailsDefaults.Unspecified.Type || + type == ProblemDetailsDefaults.Invalid.Type || + type == ProblemDetailsDefaults.Ambiguous.Type; + } + + /// + public virtual ValueTask WriteAsync( ProblemDetailsContext context ) + { + ArgumentNullException.ThrowIfNull( context ); + + var response = context.HttpContext.Response; + var obj = new ErrorObject( context.ProblemDetails ); + + OnBeforeWrite( context, ref obj ); + + return new( response.WriteAsJsonAsync( obj, options.GetTypeInfo( obj.GetType() ) ) ); + } + + /// + /// Occurs just before an error will be written. + /// + /// The current context. + /// The current error object. + /// Note to inheritors: The default implementation performs no action. + protected virtual void OnBeforeWrite( ProblemDetailsContext context, ref ErrorObject errorObject ) + { + } + +#pragma warning disable CA1815 // Override equals and operator equals on value types + + /// + /// Represents an error object. + /// + protected internal readonly partial struct ErrorObject + { + internal ErrorObject( ProblemDetails problemDetails ) => + Error = new( problemDetails ); + + /// + /// Gets the top-level error. + /// + /// The top-level error. + [JsonPropertyName( "error" )] + public ErrorDetail Error { get; } + } + + /// + /// Represents the error detail. + /// + protected internal readonly partial struct ErrorDetail + { + private const string CodeProperty = "code"; + private readonly ProblemDetails problemDetails; + private readonly InnerError? innerError; + private readonly Dictionary extensions = []; + + internal ErrorDetail( ProblemDetails problemDetails ) + { + this.problemDetails = problemDetails; + innerError = string.IsNullOrEmpty( problemDetails.Detail ) ? default : new InnerError( problemDetails ); + } + + /// + /// Gets or sets one of a server-defined set of error codes. + /// + /// A server-defined error code. + [JsonPropertyName( CodeProperty )] + [JsonIgnore( Condition = WhenWritingNull )] + public string? Code + { + get => problemDetails.Extensions.TryGetValue( CodeProperty, out var value ) && + value is string code ? code : default; + set + { + if ( value is null ) + { + problemDetails.Extensions.Remove( CodeProperty ); + } + else + { + problemDetails.Extensions[CodeProperty] = value; + } + } + } + + /// + /// Gets or sets the error message. + /// + /// A human-readable representation of the error. + [JsonPropertyName( "message" )] + [JsonIgnore( Condition = WhenWritingNull )] + public string? Message + { + get => problemDetails.Title; + set => problemDetails.Title = value; + } + + /// + /// Gets or sets the target of the error. + /// + /// The error target of the error. + [JsonPropertyName( "target" )] + [JsonIgnore( Condition = WhenWritingNull )] + public string? Target + { + get => problemDetails.Title; + set => problemDetails.Title = value; + } + + /// + /// Gets an object containing more specific information than the current object about the error, if any. + /// + /// The inner error or null. + [JsonPropertyName( "innerError" )] + [JsonIgnore( Condition = WhenWritingNull )] + public InnerError? InnerError => innerError; + + /// + /// Gets a collection of extension key/value pair members. + /// + /// A collection of extension key/value pair members. + [JsonExtensionData] + public IDictionary Extensions => extensions; + } + + /// + /// Represents an inner error. + /// + protected internal readonly partial struct InnerError + { + private readonly ProblemDetails problemDetails; + private readonly Dictionary extensions = []; + + internal InnerError( ProblemDetails problemDetails ) => + this.problemDetails = problemDetails; + + /// + /// Gets or sets the inner error message. + /// + /// The inner error message. + [JsonPropertyName( "message" )] + [JsonIgnore( Condition = WhenWritingNull )] + public string? Message + { + get => problemDetails.Detail; + set => problemDetails.Detail = value; + } + + /// + /// Gets a collection of extension key/value pair members. + /// + /// A collection of extension key/value pair members. + [JsonExtensionData] + public IDictionary Extensions => extensions; + } + + [JsonSerializable( typeof( ErrorObject ) )] + internal sealed partial class ErrorObjectJsonContext : JsonSerializerContext + { + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Format.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Format.cs new file mode 100644 index 00000000..060138aa --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Format.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Text; + +internal static class Format +{ + internal static readonly CompositeFormat MultipleDifferentApiVersionsRequested = CompositeFormat.Parse( CommonSR.MultipleDifferentApiVersionsRequested ); + internal static readonly CompositeFormat NoVersionSet = CompositeFormat.Parse( SR.NoVersionSet ); + internal static readonly CompositeFormat InvalidMediaTypeTemplate = CompositeFormat.Parse( CommonSR.InvalidMediaTypeTemplate ); + internal static readonly CompositeFormat UnsetRequestDelegate = CompositeFormat.Parse( SR.UnsetRequestDelegate ); + internal static readonly CompositeFormat VersionedResourceNotSupported = CompositeFormat.Parse( SR.VersionedResourceNotSupported ); + internal static readonly CompositeFormat InvalidDefaultApiVersion = CompositeFormat.Parse( SR.InvalidDefaultApiVersion ); + internal static readonly CompositeFormat InvalidPolicyKey = CompositeFormat.Parse( CommonSR.InvalidPolicyKey ); +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/HeaderApiVersionReader.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/HeaderApiVersionReader.cs index 674cb196..96b90c63 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/HeaderApiVersionReader.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/HeaderApiVersionReader.cs @@ -14,10 +14,7 @@ public partial class HeaderApiVersionReader /// public virtual IReadOnlyList Read( HttpRequest request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull( request ); var count = HeaderNames.Count; @@ -73,7 +70,7 @@ public virtual IReadOnlyList Read( HttpRequest request ) if ( versions == null ) { - return version == null ? Array.Empty() : new[] { version }; + return version == null ? [] : [version]; } return versions.ToArray(); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpContextExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpContextExtensions.cs index e504004c..60167f90 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpContextExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpContextExtensions.cs @@ -18,10 +18,7 @@ public static class HttpContextExtensions /// The current API versioning feature. public static IApiVersioningFeature ApiVersioningFeature( this HttpContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var feature = context.Features.Get(); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpRequestExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpRequestExtensions.cs index 209d5832..6ce309bd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpRequestExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpRequestExtensions.cs @@ -3,9 +3,9 @@ namespace Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using System.ComponentModel; +using RoutePattern = Microsoft.AspNetCore.Routing.Patterns.RoutePattern; /// /// Provides extension methods for . @@ -16,6 +16,7 @@ public static class HttpRequestExtensions /// /// Attempts to get the API version from current request path using the provided patterns. /// + /// The type of read-only list. /// The current HTTP request. /// The read-only list of /// patterns to evaluate. @@ -23,16 +24,14 @@ public static class HttpRequestExtensions /// The raw API version, if retrieved. /// True if the raw API version was retrieved; otherwise, false. [EditorBrowsable( EditorBrowsableState.Never )] - public static bool TryGetApiVersionFromPath( + public static bool TryGetApiVersionFromPath( this HttpRequest request, - IReadOnlyList routePatterns, + TList routePatterns, string constraintName, [NotNullWhen( true )] out string? apiVersion ) + where TList : IReadOnlyList { - if ( routePatterns == null ) - { - throw new ArgumentNullException( nameof( routePatterns ) ); - } + ArgumentNullException.ThrowIfNull( routePatterns ); if ( string.IsNullOrEmpty( constraintName ) || routePatterns.Count == 0 ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs index 85b375fa..9b8ed20a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Http/HttpResponseExtensions.cs @@ -23,15 +23,8 @@ public static class HttpResponseExtensions [CLSCompliant( false )] public static void WriteSunsetPolicy( this HttpResponse response, SunsetPolicy sunsetPolicy ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } - - if ( sunsetPolicy == null ) - { - throw new ArgumentNullException( nameof( sunsetPolicy ) ); - } + ArgumentNullException.ThrowIfNull( response ); + ArgumentNullException.ThrowIfNull( sunsetPolicy ); var headers = response.Headers; @@ -45,7 +38,7 @@ public static void WriteSunsetPolicy( this HttpResponse response, SunsetPolicy s if ( sunsetPolicy.Date.HasValue ) { - headers.Add( Sunset, sunsetPolicy.Date.Value.ToString( "r" ) ); + headers[Sunset] = sunsetPolicy.Date.Value.ToString( "r" ); } AddLinkHeaders( headers, sunsetPolicy.Links ); @@ -60,16 +53,7 @@ private static void AddLinkHeaders( IHeaderDictionary headers, IList @@ -81,10 +65,7 @@ private static void AddLinkHeaders( IHeaderDictionary headers, IList public static void AddApiVersionToContentType( this HttpResponse response, string name ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } + ArgumentNullException.ThrowIfNull( response ); if ( response.StatusCode < 200 && response.StatusCode > 299 ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/IApiVersionSelector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/IApiVersionSelector.cs new file mode 100644 index 00000000..70f1a38b --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/IApiVersionSelector.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using Microsoft.AspNetCore.Http; + +/// +/// content> +/// Provides additional implementation specific to ASP.NET Core. +/// +[CLSCompliant( false )] +public partial interface IApiVersionSelector +{ + /// + /// Selects an API version given the specified HTTP request and API version information. + /// + /// The current HTTP request to select the version for. + /// The model to select the version from. + /// The token that can be used to cancel the operation. + /// A task containing the selected API version. + ValueTask SelectVersionAsync( + HttpRequest request, + ApiVersionModel model, + CancellationToken cancellationToken ) => + ValueTask.FromResult( SelectVersion( request, model ) ); +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/IApiVersionSelectorExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/IApiVersionSelectorExtensions.cs new file mode 100644 index 00000000..a88a8e13 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/IApiVersionSelectorExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using Microsoft.AspNetCore.Http; + +/// +/// Provides extension methods for . +/// +[CLSCompliant( false )] +public static class IApiVersionSelectorExtensions +{ + /// + /// Selects an API version given the specified API version information. + /// + /// The extended . + /// The model to select the version from. + /// The selected API version. + public static ApiVersion SelectVersion( this IApiVersionSelector selector, ApiVersionModel model ) + { + ArgumentNullException.ThrowIfNull( selector ); + var context = new DefaultHttpContext(); + return selector.SelectVersion( context.Request, model ); + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReader.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReader.cs index 19d3a291..9a040c6f 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReader.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReader.cs @@ -13,10 +13,7 @@ public partial class MediaTypeApiVersionReader /// public virtual IReadOnlyList Read( HttpRequest request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull( request ); var headers = request.GetTypedHeaders(); var contentType = headers.ContentType; @@ -25,18 +22,18 @@ public virtual IReadOnlyList Read( HttpRequest request ) if ( accept is null || ReadAcceptHeader( accept ) is not string otherVersion ) { - return version is null ? Array.Empty() : new[] { version }; + return version is null ? [] : [version]; } var comparer = StringComparer.OrdinalIgnoreCase; if ( version is null || comparer.Equals( version, otherVersion ) ) { - return new[] { otherVersion }; + return [otherVersion]; } - return comparer.Compare( version, otherVersion ) <= 0 ? - new[] { version, otherVersion } : - new[] { otherVersion, version }; + return comparer.Compare( version, otherVersion ) <= 0 + ? [version, otherVersion] + : [otherVersion, version]; } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReaderBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReaderBuilder.cs index 45655770..92bcbd12 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReaderBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReaderBuilder.cs @@ -22,20 +22,19 @@ public partial class MediaTypeApiVersionReaderBuilder /// If a value is not specified, there is expected to be a single template parameter. /// The current . /// The template syntax is the same used by route templates; however, constraints are not supported. +#pragma warning disable IDE0079 #pragma warning disable CA1716 // Identifiers should not match keywords public virtual MediaTypeApiVersionReaderBuilder Template( string template, string? parameterName = default ) #pragma warning restore CA1716 // Identifiers should not match keywords +#pragma warning restore IDE0079 { - if ( string.IsNullOrEmpty( template ) ) - { - throw new ArgumentNullException( nameof( template ) ); - } + ArgumentException.ThrowIfNullOrEmpty( template ); var routePattern = RoutePatternFactory.Parse( template ); if ( string.IsNullOrEmpty( parameterName ) && routePattern.Parameters.Count > 1 ) { - var message = string.Format( CultureInfo.CurrentCulture, CommonSR.InvalidMediaTypeTemplate, template ); + var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidMediaTypeTemplate, template ); throw new ArgumentException( message, nameof( template ) ); } @@ -47,7 +46,7 @@ public partial class MediaTypeApiVersionReaderBuilder return this; } - private static IReadOnlyList ReadMediaTypePattern( + private static string[] ReadMediaTypePattern( IReadOnlyList mediaTypes, TemplateMatcher matcher, string? parameterName ) @@ -104,11 +103,6 @@ private static IReadOnlyList ReadMediaTypePattern( } } - if ( version is null ) - { - return Array.Empty(); - } - - return versions is null ? new[] { version } : versions.ToArray(); + return ToArray( ref version, versions ); } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/QueryStringApiVersionReader.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/QueryStringApiVersionReader.cs index 29d0d03e..144f5b34 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/QueryStringApiVersionReader.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/QueryStringApiVersionReader.cs @@ -14,10 +14,7 @@ public partial class QueryStringApiVersionReader /// public virtual IReadOnlyList Read( HttpRequest request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull( request ); var count = ParameterNames.Count; @@ -69,7 +66,7 @@ public virtual IReadOnlyList Read( HttpRequest request ) if ( versions == null ) { - return version == null ? Array.Empty() : new[] { version }; + return version == null ? [] : [version]; } return versions.ToArray(); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Rfc7231ProblemDetailsWriter.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Rfc7231ProblemDetailsWriter.cs index 261060d6..3b380197 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Rfc7231ProblemDetailsWriter.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Rfc7231ProblemDetailsWriter.cs @@ -9,7 +9,7 @@ namespace Asp.Versioning; internal sealed class Rfc7231ProblemDetailsWriter : IProblemDetailsWriter { private static readonly MediaTypeHeaderValue jsonMediaType = new( "application/json" ); - private static readonly MediaTypeHeaderValue problemDetailsJsonMediaType = new( "application/problem+json" ); + private static readonly MediaTypeHeaderValue problemDetailsJsonMediaType = new( ProblemDetailsDefaults.MediaType.Json ); private readonly IProblemDetailsWriter decorated; public Rfc7231ProblemDetailsWriter( IProblemDetailsWriter decorated ) => this.decorated = decorated; @@ -36,8 +36,11 @@ public bool CanWrite( ProblemDetailsContext context ) { var acceptHeaderValue = acceptHeader[i]; - if ( jsonMediaType.IsSubsetOf( acceptHeaderValue ) || - problemDetailsJsonMediaType.IsSubsetOf( acceptHeaderValue ) ) + // TODO: the logic is inverted in .NET 8. remove when fixed + // BUG: https://github.com/dotnet/aspnetcore/issues/52577 + // REF: https://github.com/dotnet/aspnetcore/blob/release/8.0/src/Http/Http.Extensions/src/DefaultProblemDetailsWriter.cs#L38 + if ( acceptHeaderValue.IsSubsetOf( jsonMediaType ) || + acceptHeaderValue.IsSubsetOf( problemDetailsJsonMediaType ) ) { return true; } diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/AmbiguousApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/AmbiguousApiVersionEndpoint.cs index 4cde72f6..80eb5456 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/AmbiguousApiVersionEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/AmbiguousApiVersionEndpoint.cs @@ -19,7 +19,7 @@ private static Task OnExecute( HttpContext context, ILogger logger ) { var apiVersions = context.ApiVersioningFeature().RawRequestedApiVersions; - logger.ApiVersionAmbiguous( apiVersions.ToArray() ); + logger.ApiVersionAmbiguous( [.. apiVersions] ); context.Response.StatusCode = StatusCodes.Status400BadRequest; if ( !context.TryGetProblemDetailsService( out var problemDetails ) ) @@ -29,10 +29,10 @@ private static Task OnExecute( HttpContext context, ILogger logger ) var detail = string.Format( CultureInfo.CurrentCulture, - CommonSR.MultipleDifferentApiVersionsRequested, + Format.MultipleDifferentApiVersionsRequested, string.Join( ", ", apiVersions ) ); - return problemDetails.WriteAsync( + return problemDetails.TryWriteAsync( EndpointProblem.New( context, ProblemDetailsDefaults.Ambiguous, diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionLinkGenerator.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionLinkGenerator.cs index c3040163..abdaa172 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionLinkGenerator.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionLinkGenerator.cs @@ -65,7 +65,7 @@ public class ApiVersionLinkGenerator : LinkGenerator public override string? GetUriByAddress( TAddress address, RouteValueDictionary values, - string? scheme, + string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, @@ -73,15 +73,8 @@ public class ApiVersionLinkGenerator : LinkGenerator private static void AddApiVersionRouteValueIfNecessary( HttpContext httpContext, RouteValueDictionary values ) { - if ( httpContext == null ) - { - throw new ArgumentNullException( nameof( httpContext ) ); - } - - if ( values == null ) - { - throw new ArgumentNullException( nameof( values ) ); - } + ArgumentNullException.ThrowIfNull( httpContext ); + ArgumentNullException.ThrowIfNull( values ); var feature = httpContext.ApiVersioningFeature(); var key = feature.RouteParameter; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs index 47fdad17..e43223aa 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs @@ -2,6 +2,7 @@ namespace Asp.Versioning.Routing; +using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; @@ -9,34 +10,46 @@ namespace Asp.Versioning.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Buffers; +using System.Collections.Frozen; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using static Asp.Versioning.ApiVersionMapping; +using static System.Text.RegularExpressions.RegexOptions; /// /// Represents the matcher policy for API versions. /// [CLSCompliant( false )] -public sealed class ApiVersionMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy, INodeBuilderPolicy +public sealed partial class ApiVersionMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy, INodeBuilderPolicy { private readonly IOptions options; private readonly IApiVersionParser apiVersionParser; + private readonly ApiVersionCollator collator; private readonly ILogger logger; /// /// Initializes a new instance of the class. /// /// The parser used to parse API versions. + /// The sequence of + /// API version metadata collation providers.. /// The options associated with the matcher policy. /// The logger used by the matcher policy. public ApiVersionMatcherPolicy( IApiVersionParser apiVersionParser, + IEnumerable providers, IOptions options, ILogger logger ) { - this.apiVersionParser = apiVersionParser ?? throw new ArgumentNullException( nameof( apiVersionParser ) ); - this.options = options ?? throw new ArgumentNullException( nameof( options ) ); - this.logger = logger ?? throw new ArgumentNullException( nameof( logger ) ); + ArgumentNullException.ThrowIfNull( apiVersionParser ); + ArgumentNullException.ThrowIfNull( providers ); + ArgumentNullException.ThrowIfNull( options ); + ArgumentNullException.ThrowIfNull( logger ); + + this.apiVersionParser = apiVersionParser; + collator = new( providers, options ); + this.options = options; + this.logger = logger; } /// @@ -51,10 +64,7 @@ public ApiVersionMatcherPolicy( /// public bool AppliesToEndpoints( IReadOnlyList endpoints ) { - if ( endpoints == null ) - { - throw new ArgumentNullException( nameof( endpoints ) ); - } + ArgumentNullException.ThrowIfNull( endpoints ); for ( var i = 0; i < endpoints.Count; i++ ) { @@ -68,24 +78,17 @@ public bool AppliesToEndpoints( IReadOnlyList endpoints ) } /// - public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) + public async Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) { - if ( httpContext == null ) - { - throw new ArgumentNullException( nameof( httpContext ) ); - } - - if ( candidates == null ) - { - throw new ArgumentNullException( nameof( candidates ) ); - } + ArgumentNullException.ThrowIfNull( httpContext ); + ArgumentNullException.ThrowIfNull( candidates ); var feature = httpContext.ApiVersioningFeature(); var apiVersion = feature.RequestedApiVersion; if ( apiVersion == null && Options.AssumeDefaultVersionWhenUnspecified ) { - apiVersion = TrySelectApiVersion( httpContext, candidates ); + apiVersion = await TrySelectApiVersionAsync( httpContext, candidates ).ConfigureAwait( false ); feature.RequestedApiVersion = apiVersion; } @@ -93,34 +96,33 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) if ( !matched && hasCandidates && !DifferByRouteConstraintsOnly( candidates ) ) { - var builder = new ClientErrorEndpointBuilder( feature, candidates, logger ); + var builder = new ClientErrorEndpointBuilder( feature, candidates, Options, logger ); httpContext.SetEndpoint( builder.Build() ); } - - return Task.CompletedTask; } /// public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList edges ) { - if ( edges == null ) - { - throw new ArgumentNullException( nameof( edges ) ); - } + ArgumentNullException.ThrowIfNull( edges ); - const int NumberOfRejectionEndpoints = 4; var rejection = new RouteDestination( exitDestination ); - var capacity = edges.Count - NumberOfRejectionEndpoints; + var capacity = edges.Count - EdgeBuilder.NumberOfRejectionEndpoints; var destinations = new Dictionary( capacity ); var source = ApiVersionSource; - var versionsByUrl = source.VersionsByUrl(); - var routePatterns = default( List ); + var supported = default( SortedSet ); + var deprecated = default( SortedSet ); + var routePatterns = default( RoutePattern[] ); for ( var i = 0; i < edges.Count; i++ ) { var edge = edges[i]; var state = (EdgeKey) edge.State; - var version = state.ApiVersion; + + if ( Options.ReportApiVersions ) + { + Collate( state.Metadata, ref supported, ref deprecated ); + } switch ( state.EndpointType ) { @@ -133,6 +135,9 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList 0 ) - { - routePatterns ??= new(); - routePatterns.AddRange( state.RoutePatterns ); - } - - destinations.Add( version, edge.Destination ); + // the route patterns provided to each edge is a + // singleton so any edge will do + routePatterns ??= [.. state.RoutePatterns]; + destinations.Add( state.ApiVersion, edge.Destination ); break; } } return new ApiVersionPolicyJumpTable( rejection, - destinations, - routePatterns ?? (IReadOnlyList) Array.Empty(), + destinations.ToFrozenDictionary( destinations.Comparer ), + NewPolicyFeature( supported, deprecated ), + routePatterns ?? [], apiVersionParser, source, Options ); @@ -166,16 +169,13 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList public IReadOnlyList GetEdges( IReadOnlyList endpoints ) { - if ( endpoints == null ) - { - throw new ArgumentNullException( nameof( endpoints ) ); - } + ArgumentNullException.ThrowIfNull( endpoints ); var capacity = endpoints.Count; var builder = new EdgeBuilder( capacity, ApiVersionSource, Options, logger ); var versions = new SortedSet(); - var neutralEndpoints = default( List ); - var versionedEndpoints = new (RouteEndpoint, ApiVersionModel)[capacity]; + var neutralEndpoints = default( List<(RouteEndpoint, ApiVersionMetadata)> ); + var versionedEndpoints = new (RouteEndpoint, ApiVersionModel, ApiVersionMetadata)[capacity]; var count = 0; for ( var i = 0; i < endpoints.Count; i++ ) @@ -190,14 +190,14 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints if ( model.IsApiVersionNeutral ) { - builder.Add( endpoint, ApiVersion.Neutral ); - neutralEndpoints ??= new(); - neutralEndpoints.Add( endpoint ); + builder.Add( endpoint, ApiVersion.Neutral, metadata ); + neutralEndpoints ??= []; + neutralEndpoints.Add( (endpoint, metadata) ); } else { builder.Add( endpoint ); - versionedEndpoints[count++] = (endpoint, model); + versionedEndpoints[count++] = (endpoint, model, metadata); versions.AddRange( model.DeclaredApiVersions ); } } @@ -206,24 +206,29 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints { for ( var j = 0; j < count; j++ ) { - var (endpoint, model) = versionedEndpoints[j]; + var (endpoint, model, metadata) = versionedEndpoints[j]; var mappedWithImplementation = model.ImplementedApiVersions.Contains( version ); if ( mappedWithImplementation ) { - builder.Add( endpoint, version ); + builder.Add( endpoint, version, metadata ); } } + } - if ( neutralEndpoints is null ) - { - continue; - } + if ( neutralEndpoints != null ) + { + var allVersions = collator.Items; // add an edge for all known versions because version-neutral endpoints can map to any api version - for ( var j = 0; j < neutralEndpoints.Count; j++ ) + for ( var i = 0; i < neutralEndpoints.Count; i++ ) { - builder.Add( neutralEndpoints[j], version ); + var (endpoint, metadata) = neutralEndpoints[i]; + + for ( var j = 0; j < allVersions.Count; j++ ) + { + builder.Add( endpoint, allVersions[j], metadata ); + } } } @@ -258,7 +263,7 @@ private static bool DifferByRouteConstraintsOnly( CandidateSet candidates ) // // but 3.0 is requested, 400 should be returned if we made it this far const string ReplacementPattern = "{$1}"; - var pattern = new Regex( "{([^:]+):[^}]+}", RegexOptions.Singleline | RegexOptions.IgnoreCase ); + var pattern = RouteConstraintRegex(); var comparer = StringComparer.OrdinalIgnoreCase; string? template = default; string? normalizedTemplate = default; @@ -293,6 +298,77 @@ private static bool DifferByRouteConstraintsOnly( CandidateSet candidates ) return false; } + private static void Collate( + ApiVersionMetadata metadata, + ref SortedSet? supported, + ref SortedSet? deprecated ) + { + var model = metadata.Map( Implicit | Explicit ); + var versions = model.SupportedApiVersions; + + if ( versions.Count > 0 ) + { + supported ??= []; + + for ( var j = 0; j < versions.Count; j++ ) + { + supported.Add( versions[j] ); + } + } + + versions = model.DeprecatedApiVersions; + + if ( versions.Count == 0 ) + { + return; + } + + deprecated ??= []; + + for ( var j = 0; j < versions.Count; j++ ) + { + deprecated.Add( versions[j] ); + } + } + + private static ApiVersionPolicyFeature? NewPolicyFeature( + SortedSet? supported, + SortedSet? deprecated ) + { + // this is a best guess effort at collating all supported and deprecated + // versions for an api when unmatched and it needs to be reported. it's + // impossible to sure as there is no way to correlate an arbitrary + // request url by endpoint or name. the routing system will build a tree + // based on the route template before the jump table policy is created, + // which provides a natural method of grouping. manual, contrived tests + // demonstrated that were the results were correctly collated together. + // it is possible there is an edge case that isn't covered, but it's + // unclear what that would look like. one or more test cases should be + // added to document that if discovered + ApiVersionModel model; + + if ( supported == null ) + { + if ( deprecated == null ) + { + return default; + } + + model = new( Enumerable.Empty(), deprecated ); + } + else if ( deprecated == null ) + { + model = new( supported, Enumerable.Empty() ); + } + else + { + deprecated.ExceptWith( supported ); + model = new( supported, deprecated ); + } + + return new( new( model, model ) ); + } + private static (bool Matched, bool HasCandidates) MatchApiVersion( CandidateSet candidates, ApiVersion? apiVersion ) { var total = candidates.Count; @@ -376,7 +452,7 @@ private static (bool Matched, bool HasCandidates) MatchApiVersion( CandidateSet return (matched, hasCandidates); } - private ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet candidates ) + private ValueTask TrySelectApiVersionAsync( HttpContext httpContext, CandidateSet candidates ) { var models = new List( capacity: candidates.Count ); @@ -396,24 +472,20 @@ private ApiVersion TrySelectApiVersion( HttpContext httpContext, CandidateSet ca } } - return ApiVersionSelector.SelectVersion( httpContext.Request, models.Aggregate() ); + return ApiVersionSelector.SelectVersionAsync( + httpContext.Request, + models.Aggregate(), + httpContext.RequestAborted ); } bool INodeBuilderPolicy.AppliesToEndpoints( IReadOnlyList endpoints ) => !ContainsDynamicEndpoints( endpoints ) && AppliesToEndpoints( endpoints ); - private readonly struct Match + private readonly struct Match( int index, int score, bool isExplicit ) { - internal readonly int Index; - internal readonly int Score; - internal readonly bool IsExplicit; - - internal Match( int index, int score, bool isExplicit ) - { - Index = index; - Score = score; - IsExplicit = isExplicit; - } + internal readonly int Index = index; + internal readonly int Score = score; + internal readonly bool IsExplicit = isExplicit; internal int CompareTo( in Match other ) { @@ -421,4 +493,89 @@ internal int CompareTo( in Match other ) return result == 0 ? IsExplicit.CompareTo( other.IsExplicit ) : result; } } + + private sealed class ApiVersionCollator( + IEnumerable providers, + IOptions options ) + { + private readonly IApiVersionMetadataCollationProvider[] providers = providers.ToArray(); + private readonly object syncRoot = new(); + 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 < providers.Length; i++ ) + { + providers[i].Execute( context ); + } + + var results = context.Results; + var versions = new SortedSet(); + + for ( var i = 0; i < results.Count; i++ ) + { + var model = results[i].Map( Explicit | Implicit ); + var declared = model.DeclaredApiVersions; + + for ( var j = 0; j < declared.Count; j++ ) + { + versions.Add( declared[j] ); + } + } + + if ( versions.Count == 0 ) + { + versions.Add( options.Value.DefaultApiVersion ); + } + + items = versions.ToArray(); + version = currentVersion; + } + + return items; + } + } + + private int ComputeVersion() => + providers.Length switch + { + 0 => 0, + 1 => providers[0].Version, + _ => ComputeVersion( providers ), + }; + + private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers ) + { + var hash = default( HashCode ); + + for ( var i = 0; i < providers.Length; i++ ) + { + hash.Add( providers[i].Version ); + } + + return hash.ToHashCode(); + } + } + + [GeneratedRegex( "{([^:]+):[^}]+}", IgnoreCase | Singleline )] + private static partial Regex RouteConstraintRegex(); } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyFeature.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyFeature.cs new file mode 100644 index 00000000..d6ca9ed7 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyFeature.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Routing; + +internal sealed class ApiVersionPolicyFeature +{ + public ApiVersionPolicyFeature( ApiVersionMetadata metadata ) => Metadata = metadata; + + public ApiVersionMetadata Metadata { get; } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs index 950c6cd8..24bb4a23 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs @@ -6,32 +6,38 @@ namespace Asp.Versioning.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Net.Http.Headers; +using System.Collections.Frozen; using System.Runtime.CompilerServices; internal sealed class ApiVersionPolicyJumpTable : PolicyJumpTable { private readonly bool versionsByUrl; + private readonly bool versionsByUrlOnly; private readonly bool versionsByMediaTypeOnly; private readonly RouteDestination rejection; - private readonly IReadOnlyDictionary destinations; - private readonly IReadOnlyList routePatterns; + private readonly FrozenDictionary destinations; + private readonly ApiVersionPolicyFeature? policyFeature; + private readonly RoutePattern[] routePatterns; private readonly IApiVersionParser parser; private readonly ApiVersioningOptions options; internal ApiVersionPolicyJumpTable( RouteDestination rejection, - IReadOnlyDictionary destinations, - IReadOnlyList routePatterns, + FrozenDictionary destinations, + ApiVersionPolicyFeature? policyFeature, + RoutePattern[] routePatterns, IApiVersionParser parser, IApiVersionParameterSource source, ApiVersioningOptions options ) { this.rejection = rejection; this.destinations = destinations; + this.policyFeature = policyFeature; this.routePatterns = routePatterns; this.parser = parser; this.options = options; - versionsByUrl = routePatterns.Count > 0; + versionsByUrl = routePatterns.Length > 0; + versionsByUrlOnly = source.VersionsByUrl( allowMultipleLocations: false ); versionsByMediaTypeOnly = source.VersionsByMediaType( allowMultipleLocations: false ); } @@ -40,6 +46,7 @@ public override int GetDestination( HttpContext httpContext ) var request = httpContext.Request; var feature = httpContext.ApiVersioningFeature(); var apiVersions = new List( capacity: feature.RawRequestedApiVersions.Count + 1 ); + var addedFromUrl = false; apiVersions.AddRange( feature.RawRequestedApiVersions ); @@ -48,6 +55,7 @@ public override int GetDestination( HttpContext httpContext ) DoesNotContainApiVersion( apiVersions, rawApiVersion ) ) { apiVersions.Add( rawApiVersion ); + addedFromUrl = apiVersions.Count == apiVersions.Capacity; } int destination; @@ -61,15 +69,18 @@ public override int GetDestination( HttpContext httpContext ) return destination; } - // 2. short-circuit if a default version cannot be assumed - if ( !options.AssumeDefaultVersionWhenUnspecified ) + // 2. IApiVersionSelector cannot be used yet because there are no candidates that an + // aggregated version model can be computed from to select the 'default' API version + if ( options.AssumeDefaultVersionWhenUnspecified ) { - return rejection.Unspecified; // 400 + return rejection.AssumeDefault; } - // 3. IApiVersionSelector cannot be used yet because there are no candidates that an - // aggregated version model can be computed from to select the 'default' API version - return rejection.AssumeDefault; + // 3. unspecified + return versionsByUrlOnly + /* 404 */ ? rejection.Exit + /* 400 */ : rejection.Unspecified; + case 1: rawApiVersion = apiVersions[0]; @@ -78,6 +89,11 @@ public override int GetDestination( HttpContext httpContext ) if ( versionsByUrl ) { feature.RawRequestedApiVersion = rawApiVersion; + + if ( versionsByUrlOnly ) + { + return rejection.Exit; // 404 + } } return rejection.Malformed; // 400 @@ -88,6 +104,8 @@ public override int GetDestination( HttpContext httpContext ) return destination; } + httpContext.Features.Set( policyFeature ); + if ( versionsByMediaTypeOnly ) { if ( request.Headers.ContainsKey( HeaderNames.ContentType ) ) @@ -98,11 +116,11 @@ public override int GetDestination( HttpContext httpContext ) return rejection.NotAcceptable; // 406 } - return rejection.Exit; // 404 + return addedFromUrl + /* 404 */ ? rejection.Exit + /* 400 */ : rejection.Unsupported; } - var addedFromUrl = apiVersions.Count == apiVersions.Capacity; - if ( addedFromUrl ) { feature.RawRequestedApiVersions = apiVersions; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionRouteConstraint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionRouteConstraint.cs index 943315f5..6b4cfd76 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionRouteConstraint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionRouteConstraint.cs @@ -23,10 +23,7 @@ public sealed class ApiVersionRouteConstraint : IRouteConstraint /// True if the route constraint is matched; otherwise, false. public bool Match( HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection ) { - if ( values == null ) - { - throw new ArgumentNullException( nameof( values ) ); - } + ArgumentNullException.ThrowIfNull( values ); if ( string.IsNullOrEmpty( routeKey ) ) { @@ -43,10 +40,7 @@ public bool Match( HttpContext? httpContext, IRouter? route, string routeKey, Ro return !string.IsNullOrEmpty( value ); } - if ( httpContext == null ) - { - throw new ArgumentNullException( nameof( httpContext ) ); - } + ArgumentNullException.ThrowIfNull( httpContext ); var parser = httpContext.RequestServices.GetRequiredService(); var feature = httpContext.ApiVersioningFeature(); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersioningRouteOptionsSetup.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersioningRouteOptionsSetup.cs index f41b6ed3..8d6a5755 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersioningRouteOptionsSetup.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersioningRouteOptionsSetup.cs @@ -2,8 +2,8 @@ namespace Asp.Versioning.Routing; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; +using RouteOptions = Microsoft.AspNetCore.Routing.RouteOptions; /// /// Represents the API versioning configuration for ASP.NET Core routing options. @@ -22,12 +22,9 @@ public class ApiVersioningRouteOptionsSetup : IPostConfigureOptions public virtual void PostConfigure( string? name, RouteOptions options ) { - if ( options == null ) - { - throw new ArgumentNullException( nameof( options ) ); - } + ArgumentNullException.ThrowIfNull( options ); - var key = versioningOptions.Value.RouteConstraintName; - options.ConstraintMap.Add( key, typeof( ApiVersionRouteConstraint ) ); + var token = versioningOptions.Value.RouteConstraintName; + options.SetParameterPolicy( token ); } } \ No newline at end of file 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 c61c40f0..cad20584 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs @@ -11,15 +11,18 @@ internal sealed class ClientErrorEndpointBuilder { private readonly IApiVersioningFeature feature; private readonly CandidateSet candidates; + private readonly ApiVersioningOptions options; private readonly ILogger logger; public ClientErrorEndpointBuilder( IApiVersioningFeature feature, CandidateSet candidates, + ApiVersioningOptions options, ILogger logger ) { this.feature = feature; this.candidates = candidates; + this.options = options; this.logger = logger; } @@ -30,7 +33,7 @@ public Endpoint Build() return new UnspecifiedApiVersionEndpoint( logger, GetDisplayNames() ); } - return new UnsupportedApiVersionEndpoint(); + return new UnsupportedApiVersionEndpoint( options ); } private static string DisplayName( Endpoint endpoint ) @@ -54,7 +57,7 @@ private string[] GetDisplayNames() { if ( candidates.Count == 0 ) { - return Array.Empty(); + return []; } ref readonly var candidate = ref candidates[0]; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs index 7663a809..8fb60798 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs @@ -10,13 +10,15 @@ namespace Asp.Versioning.Routing; internal sealed class EdgeBuilder { + private const int RejectionEndpointCapacity = NumberOfRejectionEndpoints + 1; + internal const int NumberOfRejectionEndpoints = 6; private readonly bool versionsByUrl; - private readonly bool unspecifiedNotAllowed; + private readonly bool unspecifiedAllowed; private readonly string constraintName; private readonly HashSet keys; private readonly Dictionary> edges; + private readonly HashSet routePatterns = new( new RoutePatternComparer() ); private EdgeKey assumeDefault = EdgeKey.AssumeDefault; - private HashSet? routePatterns; public EdgeBuilder( int capacity, @@ -25,35 +27,41 @@ public EdgeBuilder( ILogger logger ) { versionsByUrl = source.VersionsByUrl(); - unspecifiedNotAllowed = !options.AssumeDefaultVersionWhenUnspecified; + unspecifiedAllowed = options.AssumeDefaultVersionWhenUnspecified; constraintName = options.RouteConstraintName; keys = new( capacity + 1 ); - edges = new( capacity + 6 ) + edges = new( capacity + RejectionEndpointCapacity ) { [EdgeKey.Malformed] = new( capacity: 1 ) { new MalformedApiVersionEndpoint( logger ) }, [EdgeKey.Ambiguous] = new( capacity: 1 ) { new AmbiguousApiVersionEndpoint( logger ) }, [EdgeKey.Unspecified] = new( capacity: 1 ) { new UnspecifiedApiVersionEndpoint( logger ) }, - [EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint() }, - [EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint() }, + [EdgeKey.Unsupported] = new( capacity: 1 ) { new UnsupportedApiVersionEndpoint( options ) }, + [EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint( options ) }, + [EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint( options ) }, }; } - public IReadOnlyList Build() => - edges.Select( edge => new PolicyNodeEdge( edge.Key, edge.Value ) ).ToArray(); + public IReadOnlyList Build() + { + routePatterns.TrimExcess(); + return edges.Select( edge => new PolicyNodeEdge( edge.Key, edge.Value ) ).ToArray(); + } public void Add( RouteEndpoint endpoint ) { - if ( unspecifiedNotAllowed ) + if ( unspecifiedAllowed ) { - return; + Add( ref assumeDefault, endpoint ); } - - Add( ref assumeDefault, endpoint ); } - public void Add( RouteEndpoint endpoint, ApiVersion apiVersion ) + public void Add( RouteEndpoint endpoint, ApiVersion apiVersion, ApiVersionMetadata metadata ) { - var key = new EdgeKey( apiVersion ); + // use a singleton of all route patterns that version by url segment. this + // is needed to extract the value for selecting a destination in the jump + // table. any matching template will do and every edge should have the + // same list known through the application, which may be zero + var key = new EdgeKey( apiVersion, metadata, routePatterns ); Add( ref key, endpoint ); } @@ -73,18 +81,12 @@ private void Add( ref EdgeKey key, RouteEndpoint endpoint ) if ( needsRoutePattern ) { - routePatterns ??= new( new RoutePatternComparer() ); - needsRoutePattern &= routePatterns.Add( routePattern ); - - if ( needsRoutePattern ) - { - key.RoutePatterns.Add( routePattern ); - } + routePatterns.Add( routePattern ); } if ( !edges.TryGetValue( key, out var endpoints ) ) { - edges.Add( key, endpoints = new() ); + edges.Add( key, endpoints = [] ); } endpoints.Add( endpoint ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs index bc8208a6..bdc5c835 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeKey.cs @@ -9,43 +9,60 @@ namespace Asp.Versioning.Routing; internal readonly struct EdgeKey : IEquatable { public readonly ApiVersion ApiVersion; - public readonly List RoutePatterns; + public readonly ApiVersionMetadata Metadata; + public readonly HashSet RoutePatterns; public readonly EndpointType EndpointType; - private EdgeKey( EndpointType endpointType, List routePatterns ) + private EdgeKey( EndpointType endpointType, HashSet routePatterns ) { ApiVersion = ApiVersion.Default; + Metadata = ApiVersionMetadata.Empty; RoutePatterns = routePatterns; EndpointType = endpointType; } - internal EdgeKey( ApiVersion apiVersion ) + internal EdgeKey( + ApiVersion apiVersion, + ApiVersionMetadata metadata, + HashSet routePatterns ) { ApiVersion = apiVersion; - RoutePatterns = new(); + Metadata = metadata; + RoutePatterns = routePatterns; EndpointType = UserDefined; } - internal static EdgeKey Ambiguous => new( EndpointType.Ambiguous, new( capacity: 0 ) ); + internal static EdgeKey Ambiguous => new( EndpointType.Ambiguous, Set.Empty ); + + internal static EdgeKey Malformed => new( EndpointType.Malformed, Set.Empty ); - internal static EdgeKey Malformed => new( EndpointType.Malformed, new( capacity: 0 ) ); + internal static EdgeKey Unspecified => new( EndpointType.Unspecified, Set.Empty ); - internal static EdgeKey Unspecified => new( EndpointType.Unspecified, new( capacity: 0 ) ); + internal static EdgeKey Unsupported => new( EndpointType.Unsupported, Set.Empty ); - internal static EdgeKey UnsupportedMediaType => new( EndpointType.UnsupportedMediaType, new( capacity: 0 ) ); + internal static EdgeKey UnsupportedMediaType => new( EndpointType.UnsupportedMediaType, Set.Empty ); - internal static EdgeKey NotAcceptable => new( EndpointType.NotAcceptable, new( capacity: 0 ) ); + internal static EdgeKey NotAcceptable => new( EndpointType.NotAcceptable, Set.Empty ); - internal static EdgeKey AssumeDefault => new( EndpointType.AssumeDefault, new() ); + internal static EdgeKey AssumeDefault => new( EndpointType.AssumeDefault, new( new RoutePatternComparer() ) ); public bool Equals( [AllowNull] EdgeKey other ) => GetHashCode() == other.GetHashCode(); public override bool Equals( object? obj ) => obj is EdgeKey other && Equals( other ); - public override int GetHashCode() => - EndpointType == UserDefined ? - HashCode.Combine( ApiVersion, EndpointType ) : - EndpointType.GetHashCode(); + public override int GetHashCode() + { + var result = default( HashCode ); + + result.Add( EndpointType ); + + if ( EndpointType == UserDefined ) + { + result.Add( ApiVersion ); + } + + return result.ToHashCode(); + } public override string ToString() { @@ -66,4 +83,9 @@ public override string ToString() return "VER: " + value; } + + private static class Set + { + public static readonly HashSet Empty = new( capacity: 0 ); + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs index 58061d88..9a8b539b 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs @@ -4,6 +4,7 @@ namespace Asp.Versioning.Routing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.DependencyInjection; using System.Globalization; using static Asp.Versioning.ProblemDetailsDefaults; @@ -11,7 +12,6 @@ internal static class EndpointProblem { internal static ProblemDetailsContext New( HttpContext context, ProblemDetailsInfo info, string detail ) { - const string Code = nameof( Code ); var (type, title, code) = info; var newContext = new ProblemDetailsContext() { @@ -27,27 +27,45 @@ internal static ProblemDetailsContext New( HttpContext context, ProblemDetailsIn if ( string.IsNullOrEmpty( code ) ) { - newContext.ProblemDetails.Extensions[Code] = code; + newContext.ProblemDetails.Extensions[nameof( code )] = code; } return newContext; } - internal static Task UnsupportedApiVersion( HttpContext context, int statusCode ) + internal static Task UnsupportedApiVersion( + HttpContext context, + ApiVersioningOptions options, + int statusCode ) { 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) ); + } + if ( context.TryGetProblemDetailsService( out var problemDetails ) ) { var detail = string.Format( CultureInfo.CurrentCulture, - SR.VersionedResourceNotSupported, + Format.VersionedResourceNotSupported, new Uri( context.Request.GetDisplayUrl() ).SafeFullPath(), context.ApiVersioningFeature().RawRequestedApiVersion ); - return problemDetails.WriteAsync( New( context, Unsupported, detail ) ).AsTask(); + return problemDetails.TryWriteAsync( New( context, Unsupported, detail ) ).AsTask(); } return Task.CompletedTask; } + + private static Task ReportApiVersions( object state ) + { + var (reporter, response, model) = ((IReportApiVersions, HttpResponse, ApiVersionModel)) state; + reporter.Report( response, model ); + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs index f871400a..73f0c1bd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointType.cs @@ -11,4 +11,5 @@ internal enum EndpointType UnsupportedMediaType, AssumeDefault, NotAcceptable, + Unsupported, } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs index 568de2d2..c129794a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs @@ -29,11 +29,11 @@ private static Task OnExecute( HttpContext context, ILogger logger ) var detail = string.Format( CultureInfo.CurrentCulture, - SR.VersionedResourceNotSupported, + Format.VersionedResourceNotSupported, new Uri( context.Request.GetDisplayUrl() ).SafeFullPath(), requestedVersion ); - return problemDetails.WriteAsync( + return problemDetails.TryWriteAsync( EndpointProblem.New( context, ProblemDetailsDefaults.Invalid, diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs index f6b4cdbe..51731b1e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/NotAcceptableEndpoint.cs @@ -9,8 +9,12 @@ internal sealed class NotAcceptableEndpoint : Endpoint { private const string Name = "406 HTTP Not Acceptable"; - internal NotAcceptableEndpoint() : base( OnExecute, Empty, Name ) { } - - private static Task OnExecute( HttpContext context ) => - EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status406NotAcceptable ); + internal NotAcceptableEndpoint( ApiVersioningOptions options ) + : base( + context => EndpointProblem.UnsupportedApiVersion( + context, + options, + StatusCodes.Status406NotAcceptable ), + Empty, + Name ) { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ReportApiVersionsDecorator.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ReportApiVersionsDecorator.cs index 6e09df43..abd6ee83 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ReportApiVersionsDecorator.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ReportApiVersionsDecorator.cs @@ -3,35 +3,32 @@ namespace Asp.Versioning.Routing; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; internal sealed class ReportApiVersionsDecorator { private readonly RequestDelegate decorated; - private readonly ApiVersionMetadata metadata; + private readonly IReportApiVersions reporter; + private readonly ApiVersionModel model; - public ReportApiVersionsDecorator( RequestDelegate decorated, ApiVersionMetadata metadata ) + public ReportApiVersionsDecorator( RequestDelegate decorated, IReportApiVersions reporter, ApiVersionMetadata metadata ) { this.decorated = decorated; - this.metadata = metadata; + this.reporter = reporter; + model = metadata.Map( reporter.Mapping ); } public static implicit operator RequestDelegate( ReportApiVersionsDecorator decorator ) => ( context ) => { - var reporter = context.RequestServices.GetRequiredService(); - var model = decorator.metadata.Map( reporter.Mapping ); var response = context.Response; - - response.OnStarting( ReportApiVersions, (reporter, response, model) ); - + response.OnStarting( ReportApiVersions, (decorator, response) ); return decorator.decorated( context ); }; private static Task ReportApiVersions( object state ) { - var (reporter, response, model) = ((IReportApiVersions, HttpResponse, ApiVersionModel)) state; - reporter.Report( response, model ); + var (decorator, response) = ((ReportApiVersionsDecorator, HttpResponse)) state; + decorator.reporter.Report( response, decorator.model ); return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs index bda9c9d2..39dcbb64 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RouteDestination.cs @@ -8,6 +8,7 @@ internal struct RouteDestination public int Malformed; public int Ambiguous; public int Unspecified; + public int Unsupported; public int UnsupportedMediaType; public int AssumeDefault; public int NotAcceptable; @@ -18,6 +19,7 @@ public RouteDestination( int exit ) Malformed = exit; Ambiguous = exit; Unspecified = exit; + Unsupported = exit; UnsupportedMediaType = exit; AssumeDefault = exit; NotAcceptable = exit; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternComparer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternComparer.cs index 04d0a9d0..ca635586 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternComparer.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternComparer.cs @@ -2,7 +2,7 @@ namespace Asp.Versioning.Routing; -using Microsoft.AspNetCore.Routing.Patterns; +using RoutePattern = Microsoft.AspNetCore.Routing.Patterns.RoutePattern; /// /// Represents a comparer for comparing instances. diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternExtensions.cs index d7582b1d..fa467989 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/RoutePatternExtensions.cs @@ -2,8 +2,8 @@ namespace Asp.Versioning.Routing; -using Microsoft.AspNetCore.Routing.Patterns; using System.ComponentModel; +using RoutePattern = Microsoft.AspNetCore.Routing.Patterns.RoutePattern; /// /// Provides extension methods for . @@ -20,10 +20,7 @@ public static class RoutePatternExtensions /// True if the has the ; otherwise, false. public static bool HasVersionConstraint( this RoutePattern routePattern, string constraintName ) { - if ( routePattern == null ) - { - throw new ArgumentNullException( nameof( routePattern ) ); - } + ArgumentNullException.ThrowIfNull( routePattern ); if ( string.IsNullOrEmpty( constraintName ) ) { 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 dc97846f..6597aec9 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs @@ -28,7 +28,7 @@ private static Task OnExecute( HttpContext context, string[]? candidateEndpoints if ( context.TryGetProblemDetailsService( out var problemDetails ) ) { - return problemDetails.WriteAsync( + return problemDetails.TryWriteAsync( EndpointProblem.New( context, ProblemDetailsDefaults.Unspecified, diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs index 8a1661ab..1cd83a42 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedApiVersionEndpoint.cs @@ -7,10 +7,15 @@ namespace Asp.Versioning.Routing; internal sealed class UnsupportedApiVersionEndpoint : Endpoint { - private const string Name = "400 Unsupported API Version"; + private const string Name = " Unsupported API Version"; - internal UnsupportedApiVersionEndpoint() : base( OnExecute, Empty, Name ) { } - - private static Task OnExecute( HttpContext context ) => - EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status400BadRequest ); + internal UnsupportedApiVersionEndpoint( ApiVersioningOptions options ) + : base( + context => EndpointProblem.UnsupportedApiVersion( + context, + options, + options.UnsupportedApiVersionStatusCode ), + Empty, + options.UnsupportedApiVersionStatusCode + Name ) + { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs index b164bf20..1e7492e6 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnsupportedMediaTypeEndpoint.cs @@ -9,8 +9,12 @@ internal sealed class UnsupportedMediaTypeEndpoint : Endpoint { private const string Name = "415 HTTP Unsupported Media Type"; - internal UnsupportedMediaTypeEndpoint() : base( OnExecute, Empty, Name ) { } - - private static Task OnExecute( HttpContext context ) => - EndpointProblem.UnsupportedApiVersion( context, StatusCodes.Status415UnsupportedMediaType ); + internal UnsupportedMediaTypeEndpoint( ApiVersioningOptions options ) + : base( + context => EndpointProblem.UnsupportedApiVersion( + context, + options, + StatusCodes.Status415UnsupportedMediaType ), + Empty, + Name ) { } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs index 81dce11a..4a56d6b9 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs @@ -69,6 +69,24 @@ internal static string ApiVersionUnspecified { } } + /// + /// Looks up a localized string similar to A versioned API group cannot be mapped as a nested group.. + /// + internal static string CannotNestApiGroup { + get { + return ResourceManager.GetString("CannotNestApiGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A grouped API version set cannot be nested under another group.. + /// + internal static string CannotNestVersionSet { + get { + return ResourceManager.GetString("CannotNestVersionSet", resourceCulture); + } + } + /// /// Looks up a localized string similar to Conventions cannot be added after building the endpoint.. /// @@ -78,6 +96,33 @@ internal static string ConventionAddedAfterEndpointBuilt { } } + /// + /// Looks up a localized string similar to {0}.{1} is an invalid value for {2}.{3}. Did you mean to apply {4} via attribute or convention instead?. + /// + internal static string InvalidDefaultApiVersion { + get { + return ResourceManager.GetString("InvalidDefaultApiVersion", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An endpoint cannot apply multiple API version sets.. + /// + internal static string MultipleVersionSets { + get { + return ResourceManager.GetString("MultipleVersionSets", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The endpoint '{0}' does not have an associated API version set. Are you missing a call to {1} or {2}.. + /// + internal static string NoVersionSet { + get { + return ResourceManager.GetString("NoVersionSet", resourceCulture); + } + } + /// /// Looks up a localized string similar to The request type was not configured.. /// diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx index 44f12e69..dde23ba1 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx @@ -120,9 +120,29 @@ An API version is required, but was not specified. + + A versioned API group cannot be mapped as a nested group. + + + A grouped API version set cannot be nested under another group. + Conventions cannot be added after building the endpoint. + + {0}.{1} is an invalid value for {2}.{3}. Did you mean to apply {4} via attribute or convention instead? + 0 = ApiVersion +1 = Neutral +2 = ApiVersioningOptions +3 = DefaultApiVersion +4 = IApiVersionNeutral + + + An endpoint cannot apply multiple API version sets. + + + The endpoint '{0}' does not have an associated API version set. Are you missing a call to {1} or {2}. + The request type was not configured. diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/UrlSegmentApiVersionReader.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/UrlSegmentApiVersionReader.cs index 3ddb892b..ba9eec82 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/UrlSegmentApiVersionReader.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/UrlSegmentApiVersionReader.cs @@ -13,10 +13,7 @@ public partial class UrlSegmentApiVersionReader /// public virtual IReadOnlyList Read( HttpRequest request ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } + ArgumentNullException.ThrowIfNull( request ); if ( reentrant ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ValidateApiVersioningOptions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ValidateApiVersioningOptions.cs new file mode 100644 index 00000000..2abe453b --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ValidateApiVersioningOptions.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using Microsoft.Extensions.Options; +using System.Globalization; + +// ApiVersion.Neutral does not have the same meaning as IApiVersionNeutral. setting +// ApiVersioningOptions.DefaultApiVersion this value will not make all APIs version-neutral +// and will likely lead to many unexpected side effects. this is a best-effort, one-time +// validation check to help prevent people from going off the rails. if someone bypasses +// this validation by removing the check or updating the value later, then caveat emptor. +// +// REF: https://github.com/dotnet/aspnet-api-versioning/issues/1011 +#pragma warning disable CA1812 // Avoid uninstantiated internal classes +internal sealed class ValidateApiVersioningOptions : IValidateOptions +#pragma warning restore CA1812 // Avoid uninstantiated internal classes +{ + public ValidateOptionsResult Validate( string? name, ApiVersioningOptions options ) + { + if ( name is not null && name != Options.DefaultName ) + { + return ValidateOptionsResult.Skip; + } + + if ( options.DefaultApiVersion == ApiVersion.Neutral ) + { + var message = string.Format( + CultureInfo.CurrentCulture, + Format.InvalidDefaultApiVersion, + nameof( ApiVersion ), + nameof( ApiVersion.Neutral ), + nameof( ApiVersioningOptions ), + nameof( ApiVersioningOptions.DefaultApiVersion ), + nameof( IApiVersionNeutral ) ); + return ValidateOptionsResult.Fail( message ); + } + + return ValidateOptionsResult.Success; + } +} \ No newline at end of file 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 ce48b2a1..30aaf0cd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs @@ -37,10 +37,7 @@ public static class ApiDescriptionExtensions /// True if the API description is deprecated; otherwise, false. public static bool IsDeprecated( this ApiDescription apiDescription ) { - if ( apiDescription == null ) - { - throw new ArgumentNullException( nameof( apiDescription ) ); - } + ArgumentNullException.ThrowIfNull( apiDescription ); var metatadata = apiDescription.ActionDescriptor.GetApiVersionMetadata(); @@ -81,15 +78,8 @@ public static bool IsDeprecated( this ApiDescription apiDescription ) /// True if the API description was updated; otherwise, false. public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDescription apiDescription, ApiExplorerOptions options ) { - if ( apiDescription == null ) - { - throw new ArgumentNullException( nameof( apiDescription ) ); - } - - if ( options == null ) - { - throw new ArgumentNullException( nameof( options ) ); - } + ArgumentNullException.ThrowIfNull( apiDescription ); + ArgumentNullException.ThrowIfNull( options ); if ( !options.SubstituteApiVersionInUrl ) { @@ -116,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 ) { @@ -137,10 +132,7 @@ public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDe /// A new API description. public static ApiDescription Clone( this ApiDescription apiDescription ) { - if ( apiDescription == null ) - { - throw new ArgumentNullException( nameof( apiDescription ) ); - } + ArgumentNullException.ThrowIfNull( apiDescription ); var clone = new ApiDescription() { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptions.cs index 0e0aef50..b366513a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptions.cs @@ -3,6 +3,7 @@ namespace Asp.Versioning.ApiExplorer; using Asp.Versioning.Routing; +using Microsoft.AspNetCore.Http; /// /// Provides additional implementation specific to ASP.NET Core. @@ -37,7 +38,7 @@ public ApiVersion DefaultApiVersion /// The API version parameter source used to describe API version parameters. public IApiVersionParameterSource ApiVersionParameterSource { - get => parameterSource ??= ApiVersionReader.Combine( new QueryStringApiVersionReader(), new UrlSegmentApiVersionReader() ); + get => parameterSource ??= ApiVersionReader.Default; set => parameterSource = value; } @@ -47,6 +48,17 @@ public IApiVersionParameterSource ApiVersionParameterSource /// The name associated with the API version route constraint. public string RouteConstraintName { get; set; } = string.Empty; + /// + /// Gets or sets the API version selector. + /// + /// An API version selector object. + [CLSCompliant( false )] + public IApiVersionSelector ApiVersionSelector + { + get => apiVersionSelector ??= new DefaultApiVersionSelector( this ); + set => apiVersionSelector = value; + } + /// /// Gets or sets the function used to format the combination of a group name and API version. /// @@ -55,4 +67,13 @@ public IApiVersionParameterSource ApiVersionParameterSource /// The specified callback will only be invoked if a group name has been configured. The API /// version will be provided formatted according to the group name format. public FormatGroupNameCallback? FormatGroupName { get; set; } + + private sealed class DefaultApiVersionSelector : IApiVersionSelector + { + private readonly ApiExplorerOptions options; + + public DefaultApiVersionSelector( ApiExplorerOptions options ) => this.options = options; + + public ApiVersion SelectVersion( HttpRequest request, ApiVersionModel model ) => options.DefaultApiVersion; + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs index 810f3a25..1a79e0a3 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,14 +3,13 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.Extensions.Options; -using Option = Microsoft.Extensions.Options.Options; /// /// Represents a factory to create API explorer options. /// /// The type of options to create. [CLSCompliant( false )] -public class ApiExplorerOptionsFactory : IOptionsFactory where T : ApiExplorerOptions, new() +public class ApiExplorerOptionsFactory : OptionsFactory where T : ApiExplorerOptions { private readonly IOptions optionsHolder; @@ -27,11 +26,25 @@ public ApiExplorerOptionsFactory( IOptions options, IEnumerable> setups, IEnumerable> postConfigures ) - { - optionsHolder = options; - Setups = setups; - PostConfigures = postConfigures; - } + : base( setups, postConfigures ) => optionsHolder = options; + + /// + /// Initializes a new instance of the class. + /// + /// The API versioning options + /// used to create API explorer options. + /// The sequence of + /// configuration actions to run. + /// The sequence of + /// initialization actions to run. + /// The sequence of + /// validations to run. + public ApiExplorerOptionsFactory( + IOptions options, + IEnumerable> setups, + IEnumerable> postConfigures, + IEnumerable> validations ) + : base( setups, postConfigures, validations ) => optionsHolder = options; /// /// Gets the API versioning options associated with the factory. @@ -39,49 +52,28 @@ public ApiExplorerOptionsFactory( /// The API versioning options used to create API explorer options. protected ApiVersioningOptions Options => optionsHolder.Value; - /// - /// Gets the associated configuration actions to run. - /// - /// The sequence of - /// configuration actions to run. - protected IEnumerable> Setups { get; } + /// + protected override T CreateInstance( string name ) + { + var options = base.CreateInstance( name ); + CopyOptions( Options, options ); + return options; + } /// - /// Gets the associated initialization actions to run. + /// Copies the following source options to the target options. /// - /// The sequence of - /// initialization actions to run. - protected IEnumerable> PostConfigures { get; } - - /// - public virtual T Create( string name ) + /// The source options. + /// The target options. + protected static void CopyOptions( ApiVersioningOptions sourceOptions, T targetOptions ) { - var apiVersioningOptions = Options; - var options = new T() - { - AssumeDefaultVersionWhenUnspecified = apiVersioningOptions.AssumeDefaultVersionWhenUnspecified, - ApiVersionParameterSource = apiVersioningOptions.ApiVersionReader, - DefaultApiVersion = apiVersioningOptions.DefaultApiVersion, - RouteConstraintName = apiVersioningOptions.RouteConstraintName, - }; + ArgumentNullException.ThrowIfNull( targetOptions, nameof( targetOptions ) ); + ArgumentNullException.ThrowIfNull( sourceOptions, nameof( sourceOptions ) ); - foreach ( var setup in Setups ) - { - if ( setup is IConfigureNamedOptions namedSetup ) - { - namedSetup.Configure( name, options ); - } - else if ( name == Option.DefaultName ) - { - setup.Configure( options ); - } - } - - foreach ( var post in PostConfigures ) - { - post.PostConfigure( name, options ); - } - - return options; + targetOptions.AssumeDefaultVersionWhenUnspecified = sourceOptions.AssumeDefaultVersionWhenUnspecified; + targetOptions.ApiVersionParameterSource = sourceOptions.ApiVersionReader; + targetOptions.DefaultApiVersion = sourceOptions.DefaultApiVersion; + targetOptions.RouteConstraintName = sourceOptions.RouteConstraintName; + targetOptions.ApiVersionSelector = sourceOptions.ApiVersionSelector; } } \ No newline at end of file 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 db350ec3..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,32 +1,42 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + namespace Microsoft.AspNetCore.Builder; using Asp.Versioning; using Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescriptionProviderFactory { - private readonly IServiceProvider serviceProvider; - private readonly Func, IApiVersionDescriptionProvider> activator; + private readonly ISunsetPolicyManager sunsetPolicyManager; + private readonly IApiVersionMetadataCollationProvider[] providers; + private readonly IEndpointInspector endpointInspector; + private readonly IOptions options; public ApiVersionDescriptionProviderFactory( - IServiceProvider serviceProvider, - Func, IApiVersionDescriptionProvider> activator ) + ISunsetPolicyManager sunsetPolicyManager, + IEnumerable providers, + IEndpointInspector endpointInspector, + IOptions options ) { - this.serviceProvider = serviceProvider; - this.activator = activator; + this.sunsetPolicyManager = sunsetPolicyManager; + this.providers = providers.ToArray(); + this.endpointInspector = endpointInspector; + this.options = options; } public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSource ) { - var actionDescriptorCollectionProvider = serviceProvider.GetRequiredService(); - var sunsetPolicyManager = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>(); - return activator( endpointDataSource, actionDescriptorCollectionProvider, sunsetPolicyManager, options ); + var collators = new List( capacity: providers.Length + 1 ) + { + new EndpointApiVersionMetadataCollationProvider( endpointDataSource, endpointInspector ), + }; + + collators.AddRange( providers ); + + return new DefaultApiVersionDescriptionProvider( collators, sunsetPolicyManager, options ); } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionModelMetadata.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionModelMetadata.cs index 44b08cd4..bdd9020f 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionModelMetadata.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionModelMetadata.cs @@ -23,11 +23,7 @@ public sealed class ApiVersionModelMetadata : ModelMetadata public ApiVersionModelMetadata( IModelMetadataProvider modelMetadataProvider, string description ) : base( ModelMetadataIdentity.ForType( typeof( string ) ) ) { - if ( modelMetadataProvider == null ) - { - throw new ArgumentNullException( nameof( modelMetadataProvider ) ); - } - + ArgumentNullException.ThrowIfNull( modelMetadataProvider ); inner = modelMetadataProvider.GetMetadataForType( typeof( string ) ); this.description = description; } 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 c95bc1fc..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; @@ -41,7 +42,7 @@ public ApiVersionParameterDescriptionContext( ApiDescription = apiDescription ?? throw new ArgumentNullException( nameof( apiDescription ) ); ApiVersion = apiVersion ?? throw new ArgumentNullException( nameof( apiVersion ) ); ModelMetadata = modelMetadata ?? throw new ArgumentNullException( nameof( modelMetadata ) ); - optional = options.AssumeDefaultVersionWhenUnspecified && apiVersion == options.DefaultApiVersion; + optional = FirstParameterIsOptional( apiDescription, apiVersion, options ); } // intentionally an internal property so the public contract doesn't change. this will be removed @@ -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 ); @@ -440,4 +441,35 @@ private void RemoveAllParametersExcept( ApiParameterDescription parameter ) } } } + + private static bool FirstParameterIsOptional( + ApiDescription apiDescription, + ApiVersion apiVersion, + ApiExplorerOptions options ) + { + if ( !options.AssumeDefaultVersionWhenUnspecified ) + { + return false; + } + + var mapping = ApiVersionMapping.Explicit | ApiVersionMapping.Implicit; + var model = apiDescription.ActionDescriptor.GetApiVersionMetadata().Map( mapping ); + var defaultApiVersion = options.ApiVersionSelector.SelectVersion( model ); + + 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 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 ca73aefc..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,9 +1,9 @@  - 7.0.0 - 7.0.0.0 - net7.0 + 8.1.0 + 8.1.0.0 + $(DefaultTargetFramework) Asp.Versioning.ApiExplorer ASP.NET Core API Versioning API Explorer The API Explorer extensions for ASP.NET Core API Versioning. diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs index 1edc2155..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,14 +2,8 @@ namespace Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; +using Asp.Versioning.ApiExplorer.Internal; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -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. @@ -17,25 +11,23 @@ namespace Asp.Versioning.ApiExplorer; [CLSCompliant( false )] public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider { - private readonly ApiVersionDescriptionCollection collection; + private readonly ApiVersionDescriptionCollection collection; private readonly IOptions options; /// /// Initializes a new instance of the class. /// - /// The data source for endpoints. - /// The provider - /// used to enumerate the actions within an application. + /// The sequence of + /// API version metadata collation providers.. /// The manager used to resolve sunset policies. /// The container of configured /// API explorer options. public DefaultApiVersionDescriptionProvider( - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, + IEnumerable providers, ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, endpointDataSource, actionDescriptorCollectionProvider ); + collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -63,290 +55,56 @@ public DefaultApiVersionDescriptionProvider( /// A read-only list of API version descriptions. protected virtual IReadOnlyList Describe( IReadOnlyList metadata ) { - if ( metadata == null ) + ArgumentNullException.ThrowIfNull( metadata ); + + // 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 ) { - throw new ArgumentNullException( nameof( metadata ) ); + return DescriptionProvider.Describe( groupedMetadata, SunsetPolicyManager, Options ); } - var descriptions = new List( 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(); + return Array.Empty(); } - private void BucketizeApiVersions( IReadOnlyList metadata, ISet supported, ISet deprecated ) + private sealed class GroupedApiVersionMetadata : + ApiVersionMetadata, + IEquatable, + IGroupedApiVersionMetadata, + IGroupedApiVersionMetadataFactory { - var declared = new HashSet(); - var advertisedSupported = new HashSet(); - var advertisedDeprecated = new HashSet(); - - for ( var i = 0; i < metadata.Count; i++ ) - { - 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 ); - } + private GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata ) + : base( metadata ) => GroupName = groupName; - versions = model.DeprecatedApiVersions; + public string? GroupName { get; } - for ( var j = 0; j < versions.Count; j++ ) - { - var version = versions[j]; - deprecated.Add( version ); - advertisedDeprecated.Add( version ); - } - } - - 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 ); - } - } - - private void AppendDescriptions( ICollection descriptions, IEnumerable versions, bool deprecated ) - { - 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 ) ); - } - } + static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New( + string? groupName, + ApiVersionMetadata metadata ) => new( groupName, metadata ); - private sealed class ApiVersionDescriptionCollection - { - private readonly object syncRoot = new(); - private readonly DefaultApiVersionDescriptionProvider apiVersionDescriptionProvider; - private readonly EndpointApiVersionMetadataCollection endpoints; - private readonly ActionApiVersionMetadataCollection actions; - private IReadOnlyList? items; - private long version; + public bool Equals( GroupedApiVersionMetadata? other ) => + other is not null && other.GetHashCode() == GetHashCode(); - public ApiVersionDescriptionCollection( - DefaultApiVersionDescriptionProvider apiVersionDescriptionProvider, - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) - { - this.apiVersionDescriptionProvider = apiVersionDescriptionProvider; - endpoints = new( endpointDataSource ); - actions = new( actionDescriptorCollectionProvider ); - } + public override bool Equals( object? obj ) => + obj is not null && + GetType().Equals( obj.GetType() ) && + GetHashCode() == obj.GetHashCode(); - public IReadOnlyList Items + public override int GetHashCode() { - get - { - if ( items is not null && version == CurrentVersion ) - { - return items; - } - - lock ( syncRoot ) - { - var (items1, version1) = endpoints; - var (items2, version2) = actions; - var currentVersion = ComputeVersion( version1, version2 ); - - if ( items is not null && version == currentVersion ) - { - return items; - } - - var capacity = items1.Count + items2.Count; - var metadata = new List( capacity ); - - for ( var i = 0; i < items1.Count; i++ ) - { - metadata.Add( items1[i] ); - } - - for ( var i = 0; i < items2.Count; i++ ) - { - metadata.Add( items2[i] ); - } + var hash = default( HashCode ); - items = apiVersionDescriptionProvider.Describe( metadata ); - version = currentVersion; - } - - return items; - } - } - - private long CurrentVersion - { - get + if ( !string.IsNullOrEmpty( GroupName ) ) { - lock ( syncRoot ) - { - return ComputeVersion( endpoints.Version, actions.Version ); - } + hash.Add( GroupName, StringComparer.Ordinal ); } - } - private static long ComputeVersion( int version1, int version2 ) => ( ( (long) version1 ) << 32 ) | (long) version2; - } - - private sealed class EndpointApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly EndpointDataSource endpointDataSource; - private List? list; - private int version; - private int currentVersion; + hash.Add( base.GetHashCode() ); - public EndpointApiVersionMetadataCollection( EndpointDataSource endpointDataSource ) - { - this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) ); - ChangeToken.OnChange( endpointDataSource.GetChangeToken, IncrementVersion ); - } - - public int Version => version; - - public IReadOnlyList Items - { - get - { - if ( list is not null && version == currentVersion ) - { - return list; - } - - lock ( syncRoot ) - { - if ( list is not null && version == currentVersion ) - { - return list; - } - - var endpoints = endpointDataSource.Endpoints; - - if ( list == null ) - { - list = new( capacity: endpoints.Count ); - } - else - { - list.Clear(); - list.Capacity = endpoints.Count; - } - - for ( var i = 0; i < endpoints.Count; i++ ) - { - if ( endpoints[i].Metadata.GetMetadata() is ApiVersionMetadata item ) - { - list.Add( item ); - } - } - - version = currentVersion; - } - - return list; - } - } - - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } - } - - private void IncrementVersion() - { - lock ( syncRoot ) - { - currentVersion++; - } - } - } - - private sealed class ActionApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly IActionDescriptorCollectionProvider provider; - private List? list; - private int version; - - public ActionApiVersionMetadataCollection( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) => - provider = actionDescriptorCollectionProvider ?? throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ); - - public int Version => version; - - public IReadOnlyList Items - { - get - { - var collection = provider.ActionDescriptors; - - if ( list is not null && collection.Version == version ) - { - return list; - } - - lock ( syncRoot ) - { - if ( list is not null && collection.Version == version ) - { - return list; - } - - var actions = collection.Items; - - if ( list == null ) - { - list = new( capacity: actions.Count ); - } - else - { - list.Clear(); - list.Capacity = actions.Count; - } - - for ( var i = 0; i < actions.Count; i++ ) - { - list.Add( actions[i].GetApiVersionMetadata() ); - } - - version = collection.Version; - } - - return list; - } - } - - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } + return hash.ToHashCode(); } } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs index 2763a3cf..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 @@ -6,7 +6,6 @@ namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -26,12 +25,8 @@ public static class IApiVersioningBuilderExtensions /// The original . public static IApiVersioningBuilder AddApiExplorer( this IApiVersioningBuilder builder ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - AddApiExplorerServices( builder.Services ); + ArgumentNullException.ThrowIfNull( builder ); + AddApiExplorerServices( builder ); return builder; } @@ -43,83 +38,31 @@ public static IApiVersioningBuilder AddApiExplorer( this IApiVersioningBuilder b /// The original . public static IApiVersioningBuilder AddApiExplorer( this IApiVersioningBuilder builder, Action setupAction ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - var services = builder.Services; - AddApiExplorerServices( services ); - services.Configure( setupAction ); + ArgumentNullException.ThrowIfNull( builder ); + AddApiExplorerServices( builder ); + builder.Services.Configure( setupAction ); return builder; } - private static void AddApiExplorerServices( IServiceCollection services ) + private static void AddApiExplorerServices( IApiVersioningBuilder builder ) { - if ( services == null ) - { - throw new ArgumentNullException( nameof( services ) ); - } + builder.AddMvc(); + + var services = builder.Services; services.AddMvcCore().AddApiExplorer(); services.TryAddSingleton, ApiExplorerOptionsFactory>(); - 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 options = serviceProvider.GetRequiredService>(); - var mightUseCustomGroups = options.Value.FormatGroupName is not null; - - return new ApiVersionDescriptionProviderFactory( serviceProvider, mightUseCustomGroups ? NewGroupedProvider : NewDefaultProvider ); - - static IApiVersionDescriptionProvider NewDefaultProvider( - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, - ISunsetPolicyManager sunsetPolicyManager, - IOptions apiExplorerOptions ) => - new DefaultApiVersionDescriptionProvider( endpointDataSource, actionDescriptorCollectionProvider, sunsetPolicyManager, apiExplorerOptions ); - - static IApiVersionDescriptionProvider NewGroupedProvider( - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, - ISunsetPolicyManager sunsetPolicyManager, - IOptions apiExplorerOptions ) => - new GroupedApiVersionDescriptionProvider( endpointDataSource, actionDescriptorCollectionProvider, sunsetPolicyManager, apiExplorerOptions ); - } - - private static IApiVersionDescriptionProvider ResolveApiVersionDescriptionProvider( IServiceProvider serviceProvider ) - { - var endpointDataSource = serviceProvider.GetRequiredService(); - var actionDescriptorCollectionProvider = serviceProvider.GetRequiredService(); - var sunsetPolicyManager = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>(); - var mightUseCustomGroups = options.Value.FormatGroupName is not null; - - if ( mightUseCustomGroups ) - { - return new GroupedApiVersionDescriptionProvider( - endpointDataSource, - actionDescriptorCollectionProvider, - sunsetPolicyManager, - options ); - } - - return new DefaultApiVersionDescriptionProvider( - endpointDataSource, - actionDescriptorCollectionProvider, - sunsetPolicyManager, - options ); - } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs index 2026211f..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,16 +2,8 @@ namespace Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Routing; +using Asp.Versioning.ApiExplorer.Internal; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -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. @@ -19,25 +11,23 @@ namespace Asp.Versioning.ApiExplorer; [CLSCompliant( false )] public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvider { - private readonly ApiVersionDescriptionCollection collection; + private readonly ApiVersionDescriptionCollection collection; private readonly IOptions options; /// /// Initializes a new instance of the class. /// - /// The data source for endpoints. - /// The provider - /// used to enumerate the actions within an application. + /// The sequence of + /// API version metadata collation providers.. /// The manager used to resolve sunset policies. /// The container of configured /// API explorer options. public GroupedApiVersionDescriptionProvider( - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, + IEnumerable providers, ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, endpointDataSource, actionDescriptorCollectionProvider ); + collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -66,355 +56,18 @@ public GroupedApiVersionDescriptionProvider( /// version descriptions. protected virtual IReadOnlyList Describe( IReadOnlyList metadata ) { - if ( metadata == null ) - { - throw new ArgumentNullException( nameof( 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 - { - private readonly object syncRoot = new(); - private readonly GroupedApiVersionDescriptionProvider apiVersionDescriptionProvider; - private readonly EndpointApiVersionMetadataCollection endpoints; - private readonly ActionApiVersionMetadataCollection actions; - private IReadOnlyList? items; - private long version; - - public ApiVersionDescriptionCollection( - GroupedApiVersionDescriptionProvider apiVersionDescriptionProvider, - EndpointDataSource endpointDataSource, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) - { - this.apiVersionDescriptionProvider = apiVersionDescriptionProvider; - endpoints = new( endpointDataSource ); - actions = new( actionDescriptorCollectionProvider ); - } - - public IReadOnlyList Items - { - get - { - if ( items is not null && version == CurrentVersion ) - { - return items; - } - - lock ( syncRoot ) - { - var (items1, version1) = endpoints; - var (items2, version2) = actions; - var currentVersion = ComputeVersion( version1, version2 ); - - if ( items is not null && version == currentVersion ) - { - return items; - } - - var capacity = items1.Count + items2.Count; - var metadata = new List( capacity ); - - for ( var i = 0; i < items1.Count; i++ ) - { - metadata.Add( items1[i] ); - } - - for ( var i = 0; i < items2.Count; i++ ) - { - metadata.Add( items2[i] ); - } - - items = apiVersionDescriptionProvider.Describe( metadata ); - version = currentVersion; - } - - return items; - } - } - - private long CurrentVersion - { - get - { - lock ( syncRoot ) - { - return ComputeVersion( endpoints.Version, actions.Version ); - } - } - } - - private static long ComputeVersion( int version1, int version2 ) => ( ( (long) version1 ) << 32 ) | (long) version2; - } - - private sealed class EndpointApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly EndpointDataSource endpointDataSource; - private List? list; - private int version; - private int currentVersion; - - public EndpointApiVersionMetadataCollection( EndpointDataSource endpointDataSource ) - { - this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) ); - ChangeToken.OnChange( endpointDataSource.GetChangeToken, IncrementVersion ); - } - - public int Version => version; - - public IReadOnlyList Items - { - get - { - if ( list is not null && version == currentVersion ) - { - return list; - } - - lock ( syncRoot ) - { - if ( list is not null && version == currentVersion ) - { - return list; - } - - var endpoints = endpointDataSource.Endpoints; - - if ( list == null ) - { - list = new( capacity: endpoints.Count ); - } - else - { - list.Clear(); - list.Capacity = endpoints.Count; - } - - for ( var i = 0; i < endpoints.Count; i++ ) - { - var metadata = endpoints[i].Metadata; - - if ( metadata.GetMetadata() is ApiVersionMetadata item ) - { - var groupName = metadata.OfType().LastOrDefault()?.EndpointGroupName; - list.Add( new( groupName, item ) ); - } - } - - version = currentVersion; - } - - return list; - } - } - - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } - } - - private void IncrementVersion() - { - lock ( syncRoot ) - { - currentVersion++; - } - } - } - - private sealed class ActionApiVersionMetadataCollection - { - private readonly object syncRoot = new(); - private readonly IActionDescriptorCollectionProvider provider; - private List? list; - private int version; - - public ActionApiVersionMetadataCollection( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) => - provider = actionDescriptorCollectionProvider ?? throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ); - - public int Version => version; - - public IReadOnlyList Items - { - get - { - var collection = provider.ActionDescriptors; - - if ( list is not null && collection.Version == version ) - { - return list; - } - - lock ( syncRoot ) - { - if ( list is not null && collection.Version == version ) - { - return list; - } - - var actions = collection.Items; - - if ( list == null ) - { - list = new( capacity: actions.Count ); - } - else - { - list.Clear(); - list.Capacity = actions.Count; - } - - for ( var i = 0; i < actions.Count; i++ ) - { - var action = actions[i]; - list.Add( new( GetGroupName( action ), action.GetApiVersionMetadata() ) ); - } - - version = collection.Version; - } - - return list; - } - } - - // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs - private static string? GetGroupName( ActionDescriptor action ) - { - var endpointGroupName = action.EndpointMetadata.OfType().LastOrDefault(); - - if ( endpointGroupName is null ) - { - return action.GetProperty()?.GroupName; - } - - return endpointGroupName.EndpointGroupName; - } - - public void Deconstruct( out IReadOnlyList items, out int version ) - { - lock ( syncRoot ) - { - version = this.version; - items = Items; - } - } - } - - 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; - } + ArgumentNullException.ThrowIfNull( metadata ); + 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. @@ -430,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(); @@ -455,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/IEndpointRouteBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/IEndpointRouteBuilderExtensions.cs index 545278fd..d20148f0 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/IEndpointRouteBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/IEndpointRouteBuilderExtensions.cs @@ -20,14 +20,11 @@ public static class IEndpointRouteBuilderExtensions /// A new read-only list ofAPI version descriptions. public static IReadOnlyList DescribeApiVersions( this IEndpointRouteBuilder endpoints ) { - if ( endpoints == null ) - { - throw new ArgumentNullException( nameof( endpoints ) ); - } + ArgumentNullException.ThrowIfNull( endpoints ); var services = endpoints.ServiceProvider; - var source = new CompositeEndpointDataSource( endpoints.DataSources ); var factory = services.GetRequiredService(); + using var source = new CompositeEndpointDataSource( endpoints.DataSources ); var provider = factory.Create( source ); return provider.ApiVersionDescriptions; 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 diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs index 0fda8310..e1f22177 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/VersionedApiDescriptionProvider.cs @@ -34,7 +34,13 @@ public VersionedApiDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, IModelMetadataProvider modelMetadataProvider, IOptions options ) - : this( sunsetPolicyManager, modelMetadataProvider, new SimpleConstraintResolver( options ), options ) { } + : this( + sunsetPolicyManager, + modelMetadataProvider, + new SimpleConstraintResolver( options ?? throw new ArgumentNullException( nameof( options ) ) ), + options ) + { + } // intentionally hiding IInlineConstraintResolver from public signature until ASP.NET Core fixes their bug // BUG: https://github.com/dotnet/aspnetcore/issues/41773 @@ -110,10 +116,7 @@ protected virtual void PopulateApiVersionParameters( ApiDescription apiDescripti /// The default implementation performs no action. public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var results = context.Results; @@ -123,24 +126,31 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) } var groupResults = new List( capacity: results.Count ); - var unversioned = default( List ); + var unversioned = default( Dictionary ); var formatGroupName = Options.FormatGroupName; + var versions = FlattenApiVersions( results ); - foreach ( var version in FlattenApiVersions( results ) ) + for ( var i = 0; i < versions.Length; i++ ) { + var version = versions[i]; var formattedVersion = version.ToString( Options.GroupNameFormat, CurrentCulture ); - for ( var i = 0; i < results.Count; i++ ) + for ( var j = 0; j < results.Count; j++ ) { - var result = results[i]; + if ( unversioned != null && unversioned.ContainsKey( j ) ) + { + continue; + } + + var result = results[j]; var action = result.ActionDescriptor; if ( !ShouldExploreAction( action, version ) ) { if ( IsUnversioned( action ) ) { - unversioned ??= new(); - unversioned.Add( result ); + unversioned ??= []; + unversioned.Add( j, result ); } continue; @@ -151,11 +161,11 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) var groupResult = result.Clone(); var metadata = action.GetApiVersionMetadata(); - if ( string.IsNullOrEmpty( groupResult.GroupName ) || formatGroupName is null ) + if ( string.IsNullOrEmpty( groupResult.GroupName ) ) { groupResult.GroupName = formattedVersion; } - else + else if ( formatGroupName is not null ) { groupResult.GroupName = formatGroupName( groupResult.GroupName, formattedVersion ); } @@ -167,15 +177,15 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) groupResult.SetApiVersion( version ); PopulateApiVersionParameters( groupResult, version ); - groupResults.Add( groupResult ); + AddOrUpdateResult( groupResults, groupResult, metadata, version ); } } results.Clear(); - for ( var i = 0; i < groupResults.Count; i++ ) + for ( var j = 0; j < groupResults.Count; j++ ) { - results.Add( groupResults[i] ); + results.Add( groupResults[j] ); } if ( unversioned == null ) @@ -183,9 +193,9 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) return; } - for ( var i = 0; i < unversioned.Count; i++ ) + foreach ( var result in unversioned.Values ) { - results.Add( unversioned[i] ); + results.Add( result ); } } @@ -240,7 +250,48 @@ private static void TryUpdateControllerRouteValueForMinimalApi( ApiDescription d } } - private IEnumerable FlattenApiVersions( IList descriptions ) + private static void AddOrUpdateResult( + List results, + ApiDescription result, + ApiVersionMetadata metadata, + ApiVersion version ) + { + var comparer = StringComparer.OrdinalIgnoreCase; + + for ( var i = results.Count - 1; i >= 0; i-- ) + { + var other = results[i]; + + if ( comparer.Equals( result.GroupName, other.GroupName ) && + comparer.Equals( result.RelativePath, other.RelativePath ) && + comparer.Equals( result.HttpMethod, other.HttpMethod ) ) + { + var mapping = other.ActionDescriptor.GetApiVersionMetadata().MappingTo( version ); + + switch ( metadata.MappingTo( version ) ) + { + case Explicit: + if ( mapping == Implicit ) + { + results.RemoveAt( i ); + } + + break; + case Implicit: + if ( mapping == Explicit ) + { + return; + } + + break; + } + } + } + + results.Add( result ); + } + + private ApiVersion[] FlattenApiVersions( IList descriptions ) { var versions = default( SortedSet ); @@ -252,7 +303,7 @@ private IEnumerable FlattenApiVersions( IList descri if ( versions is null && declaredVersions.Count > 0 ) { - versions = new(); + versions = []; } for ( var j = 0; j < declaredVersions.Count; j++ ) @@ -263,18 +314,14 @@ private IEnumerable FlattenApiVersions( IList descri if ( versions is null ) { - return new[] { Options.DefaultApiVersion }; + return [Options.DefaultApiVersion]; } - return versions; + return [.. versions]; } - private sealed class SimpleConstraintResolver : IInlineConstraintResolver + private sealed class SimpleConstraintResolver( IOptions options ) : IInlineConstraintResolver { - private readonly IOptions options; - - internal SimpleConstraintResolver( IOptions options ) => this.options = options; - public IRouteConstraint? ResolveConstraint( string inlineConstraint ) { if ( options.Value.RouteConstraintName == inlineConstraint ) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Abstractions/ActionDescriptorExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Abstractions/ActionDescriptorExtensions.cs index 61fb9fc0..9ca5df02 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Abstractions/ActionDescriptorExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Abstractions/ActionDescriptorExtensions.cs @@ -17,10 +17,7 @@ public static class ActionDescriptorExtensions /// The API version information for the action. public static ApiVersionMetadata GetApiVersionMetadata( this ActionDescriptor action ) { - if ( action == null ) - { - throw new ArgumentNullException( nameof( action ) ); - } + ArgumentNullException.ThrowIfNull( action ); var endpointMetadata = action.EndpointMetadata; @@ -46,7 +43,7 @@ internal static void AddOrReplaceApiVersionMetadata( this ActionDescriptor actio if ( endpointMetadata == null ) { - action.EndpointMetadata = new List() { value }; + action.EndpointMetadata = [value]; return; } @@ -68,7 +65,7 @@ internal static void AddOrReplaceApiVersionMetadata( this ActionDescriptor actio if ( endpointMetadata.IsReadOnly ) { - action.EndpointMetadata = new List() { value }; + action.EndpointMetadata = [value]; } else { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs new file mode 100644 index 00000000..6614986a --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.ApiExplorer; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; + +/// +/// Represents an API version metadata collection provider for controller actions. +/// +[CLSCompliant( false )] +public sealed class ActionApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider +{ + private readonly IActionDescriptorCollectionProvider provider; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying + /// action descriptor collection provider. + public ActionApiVersionMetadataCollationProvider( IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) => + provider = actionDescriptorCollectionProvider ?? throw new ArgumentNullException( nameof( actionDescriptorCollectionProvider ) ); + + /// + public int Version => provider.ActionDescriptors.Version; + + /// + public void Execute( ApiVersionMetadataCollationContext context ) + { + ArgumentNullException.ThrowIfNull( context ); + + var actions = provider.ActionDescriptors.Items; + + for ( var i = 0; i < actions.Count; i++ ) + { + var action = actions[i]; + var item = action.GetApiVersionMetadata(); + var groupName = GetGroupName( action ); + + context.Results.Add( item, groupName ); + } + } + + // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.ApiExplorer/src/DefaultApiDescriptionProvider.cs + private static string? GetGroupName( ActionDescriptor action ) + { + var endpointGroupName = action.EndpointMetadata.OfType().LastOrDefault(); + + if ( endpointGroupName is null ) + { + return action.GetProperty()?.GroupName; + } + + return endpointGroupName.EndpointGroupName; + } +} \ 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..28bed7bf --- /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/ApiVersionCollator.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs index f0e2b64c..e621198a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs @@ -2,10 +2,8 @@ namespace Asp.Versioning; -using Asp.Versioning.ApplicationModels; using Asp.Versioning.Conventions; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using System.Runtime.CompilerServices; using static Asp.Versioning.ApiVersionMapping; @@ -34,10 +32,7 @@ public class ApiVersionCollator : IActionDescriptorProvider /// public virtual void OnProvidersExecuted( ActionDescriptorProviderContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); foreach ( var actions in GroupActionsByController( context.Results ) ) { @@ -84,10 +79,7 @@ public virtual void OnProvidersExecuting( ActionDescriptorProviderContext contex /// protected virtual string GetControllerName( ActionDescriptor action ) { - if ( action == null ) - { - throw new ArgumentNullException( nameof( action ) ); - } + ArgumentNullException.ThrowIfNull( action ); if ( !action.RouteValues.TryGetValue( "controller", out var name ) || name is null ) { @@ -122,7 +114,7 @@ private IEnumerable> GroupActionsByController( I if ( !groups.TryGetValue( key, out var values ) ) { - groups.Add( key, values = new() ); + groups.Add( key, values = [] ); } values.Add( action ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinder.cs index 0e4f4618..965ed1bd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinder.cs @@ -15,10 +15,7 @@ public class ApiVersionModelBinder : IModelBinder /// public virtual Task BindModelAsync( ModelBindingContext bindingContext ) { - if ( bindingContext == null ) - { - throw new ArgumentNullException( nameof( bindingContext ) ); - } + ArgumentNullException.ThrowIfNull( bindingContext ); var feature = bindingContext.HttpContext.ApiVersioningFeature(); var model = feature.RequestedApiVersion; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinderProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinderProvider.cs index 25955ba2..f878eab0 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinderProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionModelBinderProvider.cs @@ -10,10 +10,7 @@ internal sealed class ApiVersionModelBinderProvider : IModelBinderProvider public IModelBinder? GetBinder( ModelBinderProviderContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); if ( typeof( ApiVersion ).IsAssignableFrom( context.Metadata.ModelType ) ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningApplicationModelProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningApplicationModelProvider.cs index ed1e996b..72023770 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningApplicationModelProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningApplicationModelProvider.cs @@ -69,10 +69,7 @@ public virtual void OnProvidersExecuted( ApplicationModelProviderContext context /// public virtual void OnProvidersExecuting( ApplicationModelProviderContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var application = context.Result; var controllers = ControllerFilter.Apply( application.Controllers ); @@ -81,7 +78,14 @@ public virtual void OnProvidersExecuting( ApplicationModelProviderContext contex { var controller = controllers[i]; - controller.ControllerName = NamingConvention.NormalizeName( controller.ControllerName ); + if ( controller.RouteValues.TryGetValue( "controller", out var name ) ) + { + controller.ControllerName = name!; + } + else + { + controller.ControllerName = NamingConvention.NormalizeName( controller.ControllerName ); + } if ( !ConventionBuilder.ApplyTo( controller ) ) { diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs index d7ff47fd..bf7290ba 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersioningMvcOptionsSetup.cs @@ -25,10 +25,7 @@ public class ApiVersioningMvcOptionsSetup : IPostConfigureOptions /// public virtual void PostConfigure( string? name, MvcOptions options ) { - if ( options == null ) - { - throw new ArgumentNullException( nameof( options ) ); - } + ArgumentNullException.ThrowIfNull( options ); var value = versioningOptions.Value; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs index 092a3472..69206a57 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs @@ -3,6 +3,7 @@ namespace Asp.Versioning.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Infrastructure; using System.Reflection; /// @@ -11,17 +12,19 @@ namespace Asp.Versioning.ApplicationModels; [CLSCompliant( false )] public sealed class ApiBehaviorSpecification : IApiControllerSpecification { - static ApiBehaviorSpecification() + /// + public bool IsSatisfiedBy( ControllerModel controller ) { - const string ApiBehaviorApplicationModelProviderTypeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ApiBehaviorApplicationModelProvider, Microsoft.AspNetCore.Mvc.Core"; - var type = Type.GetType( ApiBehaviorApplicationModelProviderTypeName, throwOnError: true )!; - var method = type.GetRuntimeMethods().Single( m => m.Name == "IsApiController" ); + ArgumentNullException.ThrowIfNull( controller ); - IsApiController = (Func) method.CreateDelegate( typeof( Func ) ); - } + // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs + if ( controller.Attributes.OfType().Any() ) + { + return true; + } - private static Func IsApiController { get; } + var assembly = controller.ControllerType.Assembly; - /// - public bool IsSatisfiedBy( ControllerModel controller ) => IsApiController( controller ); + return assembly.GetCustomAttributes().OfType().Any(); + } } \ No newline at end of file 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/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ModelExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ModelExtensions.cs index 043435f8..9ccb55d5 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ModelExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ModelExtensions.cs @@ -21,10 +21,7 @@ public static class ModelExtensions [EditorBrowsable( EditorBrowsableState.Never )] public static ApiVersionModel GetApiVersionModel( this ControllerModel controller ) { - if ( controller == null ) - { - throw new ArgumentNullException( nameof( controller ) ); - } + ArgumentNullException.ThrowIfNull( controller ); if ( controller.Properties.TryGetValue( typeof( ApiVersionModel ), out var value ) && value is ApiVersionModel model ) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplyContentTypeVersionActionFilter.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplyContentTypeVersionActionFilter.cs index c4ae0a3c..a1d110ed 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplyContentTypeVersionActionFilter.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplyContentTypeVersionActionFilter.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA1812 namespace Asp.Versioning; @@ -19,13 +20,8 @@ public void OnActionExecuted( ActionExecutedContext context ) { } public void OnActionExecuting( ActionExecutingContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } - + ArgumentNullException.ThrowIfNull( context ); var httpContext = context.HttpContext; - httpContext.Response.OnStarting( AddContentTypeApiVersion, httpContext ); } 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 d3f07dc9..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,13 +1,14 @@  - 7.0.0 - 7.0.0.0 - net7.0 + 8.1.0 + 8.1.0.0 + $(DefaultTargetFramework) Asp.Versioning ASP.NET Core API Versioning A service API versioning library for Microsoft ASP.NET Core MVC. Asp;AspNet;AspNetCore;MVC;Versioning + true diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs index bba2494e..82770380 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs @@ -16,10 +16,7 @@ public partial class ActionApiVersionConventionBuilderBase : IApiVersionConventi /// The action model to apply the conventions to. public virtual void ApplyTo( ActionModel item ) { - if ( item == null ) - { - throw new ArgumentNullException( nameof( item ) ); - } + ArgumentNullException.ThrowIfNull( item ); MergeAttributesWithConventions( item.Attributes ); @@ -51,7 +48,7 @@ public virtual void ApplyTo( ActionModel item ) } else { - emptyVersions = Array.Empty(); + emptyVersions = []; endpointModel = new( declaredVersions: emptyVersions, inheritedSupported, @@ -71,7 +68,7 @@ public virtual void ApplyTo( ActionModel item ) } else { - emptyVersions = Array.Empty(); + emptyVersions = []; endpointModel = new( declaredVersions: mapped, supportedVersions: inheritedSupported, diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ControllerApiVersionConventionBuilderBase.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ControllerApiVersionConventionBuilderBase.cs index 4ddc0718..5222b4a7 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ControllerApiVersionConventionBuilderBase.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Conventions/ControllerApiVersionConventionBuilderBase.cs @@ -2,7 +2,6 @@ namespace Asp.Versioning.Conventions; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using System.Reflection; @@ -35,11 +34,7 @@ public abstract class ControllerApiVersionConventionBuilderBase : ApiVersionConv /// The controller model to apply the conventions to. public virtual void ApplyTo( ControllerModel item ) { - if ( item == null ) - { - throw new ArgumentNullException( nameof( item ) ); - } - + ArgumentNullException.ThrowIfNull( item ); MergeAttributesWithConventions( item.Attributes ); ApplyActionConventions( item ); } @@ -50,9 +45,7 @@ public virtual void ApplyTo( ControllerModel item ) /// The method representing the action to retrieve the convention for. /// The retrieved convention or null. /// True if the convention was successfully retrieved; otherwise, false. - protected abstract bool TryGetConvention( - MethodInfo method, - [MaybeNullWhen( false )] out IApiVersionConvention convention ); + protected abstract bool TryGetConvention( MethodInfo method, [MaybeNullWhen( false )] out IApiVersionConvention convention ); private void ApplyActionConventions( ControllerModel controller ) { 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 eee52677..eef6b507 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +// Ignore Spelling: Mvc namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; +using Asp.Versioning.ApiExplorer; using Asp.Versioning.ApplicationModels; using Asp.Versioning.Conventions; using Asp.Versioning.Routing; @@ -12,7 +14,8 @@ namespace Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; +using System.Runtime.CompilerServices; +using static ServiceDescriptor; /// /// Provides ASP.NET Core MVC specific extension methods for . @@ -26,13 +29,8 @@ public static class IApiVersioningBuilderExtensions /// The original . public static IApiVersioningBuilder AddMvc( this IApiVersioningBuilder builder ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); AddServices( builder.Services ); - return builder; } @@ -44,10 +42,7 @@ public static IApiVersioningBuilder AddMvc( this IApiVersioningBuilder builder ) /// The original . public static IApiVersioningBuilder AddMvc( this IApiVersioningBuilder builder, Action setupAction ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } + ArgumentNullException.ThrowIfNull( builder ); var services = builder.Services; @@ -59,17 +54,20 @@ public static IApiVersioningBuilder AddMvc( this IApiVersioningBuilder builder, private static void AddServices( IServiceCollection services ) { + services.AddMvcCore(); services.TryAddSingleton, MvcApiVersioningOptionsFactory>(); services.TryAddSingleton(); - services.TryAddSingleton( sp => new ApiVersionConventionBuilder( sp.GetRequiredService() ) ); + 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() ); services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Transient() ); + services.TryAddEnumerable( Singleton() ); services.Replace( WithUrlHelperFactoryDecorator( services ) ); + services.TryReplace(); } private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor ) @@ -87,19 +85,42 @@ private static object CreateInstance( this IServiceProvider services, ServiceDes return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType! ); } - private static ServiceDescriptor WithUrlHelperFactoryDecorator( IServiceCollection services ) + 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 ) { var descriptor = services.FirstOrDefault( sd => sd.ServiceType == typeof( IUrlHelperFactory ) ); - if ( descriptor is DecoratedServiceDescriptor ) + if ( descriptor is DecoratedServiceDescriptor sd ) { - return descriptor; + return sd; } - var lifetime = ServiceLifetime.Singleton; - Func instantiate = sp => new UrlHelperFactory(); + ServiceLifetime lifetime; + Func instantiate; - if ( descriptor != null ) + if ( descriptor == null ) + { + lifetime = ServiceLifetime.Singleton; + instantiate = static sp => new UrlHelperFactory(); + } + else { lifetime = descriptor.Lifetime; instantiate = sp => sp.CreateInstance( descriptor ); @@ -113,14 +134,14 @@ IUrlHelperFactory NewFactory( IServiceProvider serviceProvider ) if ( source.VersionsByUrl() ) { - var factory = ActivatorUtilities.CreateFactory( typeof( ApiVersionUrlHelperFactory ), new[] { typeof( IUrlHelperFactory ) } ); - instance = factory( serviceProvider, new[] { decorated } ); + var factory = ActivatorUtilities.CreateFactory( typeof( ApiVersionUrlHelperFactory ), [typeof( IUrlHelperFactory )] ); + instance = factory( serviceProvider, [decorated] ); } return (IUrlHelperFactory) instance; } - return new DecoratedServiceDescriptor( typeof( IUrlHelperFactory ), NewFactory, lifetime ); + return new( typeof( IUrlHelperFactory ), NewFactory, lifetime ); } private sealed class DecoratedServiceDescriptor : ServiceDescriptor diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/MvcFormat.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/MvcFormat.cs new file mode 100644 index 00000000..97287926 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/MvcFormat.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Text; + +internal static class MvcFormat +{ + internal static readonly CompositeFormat ActionMethodNotFound = CompositeFormat.Parse( MvcSR.ActionMethodNotFound ); + internal static readonly CompositeFormat AmbiguousActionMethod = CompositeFormat.Parse( MvcSR.AmbiguousActionMethod ); + internal static readonly CompositeFormat MultipleApiVersionsInferredFromNamespaces = CompositeFormat.Parse( MvcSR.MultipleApiVersionsInferredFromNamespaces ); + internal static readonly CompositeFormat InvalidActionMethodExpression = CompositeFormat.Parse( MvcSR.InvalidActionMethodExpression ); + internal static readonly CompositeFormat ConventionStyleMismatch = CompositeFormat.Parse( MvcSR.ConventionStyleMismatch ); +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReportApiVersionsAttribute.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReportApiVersionsAttribute.cs index 7a60aafe..90348fff 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReportApiVersionsAttribute.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ReportApiVersionsAttribute.cs @@ -22,10 +22,7 @@ public sealed partial class ReportApiVersionsAttribute /// streaming to the client. public override void OnActionExecuting( ActionExecutingContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var httpContext = context.HttpContext; var endpoint = httpContext.GetEndpoint(); 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 18b95aac..bbf03793 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs @@ -51,11 +51,7 @@ public ApiVersionUrlHelper( ActionContext actionContext, IUrlHelper url ) /// public virtual string? Action( UrlActionContext actionContext ) { - if ( actionContext == null ) - { - throw new ArgumentNullException( nameof( actionContext ) ); - } - + ArgumentNullException.ThrowIfNull( actionContext ); actionContext.Values = AddApiVersionRouteValueIfNecessary( actionContext.Values ); return Url.Action( actionContext ); } @@ -68,20 +64,20 @@ public ApiVersionUrlHelper( ActionContext actionContext, IUrlHelper url ) Url.Link( routeName, AddApiVersionRouteValueIfNecessary( values ) ); /// +#pragma warning disable IDE0079 #pragma warning disable CA1054 // URI-like parameters should not be strings public virtual bool IsLocalUrl( string? url ) => Url.IsLocalUrl( url ); -#pragma warning restore CA1054 +#pragma warning restore CA1054 // URI-like parameters should not be strings +#pragma warning restore IDE0079 /// +#pragma warning disable IDE0079 #pragma warning disable CA1055 // URI-like return values should not be strings public virtual string? RouteUrl( UrlRouteContext routeContext ) -#pragma warning restore CA1055 +#pragma warning restore CA1055 // URI-like return values should not be strings +#pragma warning restore IDE0079 { - if ( routeContext == null ) - { - throw new ArgumentNullException( nameof( routeContext ) ); - } - + ArgumentNullException.ThrowIfNull( routeContext ); routeContext.Values = AddApiVersionRouteValueIfNecessary( routeContext.Values ); return Url.RouteUrl( routeContext ); } diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelperFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelperFactory.cs index 2f3a50f5..3c69680e 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelperFactory.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelperFactory.cs @@ -26,10 +26,7 @@ public class ApiVersionUrlHelperFactory : IUrlHelperFactory /// public virtual IUrlHelper GetUrlHelper( ActionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var items = context.HttpContext.Items; diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/IUrlHelperExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/IUrlHelperExtensions.cs index 46ad8f33..29fd1648 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/IUrlHelperExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/IUrlHelperExtensions.cs @@ -24,10 +24,7 @@ public static class IUrlHelperExtensions /// it would be erroneously added as a query string parameter. public static IUrlHelper WithoutApiVersion( this IUrlHelper urlHelper ) { - if ( urlHelper == null ) - { - throw new ArgumentNullException( nameof( urlHelper ) ); - } + ArgumentNullException.ThrowIfNull( urlHelper ); if ( urlHelper is WithoutApiVersionUrlHelper || urlHelper.ActionContext.HttpContext.Features.Get() is null ) diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/WithoutApiVersionUrlHelper.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/WithoutApiVersionUrlHelper.cs index 72caa212..1496708b 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/WithoutApiVersionUrlHelper.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/WithoutApiVersionUrlHelper.cs @@ -28,7 +28,7 @@ internal sealed class WithoutApiVersionUrlHelper : IUrlHelper return decorated.Action( actionContext ); } - [return: NotNullIfNotNull( "contentPath" )] + [return: NotNullIfNotNull( nameof( contentPath ) )] public string? Content( string? contentPath ) { if ( Feature is IApiVersioningFeature feature ) diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Asp.Versioning.Http.Tests.csproj b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Asp.Versioning.Http.Tests.csproj index ef624abf..21ec38b6 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Asp.Versioning.Http.Tests.csproj +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Asp.Versioning.Http.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + $(DefaultTargetFramework) Asp.Versioning diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointConventionBuilderExtensionsTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointConventionBuilderExtensionsTest.cs index f5856939..13907a8c 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointConventionBuilderExtensionsTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointConventionBuilderExtensionsTest.cs @@ -6,7 +6,9 @@ namespace Asp.Versioning.Builder; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using static Asp.Versioning.ApiVersionProviderOptions; public class IEndpointConventionBuilderExtensionsTest { @@ -250,6 +252,485 @@ public void with_api_version_set_should_collate_across_grouped_endpoints() .BeEquivalentTo( ApiVersionMetadata.Neutral ); } + [Fact] + public void with_api_version_set_should_not_be_allowed_multiple_times() + { + // arrange + var builder = WebApplication.CreateBuilder(); + var services = builder.Services; + + services.AddControllers(); + services.AddApiVersioning(); + + var app = builder.Build(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + var get = app.MapGet( "/", () => Results.Ok() ); + IEndpointRouteBuilder endpoints = app; + + get.WithApiVersionSet( versionSet ); + get.WithApiVersionSet( versionSet ); + + // act + var build = () => endpoints.DataSources.Single().Endpoints; + + // assert + build.Should().Throw(); + } + + [Fact] + public void with_api_version_set_should_not_override_existing_metadata() + { + // arrange + var builder = WebApplication.CreateBuilder(); + var services = builder.Services; + + services.AddControllers(); + services.AddApiVersioning(); + + var app = builder.Build(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + var group = app.NewVersionedApi(); + var get = group.MapGet( "/", () => Results.Ok() ); + IEndpointRouteBuilder endpoints = app; + + get.WithApiVersionSet( versionSet ); + + // act + var build = () => endpoints.DataSources.Single().Endpoints; + + // assert + build.Should().Throw(); + } + + [Fact] + public void report_api_versions_should_add_convention() + { + // arrange + var conventions = new Mock(); + var reportApiVersions = default( IReportApiVersions ); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + reportApiVersions = endpoint.Metadata.OfType().First(); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.ReportApiVersions(); + + // assert + reportApiVersions.Should().NotBeNull(); + } + + [Fact] + public void is_api_version_neutral_should_add_convention() + { + // arrange + var conventions = new Mock(); + var versionNeutral = default( IApiVersionNeutral ); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + versionNeutral = endpoint.Metadata.OfType().First(); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.IsApiVersionNeutral(); + + // assert + versionNeutral.Should().NotBeNull(); + } + + [Fact] + public void has_api_version_should_add_convention() + { + // arrange + var conventions = new Mock(); + var provider = default( IApiVersionProvider ); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + provider = endpoint.Metadata.OfType().First(); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.HasApiVersion( 1.0 ); + + // assert + provider.Should().BeEquivalentTo( + new + { + Options = None, + Versions = new[] { new ApiVersion( 1.0 ) }, + } ); + } + + [Fact] + public void has_api_version_should_propagate_to_version_set() + { + // arrange + var conventions = new Mock(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.HasApiVersion( 1.0 ); + + // assert + versionSet.Build( new() ).SupportedApiVersions.Single().Should().Be( new ApiVersion( 1.0 ) ); + } + + [Fact] + public void has_deprecated_api_version_should_add_convention() + { + // arrange + var conventions = new Mock(); + var provider = default( IApiVersionProvider ); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + provider = endpoint.Metadata.OfType().First(); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.HasDeprecatedApiVersion( 0.9 ); + + // assert + provider.Should().BeEquivalentTo( + new + { + Options = Deprecated, + Versions = new[] { new ApiVersion( 0.9 ) }, + } ); + } + + [Fact] + public void has_deprecated_api_version_should_propagate_to_version_set() + { + // arrange + var conventions = new Mock(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.HasDeprecatedApiVersion( 0.9 ); + + // assert + versionSet.Build( new() ).DeprecatedApiVersions.Single().Should().Be( new ApiVersion( 0.9 ) ); + } + + [Fact] + public void advertises_api_version_should_add_convention() + { + // arrange + var conventions = new Mock(); + var provider = default( IApiVersionProvider ); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + provider = endpoint.Metadata.OfType().First(); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.AdvertisesApiVersion( 42.0 ); + + // assert + provider.Should().BeEquivalentTo( + new + { + Options = Advertised, + Versions = new[] { new ApiVersion( 42.0 ) }, + } ); + } + + [Fact] + public void advertises_api_version_should_propagate_to_version_set() + { + // arrange + var conventions = new Mock(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.AdvertisesApiVersion( 42.0 ); + + // assert + versionSet.Build( new() ).SupportedApiVersions.Single().Should().Be( new ApiVersion( 42.0 ) ); + } + + [Fact] + public void advertises_deprecated_api_version_should_add_convention() + { + // arrange + var conventions = new Mock(); + var provider = default( IApiVersionProvider ); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + provider = endpoint.Metadata.OfType().First(); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.AdvertisesDeprecatedApiVersion( 42.0, "rc" ); + + // assert + provider.Should().BeEquivalentTo( + new + { + Options = Advertised | Deprecated, + Versions = new[] { new ApiVersion( 42.0, "rc" ) }, + } ); + } + + [Fact] + public void advertises_deprecated_api_version_should_propagate_to_version_set() + { + // arrange + var conventions = new Mock(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.AdvertisesDeprecatedApiVersion( 42.0, "rc" ); + + // assert + versionSet.Build( new() ).DeprecatedApiVersions.Single().Should().Be( new ApiVersion( 42.0, "rc" ) ); + } + + [Fact] + public void map_to_api_version_should_add_convention() + { + // arrange + var conventions = new Mock(); + var provider = default( IApiVersionProvider ); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => + { + var endpoint = Mock.Of(); + var versionSet = new ApiVersionSetBuilder( default ).Build(); + endpoint.Metadata.Add( versionSet ); + callback( endpoint ); + provider = endpoint.Metadata.OfType().First(); + } ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + route.MapToApiVersion( 2.0 ); + + // assert + provider.Should().BeEquivalentTo( + new + { + Options = Mapped, + Versions = new[] { new ApiVersion( 2.0 ) }, + } ); + } + + [Fact] + public void map_to_api_version_should_throw_exception_without_version_set() + { + // arrange + var conventions = new Mock(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => callback( Mock.Of() ) ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + var mapToApiVersion = () => route.MapToApiVersion( 2.0 ); + + // assert + mapToApiVersion.Should().Throw(); + } + + [Fact] + public void has_api_version_should_throw_exception_without_version_set() + { + // arrange + var conventions = new Mock(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => callback( Mock.Of() ) ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + var hasApiVersion = () => route.HasApiVersion( 2.0 ); + + // assert + hasApiVersion.Should().Throw(); + } + + [Fact] + public void has_deprecated_api_version_should_throw_exception_without_version_set() + { + // arrange + var conventions = new Mock(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => callback( Mock.Of() ) ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + var hasDeprecatedApiVersion = () => route.HasDeprecatedApiVersion( 2.0 ); + + // assert + hasDeprecatedApiVersion.Should().Throw(); + } + + [Fact] + public void advertises_api_version_should_throw_exception_without_version_set() + { + // arrange + var conventions = new Mock(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => callback( Mock.Of() ) ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + var advertisesApiVersion = () => route.AdvertisesApiVersion( 2.0 ); + + // assert + advertisesApiVersion.Should().Throw(); + } + + [Fact] + public void advertises_deprecated_api_version_should_throw_exception_without_version_set() + { + // arrange + var conventions = new Mock(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => callback( Mock.Of() ) ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + var advertisesDeprecatedApiVersion = () => route.AdvertisesDeprecatedApiVersion( 2.0 ); + + // assert + advertisesDeprecatedApiVersion.Should().Throw(); + } + + [Fact] + public void is_api_version_neutral_should_throw_exception_without_version_set() + { + // arrange + var conventions = new Mock(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => callback( Mock.Of() ) ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + var isApiVersionNeutral = () => route.IsApiVersionNeutral(); + + // assert + isApiVersionNeutral.Should().Throw(); + } + + [Fact] + public void reports_api_versions_should_throw_exception_without_version_set() + { + // arrange + var conventions = new Mock(); + + conventions.Setup( b => b.Add( It.IsAny>() ) ) + .Callback( ( Action callback ) => callback( Mock.Of() ) ); + + var route = new RouteHandlerBuilder( new[] { conventions.Object } ); + + // act + var reportsApiVersions = () => route.ReportApiVersions(); + + // assert + reportsApiVersions.Should().Throw(); + } + private sealed class MockServiceProvider : IServiceProvider { private readonly IOptions options = Options.Create( new ApiVersioningOptions() ); @@ -266,11 +747,6 @@ public object GetService( Type serviceType ) return options.Value.ApiVersionReader; } - if ( typeof( IApiVersionSetBuilderFactory ) == serviceType ) - { - return new DefaultApiVersionSetBuilderFactory(); - } - return null; } } diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointRouteBuilderExtensionsTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointRouteBuilderExtensionsTest.cs index 0c38fc7c..4a73b4ac 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointRouteBuilderExtensionsTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/IEndpointRouteBuilderExtensionsTest.cs @@ -29,4 +29,85 @@ public void new_api_version_set_should_use_name() // assert versionSet.Name.Should().Be( "Test" ); } + + [Fact] + public void with_api_version_set_should_not_be_allowed_multiple_times() + { + // arrange + var builder = WebApplication.CreateBuilder(); + var services = builder.Services; + + services.AddControllers(); + services.AddApiVersioning(); + + var app = builder.Build(); + var group = app.MapGroup( "Test" ); + + // act + var withApiVersionSet = () => group.WithApiVersionSet().WithApiVersionSet(); + + // assert + withApiVersionSet.Should().Throw(); + } + + [Fact] + public void with_api_version_set_should_not_allow_nesting() + { + // arrange + var builder = WebApplication.CreateBuilder(); + var services = builder.Services; + + services.AddControllers(); + services.AddApiVersioning(); + + var app = builder.Build(); + var g1 = app.MapGroup( "Root" ).WithApiVersionSet(); + var g2 = g1.MapGroup( "Test" ); + + // act + var withApiVersionSet = () => g2.WithApiVersionSet(); + + // assert + withApiVersionSet.Should().Throw(); + } + + [Fact] + public void new_versioned_api_should_not_be_allowed_multiple_times() + { + // arrange + var builder = WebApplication.CreateBuilder(); + var services = builder.Services; + + services.AddControllers(); + services.AddApiVersioning(); + + var app = builder.Build(); + + // act + var newVersionedApi = () => app.NewVersionedApi().NewVersionedApi(); + + // assert + newVersionedApi.Should().Throw(); + } + + [Fact] + public void new_versioned_api_should_not_allow_nesting() + { + // arrange + var builder = WebApplication.CreateBuilder(); + var services = builder.Services; + + services.AddControllers(); + services.AddApiVersioning(); + + var app = builder.Build(); + var g1 = app.NewVersionedApi(); + var g2 = g1.MapGroup( "Test" ); + + // act + var newVersionedApi = () => g2.NewVersionedApi(); + + // assert + newVersionedApi.Should().Throw(); + } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/RouteHandlerBuilderExtensionsTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/RouteHandlerBuilderExtensionsTest.cs deleted file mode 100644 index d2f9c1a1..00000000 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Builder/RouteHandlerBuilderExtensionsTest.cs +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. - -namespace Asp.Versioning.Builder; - -using Microsoft.AspNetCore.Builder; -using static Asp.Versioning.ApiVersionProviderOptions; - -public class RouteHandlerBuilderExtensionsTest -{ - [Fact] - public void report_api_versions_should_add_convention() - { - // arrange - var conventions = new Mock(); - var reportApiVersions = default( IReportApiVersions ); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - callback( endpoint ); - reportApiVersions = endpoint.Metadata.OfType().First(); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.ReportApiVersions(); - - // assert - reportApiVersions.Should().NotBeNull(); - } - - [Fact] - public void is_api_version_neutral_should_add_convention() - { - // arrange - var conventions = new Mock(); - var versionNeutral = default( IApiVersionNeutral ); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - callback( endpoint ); - versionNeutral = endpoint.Metadata.OfType().First(); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.IsApiVersionNeutral(); - - // assert - versionNeutral.Should().NotBeNull(); - } - - [Fact] - public void has_api_version_should_add_convention() - { - // arrange - var conventions = new Mock(); - var provider = default( IApiVersionProvider ); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - callback( endpoint ); - provider = endpoint.Metadata.OfType().First(); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.HasApiVersion( 1.0 ); - - // assert - provider.Should().BeEquivalentTo( - new - { - Options = None, - Versions = new[] { new ApiVersion( 1.0 ) }, - } ); - } - - [Fact] - public void has_api_version_should_propagate_to_version_set() - { - // arrange - var conventions = new Mock(); - var versionSet = new ApiVersionSetBuilder( default ).Build(); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - endpoint.Metadata.Add( versionSet ); - callback( endpoint ); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.HasApiVersion( 1.0 ); - - // assert - versionSet.Build( new() ).SupportedApiVersions.Single().Should().Be( new ApiVersion( 1.0 ) ); - } - - [Fact] - public void has_deprecated_api_version_should_add_convention() - { - // arrange - var conventions = new Mock(); - var provider = default( IApiVersionProvider ); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - callback( endpoint ); - provider = endpoint.Metadata.OfType().First(); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.HasDeprecatedApiVersion( 0.9 ); - - // assert - provider.Should().BeEquivalentTo( - new - { - Options = Deprecated, - Versions = new[] { new ApiVersion( 0.9 ) }, - } ); - } - - [Fact] - public void has_deprecated_api_version_should_propagate_to_version_set() - { - // arrange - var conventions = new Mock(); - var versionSet = new ApiVersionSetBuilder( default ).Build(); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - endpoint.Metadata.Add( versionSet ); - callback( endpoint ); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.HasDeprecatedApiVersion( 0.9 ); - - // assert - versionSet.Build( new() ).DeprecatedApiVersions.Single().Should().Be( new ApiVersion( 0.9 ) ); - } - - [Fact] - public void advertises_api_version_should_add_convention() - { - // arrange - var conventions = new Mock(); - var provider = default( IApiVersionProvider ); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - callback( endpoint ); - provider = endpoint.Metadata.OfType().First(); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.AdvertisesApiVersion( 42.0 ); - - // assert - provider.Should().BeEquivalentTo( - new - { - Options = Advertised, - Versions = new[] { new ApiVersion( 42.0 ) }, - } ); - } - - [Fact] - public void advertises_api_version_should_propagate_to_version_set() - { - // arrange - var conventions = new Mock(); - var versionSet = new ApiVersionSetBuilder( default ).Build(); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - endpoint.Metadata.Add( versionSet ); - callback( endpoint ); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.AdvertisesApiVersion( 42.0 ); - - // assert - versionSet.Build( new() ).SupportedApiVersions.Single().Should().Be( new ApiVersion( 42.0 ) ); - } - - [Fact] - public void advertises_deprecated_api_version_should_add_convention() - { - // arrange - var conventions = new Mock(); - var provider = default( IApiVersionProvider ); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - callback( endpoint ); - provider = endpoint.Metadata.OfType().First(); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.AdvertisesDeprecatedApiVersion( 42.0, "rc" ); - - // assert - provider.Should().BeEquivalentTo( - new - { - Options = Advertised | Deprecated, - Versions = new[] { new ApiVersion( 42.0, "rc" ) }, - } ); - } - - [Fact] - public void advertises_deprecated_api_version_should_propagate_to_version_set() - { - // arrange - var conventions = new Mock(); - var versionSet = new ApiVersionSetBuilder( default ).Build(); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - endpoint.Metadata.Add( versionSet ); - callback( endpoint ); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.AdvertisesDeprecatedApiVersion( 42.0, "rc" ); - - // assert - versionSet.Build( new() ).DeprecatedApiVersions.Single().Should().Be( new ApiVersion( 42.0, "rc" ) ); - } - - [Fact] - public void map_to_api_version_should_add_convention() - { - // arrange - var conventions = new Mock(); - var provider = default( IApiVersionProvider ); - - conventions.Setup( b => b.Add( It.IsAny>() ) ) - .Callback( ( Action callback ) => - { - var endpoint = Mock.Of(); - callback( endpoint ); - provider = endpoint.Metadata.OfType().First(); - } ); - - var route = new RouteHandlerBuilder( new[] { conventions.Object } ); - - // act - route.MapToApiVersion( 2.0 ); - - // assert - provider.Should().BeEquivalentTo( - new - { - Options = Mapped, - Versions = new[] { new ApiVersion( 2.0 ) }, - } ); - } -} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs index 99b8a466..9125c9ae 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DefaultApiVersionReporterTest.cs @@ -12,8 +12,8 @@ public class DefaultApiVersionReporterTest public void report_should_add_expected_headers() { // arrange - var reporter = new DefaultApiVersionReporter(); var sunsetDate = DateTimeOffset.Now; + var reporter = new DefaultApiVersionReporter( new TestSunsetPolicyManager( sunsetDate ) ); var httpContext = new Mock(); var features = new Mock(); var query = new Mock(); @@ -50,7 +50,6 @@ public void report_should_add_expected_headers() response.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object ); serviceProvider.Setup( sp => sp.GetService( typeof( IApiVersionParser ) ) ).Returns( ApiVersionParser.Default ); serviceProvider.Setup( sp => sp.GetService( typeof( IApiVersionReader ) ) ).Returns( new QueryStringApiVersionReader() ); - serviceProvider.Setup( sp => sp.GetService( typeof( ISunsetPolicyManager ) ) ).Returns( new TestSunsetPolicyManager( sunsetDate ) ); httpContext.SetupGet( c => c.Features ).Returns( features.Object ); httpContext.SetupGet( c => c.Request ).Returns( request.Object ); httpContext.SetupProperty( c => c.RequestServices, serviceProvider.Object ); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DependencyInjection/IServiceCollectionExtensionsTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DependencyInjection/IServiceCollectionExtensionsTest.cs new file mode 100644 index 00000000..8ed7463e --- /dev/null +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/DependencyInjection/IServiceCollectionExtensionsTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Microsoft.Extensions.DependencyInjection; + +using Asp.Versioning; +using Microsoft.Extensions.Options; + +public class IServiceCollectionExtensionsTest +{ + [Fact] + public void add_api_versioning_should_not_allow_default_neutral_api_version() + { + // arrange + var services = new ServiceCollection(); + + services.AddApiVersioning( options => options.DefaultApiVersion = ApiVersion.Neutral ); + + var provider = services.BuildServiceProvider(); + + // act + Func options = () => provider.GetRequiredService>().Value; + + // assert + options.Should().Throw(); + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/ErrorObjectWriterTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/ErrorObjectWriterTest.cs new file mode 100644 index 00000000..822f619b --- /dev/null +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/ErrorObjectWriterTest.cs @@ -0,0 +1,133 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Options; +using System.Text.Json; +using System.Threading.Tasks; + +public class ErrorObjectWriterTest +{ + [Theory] + [InlineData( "https://docs.api-versioning.org/problems#unsupported" )] + [InlineData( "https://docs.api-versioning.org/problems#unspecified" )] + [InlineData( "https://docs.api-versioning.org/problems#invalid" )] + [InlineData( "https://docs.api-versioning.org/problems#ambiguous" )] + public void can_write_should_be_true_for_api_versioning_problem_types( string type ) + { + // arrange + var writer = new ErrorObjectWriter( Options.Create( new JsonOptions() ) ); + var context = new ProblemDetailsContext() + { + HttpContext = new DefaultHttpContext(), + ProblemDetails = + { + Type = type, + }, + }; + + // act + var result = writer.CanWrite( context ); + + // assert + result.Should().BeTrue(); + } + + [Fact] + public void can_write_should_be_false_for_other_problem_types() + { + // arrange + const string BadRequest = "https://tools.ietf.org/html/rfc7231#section-6.5.1"; + var writer = new ErrorObjectWriter( Options.Create( new JsonOptions() ) ); + var context = new ProblemDetailsContext() + { + HttpContext = new DefaultHttpContext(), + ProblemDetails = + { + Type = BadRequest, + }, + }; + + // act + var result = writer.CanWrite( context ); + + // assert + result.Should().BeFalse(); + } + + [Fact] + public async Task write_async_should_output_expected_json() + { + // arrange + var example = new + { + error = new + { + code = default( string ), + message = default( string ), + target = default( string ), + innerError = new + { + message = default( string ), + }, + }, + }; + + var writer = new ErrorObjectWriter( Options.Create( new JsonOptions() ) ); + using var stream = new MemoryStream(); + var response = new Mock() { CallBase = true }; + var httpContext = new Mock() { CallBase = true }; + + response.SetupGet( r => r.Body ).Returns( stream ); + response.SetupProperty( r => r.ContentType ); + response.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object ); + httpContext.SetupGet( c => c.Response ).Returns( response.Object ); + + var context = new ProblemDetailsContext() + { + HttpContext = httpContext.Object, + ProblemDetails = + { + Type = ProblemDetailsDefaults.Unsupported.Type, + Title = ProblemDetailsDefaults.Unsupported.Title, + Status = 400, + Detail = "The HTTP resource that matches the request URI 'https://tempuri.org' does not support the API version '42.0'.", + Extensions = + { + ["code"] = ProblemDetailsDefaults.Unsupported.Code, + }, + }, + }; + + // act + await writer.WriteAsync( context ); + + await stream.FlushAsync(); + stream.Position = 0; + + var error = await DeserializeByExampleAsync( stream, example ); + + // assert + response.Object.ContentType.Should().Be( "application/json; charset=utf-8" ); + error.Should().BeEquivalentTo( + new + { + error = new + { + code = "UnsupportedApiVersion", + message = "Unsupported API version", + innerError = new + { + message = "The HTTP resource that matches the request URI 'https://tempuri.org' does not support the API version '42.0'.", + }, + }, + } ); + } + +#pragma warning disable IDE0060 // Remove unused parameter + private static ValueTask DeserializeByExampleAsync( Stream stream, T example ) => + JsonSerializer.DeserializeAsync( stream ); +#pragma warning restore IDE0060 // Remove unused parameter +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionBuilderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionBuilderTest.cs index fc842c09..7050bf6a 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionBuilderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionBuilderTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable ASP0015 // Suggest using IHeaderDictionary properties + namespace Asp.Versioning; using Microsoft.AspNetCore.Http; @@ -260,7 +262,7 @@ public void read_should_assume_version_from_single_parameter_in_media_type_templ [InlineData( "application/vnd-v{v}+json", "v", "application/vnd-v2.1+json", "2.1" )] [InlineData( "application/vnd-v{ver}+json", "ver", "application/vnd-v2022-11-01+json", "2022-11-01" )] [InlineData( "application/vnd-{version}+xml", "version", "application/vnd-1.1-beta+xml", "1.1-beta" )] - public void read_should_retreive_version_from_media_type_template( + public void read_should_retrieve_version_from_media_type_template( string template, string parameterName, string mediaType, diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionReaderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionReaderTest.cs index 43b097cc..16671d3e 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionReaderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/MediaTypeApiVersionReaderTest.cs @@ -33,7 +33,9 @@ public void read_should_retrieve_version_from_content_type() var request = new Mock(); var headers = new Mock(); +#pragma warning disable ASP0015 // Suggest using IHeaderDictionary properties headers.SetupGet( h => h["Content-Type"] ).Returns( new StringValues( "application/json;v=2.0" ) ); +#pragma warning restore ASP0015 // Suggest using IHeaderDictionary properties request.SetupGet( r => r.Headers ).Returns( headers.Object ); request.SetupProperty( r => r.Body, Null ); request.SetupProperty( r => r.ContentLength, 0L ); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionMatcherPolicyTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionMatcherPolicyTest.cs index 526ba3ea..bc8d7e9c 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionMatcherPolicyTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionMatcherPolicyTest.cs @@ -2,6 +2,7 @@ namespace Asp.Versioning.Routing; +using Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; @@ -96,7 +97,7 @@ public async Task apply_should_have_candidate_for_matched_api_version() var model = new ApiVersionModel( new ApiVersion( 1, 0 ) ); var items = new object[] { new ApiVersionMetadata( model, model ) }; var endpoint = new Endpoint( Limbo, new( items ), default ); - var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, new[] { 0 } ); + var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, [0] ); var policy = NewApiVersionMatcherPolicy(); feature.SetupProperty( f => f.RequestedApiVersion, new ApiVersion( 1, 0 ) ); @@ -124,7 +125,7 @@ public async Task apply_should_use_400_endpoint_for_unmatched_api_version() var model = new ApiVersionModel( new ApiVersion( 1, 0 ) ); var items = new object[] { new ApiVersionMetadata( model, model ) }; var endpoint = new Endpoint( Limbo, new( items ), default ); - var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, new[] { 0 } ); + var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, [0] ); var httpContext = NewHttpContext( feature ); // act @@ -191,7 +192,7 @@ public async Task apply_should_use_400_endpoint_for_unspecified_api_version() var model = new ApiVersionModel( new ApiVersion( 1, 0 ) ); var items = new object[] { new ApiVersionMetadata( model, model ) }; var endpoint = new Endpoint( Limbo, new( items ), "Test" ); - var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, new[] { 0 } ); + var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, [0] ); var httpContext = NewHttpContext( feature ); // act @@ -209,7 +210,7 @@ public async Task apply_should_have_candidate_for_unspecified_api_version() var model = new ApiVersionModel( new ApiVersion( 1, 0 ) ); var items = new object[] { new ApiVersionMetadata( model, model ) }; var endpoint = new Endpoint( Limbo, new( items ), default ); - var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, new[] { 0 } ); + var candidates = new CandidateSet( new[] { endpoint }, new[] { new RouteValueDictionary() }, [0] ); var options = new ApiVersioningOptions() { AssumeDefaultVersionWhenUnspecified = true }; var policy = NewApiVersionMatcherPolicy( options ); @@ -229,7 +230,11 @@ public async Task apply_should_have_candidate_for_unspecified_api_version() private static Task Limbo( HttpContext context ) => Task.CompletedTask; private static ApiVersionMatcherPolicy NewApiVersionMatcherPolicy( ApiVersioningOptions options = default ) => - new( ApiVersionParser.Default, Options.Create( options ?? new() ), Mock.Of>() ); + new( + ApiVersionParser.Default, + Enumerable.Empty(), + Options.Create( options ?? new() ), + Mock.Of>() ); private static HttpContext NewHttpContext( Mock apiVersioningFeature, diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionRouteConstraintTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionRouteConstraintTest.cs index 9c114f92..16084847 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionRouteConstraintTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Http.Tests/Routing/ApiVersionRouteConstraintTest.cs @@ -137,7 +137,7 @@ public void url_helper_should_create_action_with_api_version_constraint() url.Should().Be( "/v1.1/Order/Place" ); } - private class PassThroughRouter : IRouter + private sealed class PassThroughRouter : IRouter { public VirtualPathData GetVirtualPath( VirtualPathContext context ) => null; @@ -163,11 +163,11 @@ private static HttpContext NewHttpContext() return httpContext.Object; } - private static IRouteBuilder CreateRouteBuilder( IServiceProvider services ) + private static RouteBuilder CreateRouteBuilder( IServiceProvider services ) { var app = new Mock(); app.SetupGet( a => a.ApplicationServices ).Returns( services ); - return new RouteBuilder( app.Object ) { DefaultHandler = new PassThroughRouter() }; + return new( app.Object ) { DefaultHandler = new PassThroughRouter() }; } private static IUrlHelper NewUrlHelper( string controller, string action, string version ) diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiDescriptionExtensionsTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiDescriptionExtensionsTest.cs index cf881b07..5f209cf7 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiDescriptionExtensionsTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiDescriptionExtensionsTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dneutral + namespace Asp.Versioning.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApiExplorer; 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 1aec0e18..ec26b099 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 @@ -249,13 +249,14 @@ public void add_parameter_should_add_descriptor_for_media_type_parameter() public void add_parameter_should_add_optional_parameter_when_allowed() { // arrange - var version = new ApiVersion( 1, 0 ); + var version = new ApiVersion( 2.0 ); var description = NewApiDescription( version ); var modelMetadata = new Mock( ModelMetadataIdentity.ForType( typeof( string ) ) ).Object; var options = new ApiExplorerOptions() { - DefaultApiVersion = version, + DefaultApiVersion = ApiVersion.Default, ApiVersionParameterSource = new QueryStringApiVersionReader(), + ApiVersionSelector = new ConstantApiVersionSelector( version ), AssumeDefaultVersionWhenUnspecified = true, }; var context = new ApiVersionParameterDescriptionContext( description, version, modelMetadata, options ); @@ -270,7 +271,7 @@ public void add_parameter_should_add_optional_parameter_when_allowed() Name = "api-version", ModelMetadata = modelMetadata, Source = BindingSource.Query, - DefaultValue = (object) "1.0", + DefaultValue = (object) "2.0", IsRequired = false, Type = typeof( string ), }, diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/Asp.Versioning.Mvc.ApiExplorer.Tests.csproj b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/Asp.Versioning.Mvc.ApiExplorer.Tests.csproj index 100e986c..3fce41d6 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/Asp.Versioning.Mvc.ApiExplorer.Tests.csproj +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/Asp.Versioning.Mvc.ApiExplorer.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + $(DefaultTargetFramework) Asp.Versioning.ApiExplorer diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs index a4761268..24460974 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/DefaultApiVersionDescriptionProviderTest.cs @@ -11,8 +11,11 @@ public void api_version_descriptions_should_collate_expected_versions() { // arrange var descriptionProvider = new DefaultApiVersionDescriptionProvider( - new TestEndpointDataSource(), - new TestActionDescriptorCollectionProvider(), + new IApiVersionMetadataCollationProvider[] + { + new EndpointApiVersionMetadataCollationProvider( new TestEndpointDataSource() ), + new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), + }, Mock.Of(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); @@ -41,8 +44,11 @@ public void api_version_descriptions_should_apply_sunset_policy() policyManager.Setup( pm => pm.TryGetPolicy( default, apiVersion, out expected ) ).Returns( true ); var descriptionProvider = new DefaultApiVersionDescriptionProvider( - new TestEndpointDataSource(), - new TestActionDescriptorCollectionProvider(), + new IApiVersionMetadataCollationProvider[] + { + new EndpointApiVersionMetadataCollationProvider( new TestEndpointDataSource() ), + new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), + }, policyManager.Object, Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs index ba8630d0..9be0191f 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs @@ -14,8 +14,11 @@ public void api_version_descriptions_should_collate_expected_versions() { // arrange var descriptionProvider = new GroupedApiVersionDescriptionProvider( - new TestEndpointDataSource(), - new TestActionDescriptorCollectionProvider(), + new IApiVersionMetadataCollationProvider[] + { + new EndpointApiVersionMetadataCollationProvider( new TestEndpointDataSource() ), + new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), + }, Mock.Of(), Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); @@ -38,7 +41,7 @@ public void api_version_descriptions_should_collate_expected_versions_with_custo { // arrange var provider = new TestActionDescriptorCollectionProvider(); - var source = new CompositeEndpointDataSource( Enumerable.Empty() ); + using var source = new CompositeEndpointDataSource( Enumerable.Empty() ); var data = new ApiDescriptionActionData() { GroupName = "Test" }; foreach ( var descriptor in provider.ActionDescriptors.Items ) @@ -47,8 +50,11 @@ public void api_version_descriptions_should_collate_expected_versions_with_custo } var descriptionProvider = new GroupedApiVersionDescriptionProvider( - source, - provider, + new IApiVersionMetadataCollationProvider[] + { + new EndpointApiVersionMetadataCollationProvider( source ), + new ActionApiVersionMetadataCollationProvider( provider ), + }, Mock.Of(), Options.Create( new ApiExplorerOptions() @@ -82,8 +88,11 @@ public void api_version_descriptions_should_apply_sunset_policy() policyManager.Setup( pm => pm.TryGetPolicy( default, apiVersion, out expected ) ).Returns( true ); var descriptionProvider = new GroupedApiVersionDescriptionProvider( - new TestEndpointDataSource(), - new TestActionDescriptorCollectionProvider(), + new IApiVersionMetadataCollationProvider[] + { + new EndpointApiVersionMetadataCollationProvider( new TestEndpointDataSource() ), + new ActionApiVersionMetadataCollationProvider( new TestActionDescriptorCollectionProvider() ), + }, policyManager.Object, Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs index 3361f93e..4ed2d30e 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestActionDescriptorCollectionProvider.cs @@ -7,7 +7,27 @@ namespace Asp.Versioning.ApiExplorer; internal sealed class TestActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider { - private readonly Lazy collection = new( CreateActionDescriptors ); + private readonly Lazy collection; + + public TestActionDescriptorCollectionProvider() => collection = new( CreateActionDescriptors ); + + public TestActionDescriptorCollectionProvider( ActionDescriptor action, params ActionDescriptor[] otherActions ) + { + ActionDescriptor[] actions; + + if ( otherActions.Length == 0 ) + { + actions = [action]; + } + else + { + actions = new ActionDescriptor[otherActions.Length + 1]; + actions[0] = action; + Array.Copy( otherActions, 0, actions, 1, otherActions.Length ); + } + + collection = new( () => new( actions, 0 ) ); + } public ActionDescriptorCollection ActionDescriptors => collection.Value; @@ -21,7 +41,7 @@ private static ActionDescriptorCollection CreateActionDescriptors() return new( actions.ToArray(), 0 ); } - private static void AddOrderActionDescriptors( ICollection actions ) + private static void AddOrderActionDescriptors( List actions ) { // api version 0.9 and 1.0 actions.Add( @@ -86,7 +106,7 @@ private static void AddOrderActionDescriptors( ICollection act advertised: new ApiVersion[] { new( 4, 0 ) } ) ); } - private static void AddPeopleActionDescriptors( ICollection actions ) + private static void AddPeopleActionDescriptors( List actions ) { // api version 0.9 and 1.0 actions.Add( diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestEndpointDataSource.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestEndpointDataSource.cs index dbe79bcf..b0cd40b3 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestEndpointDataSource.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/TestEndpointDataSource.cs @@ -8,13 +8,13 @@ namespace Asp.Versioning.ApiExplorer; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; -internal class TestEndpointDataSource : EndpointDataSource +internal sealed class TestEndpointDataSource : EndpointDataSource { - public override IReadOnlyList Endpoints { get; } = CreateEndpoints(); + public override List Endpoints { get; } = CreateEndpoints(); public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; - private static IReadOnlyList CreateEndpoints() + private static List CreateEndpoints() { var endpoints = new List(); @@ -24,154 +24,154 @@ private static IReadOnlyList CreateEndpoints() return endpoints; } - private static void AddOrderEndpoints( ICollection endpoints ) + private static void AddOrderEndpoints( List endpoints ) { // api version 0.9 and 1.0 endpoints.Add( NewEndpoint( "GET-orders/{id}", "orders/{id}", - declared: new ApiVersion[] { new( 0, 9 ), new( 1, 0 ) }, - supported: new ApiVersion[] { new( 1, 0 ) }, - deprecated: new ApiVersion[] { new( 0, 9 ) } ) ); + declared: [new( 0, 9 ), new( 1, 0 )], + supported: [new( 1, 0 )], + deprecated: [new( 0, 9 )] ) ); endpoints.Add( NewEndpoint( "POST-orders", "orders", - declared: new ApiVersion[] { new( 1, 0 ) }, - supported: new ApiVersion[] { new( 1, 0 ) } ) ); + declared: [new( 1, 0 )], + supported: [new( 1, 0 )] ) ); // api version 2.0 endpoints.Add( NewEndpoint( "GET-orders", "orders", - declared: new ApiVersion[] { new( 2, 0 ) }, - supported: new ApiVersion[] { new( 2, 0 ) } ) ); + declared: [new( 2, 0 )], + supported: [new( 2, 0 )] ) ); endpoints.Add( NewEndpoint( "GET-orders/{id}", "orders/{id}", - declared: new ApiVersion[] { new( 2, 0 ) }, - supported: new ApiVersion[] { new( 2, 0 ) } ) ); + declared: [new( 2, 0 )], + supported: [new( 2, 0 )] ) ); endpoints.Add( NewEndpoint( "POST-orders", "orders", - declared: new ApiVersion[] { new( 2, 0 ) }, - supported: new ApiVersion[] { new( 2, 0 ) } ) ); + declared: [new( 2, 0 )], + supported: [new( 2, 0 )] ) ); // api version 3.0 endpoints.Add( NewEndpoint( "GET-orders", "orders", - declared: new ApiVersion[] { new( 3, 0 ) }, - supported: new ApiVersion[] { new( 3, 0 ) }, - advertised: new ApiVersion[] { new( 4, 0 ) } ) ); + declared: [new( 3, 0 )], + supported: [new( 3, 0 )], + advertised: [new( 4, 0 )] ) ); endpoints.Add( NewEndpoint( "GET-orders/{id}", "orders/{id}", - declared: new ApiVersion[] { new( 3, 0 ) }, - supported: new ApiVersion[] { new( 3, 0 ) }, - advertised: new ApiVersion[] { new( 4, 0 ) } ) ); + declared: [new( 3, 0 )], + supported: [new( 3, 0 )], + advertised: [new( 4, 0 )] ) ); endpoints.Add( NewEndpoint( "POST-orders", "orders", - declared: new ApiVersion[] { new( 3, 0 ) }, - supported: new ApiVersion[] { new( 3, 0 ) }, - advertised: new ApiVersion[] { new( 4, 0 ) } ) ); + declared: [new( 3, 0 )], + supported: [new( 3, 0 )], + advertised: [new( 4, 0 )] ) ); endpoints.Add( NewEndpoint( "DELETE-orders/{id}", "orders/{id}", - declared: new ApiVersion[] { new( 3, 0 ) }, - supported: new ApiVersion[] { new( 3, 0 ) }, - advertised: new ApiVersion[] { new( 4, 0 ) } ) ); + declared: [new( 3, 0 )], + supported: [new( 3, 0 )], + advertised: [new( 4, 0 )] ) ); } - private static void AddPeopleEndpoints( ICollection endpoints ) + private static void AddPeopleEndpoints( List endpoints ) { // api version 0.9 and 1.0 endpoints.Add( NewEndpoint( "GET-people/{id}", "people/{id}", - declared: new ApiVersion[] { new( 0, 9 ), new( 1, 0 ) }, - supported: new ApiVersion[] { new( 1, 0 ) }, - deprecated: new ApiVersion[] { new( 0, 9 ) } ) ); + declared: [new( 0, 9 ), new( 1, 0 )], + supported: [new( 1, 0 )], + deprecated: [new( 0, 9 )] ) ); endpoints.Add( NewEndpoint( "POST-people", "people", - declared: new ApiVersion[] { new( 1, 0 ) }, - supported: new ApiVersion[] { new( 1, 0 ) } ) ); + declared: [new( 1, 0 )], + supported: [new( 1, 0 )] ) ); // api version 2.0 endpoints.Add( NewEndpoint( "GET-people", "people", - declared: new ApiVersion[] { new( 2, 0 ) }, - supported: new ApiVersion[] { new( 2, 0 ) } ) ); + declared: [new( 2, 0 )], + supported: [new( 2, 0 )] ) ); endpoints.Add( NewEndpoint( "GET-people/{id}", "people/{id}", - declared: new ApiVersion[] { new( 2, 0 ) }, - supported: new ApiVersion[] { new( 2, 0 ) } ) ); + declared: [new( 2, 0 )], + supported: [new( 2, 0 )] ) ); endpoints.Add( NewEndpoint( "POST-people", "people", - declared: new ApiVersion[] { new( 2, 0 ) }, - supported: new ApiVersion[] { new( 2, 0 ) } ) ); + declared: [new( 2, 0 )], + supported: [new( 2, 0 )] ) ); // api version 3.0 endpoints.Add( NewEndpoint( "GET-people", "people", - declared: new ApiVersion[] { new( 3, 0 ) }, - supported: new ApiVersion[] { new( 3, 0 ) }, - advertised: new ApiVersion[] { new( 4, 0 ) } ) ); + declared: [new( 3, 0 )], + supported: [new( 3, 0 )], + advertised: [new( 4, 0 )] ) ); endpoints.Add( NewEndpoint( "GET-people/{id}", "people/{id}", - declared: new ApiVersion[] { new( 3, 0 ) }, - supported: new ApiVersion[] { new( 3, 0 ) }, - advertised: new ApiVersion[] { new( 4, 0 ) } ) ); + declared: [new( 3, 0 )], + supported: [new( 3, 0 )], + advertised: [new( 4, 0 )] ) ); endpoints.Add( NewEndpoint( "POST-people", "people", - declared: new ApiVersion[] { new( 3, 0 ) }, - supported: new ApiVersion[] { new( 3, 0 ) }, - advertised: new ApiVersion[] { new( 4, 0 ) } ) ); + declared: [new( 3, 0 )], + supported: [new( 3, 0 )], + advertised: [new( 4, 0 )] ) ); } private static Endpoint NewEndpoint( string displayName, string pattern, - IEnumerable declared, - IEnumerable supported, - IEnumerable deprecated = null, - IEnumerable advertised = null, - IEnumerable advertisedDeprecated = null ) + ApiVersion[] declared, + ApiVersion[] supported, + ApiVersion[] deprecated = null, + ApiVersion[] advertised = null, + ApiVersion[] advertisedDeprecated = null ) { var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs index f5eec3d5..8af044ae 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/VersionedApiDescriptionProviderTest.cs @@ -2,6 +2,7 @@ namespace Asp.Versioning.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; @@ -92,6 +93,153 @@ public void versioned_api_explorer_should_apply_sunset_policy() .BeTrue(); } + [Fact] + public void versioned_api_explorer_should_preserve_group_name() + { + // arrange + var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( ApiVersion.Default ) ); + var descriptor = new ActionDescriptor() { EndpointMetadata = new[] { metadata } }; + var actionProvider = new TestActionDescriptorCollectionProvider( descriptor ); + var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); + var apiExplorer = new VersionedApiDescriptionProvider( + Mock.Of(), + NewModelMetadataProvider(), + Options.Create( new ApiExplorerOptions() ) ); + + context.Results.Add( new() + { + ActionDescriptor = descriptor, + GroupName = "Test", + } ); + + // act + apiExplorer.OnProvidersExecuted( context ); + + // assert + context.Results.Single().GroupName.Should().Be( "Test" ); + } + + [Fact] + public void versioned_api_explorer_should_use_custom_group_name() + { + // arrange + var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( ApiVersion.Default ) ); + var descriptor = new ActionDescriptor() { EndpointMetadata = new[] { metadata } }; + var actionProvider = new TestActionDescriptorCollectionProvider( descriptor ); + var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); + var options = new ApiExplorerOptions() + { + FormatGroupName = ( group, version ) => $"{group}-{version}", + }; + var apiExplorer = new VersionedApiDescriptionProvider( + Mock.Of(), + NewModelMetadataProvider(), + Options.Create( options ) ); + + context.Results.Add( new() + { + ActionDescriptor = descriptor, + GroupName = "Test", + } ); + + // act + apiExplorer.OnProvidersExecuted( context ); + + // assert + context.Results.Single().GroupName.Should().Be( "Test-1.0" ); + } + + [Fact] + public void versioned_api_explorer_should_prefer_explicit_over_implicit_action_match() + { + // arrange + var @implicit = new ActionDescriptor() + { + DisplayName = "Implicit GET ~/test?api-version=[1.0,2.0]", + EndpointMetadata = new[] + { + new ApiVersionMetadata( + new ApiVersionModel( + new ApiVersion[] { new( 1.0 ), new( 2.0 ) }, + new ApiVersion[] { new( 1.0 ), new( 2.0 ) }, + Array.Empty(), + Array.Empty(), + Array.Empty() ), + new ApiVersionModel( + Array.Empty(), + new ApiVersion[] { new( 1.0 ), new( 2.0 ) }, + Array.Empty(), + Array.Empty(), + Array.Empty() ) ), + }, + }; + var @explicit = new ActionDescriptor() + { + DisplayName = "Explicit GET ~/test?api-version=2.0", + EndpointMetadata = new[] + { + new ApiVersionMetadata( + new ApiVersionModel( + new ApiVersion[] { new( 1.0 ), new( 2.0 ) }, + new ApiVersion[] { new( 1.0 ), new( 2.0 ) }, + Array.Empty(), + Array.Empty(), + Array.Empty() ), + new ApiVersionModel( + new ApiVersion[] { new( 2.0 ) }, + new ApiVersion[] { new( 1.0 ), new( 2.0 ) }, + Array.Empty(), + Array.Empty(), + Array.Empty() ) ), + }, + }; + var actionProvider = new TestActionDescriptorCollectionProvider( @implicit, @explicit ); + var context = new ApiDescriptionProviderContext( actionProvider.ActionDescriptors.Items ); + + context.Results.Add( + new() + { + HttpMethod = "GET", + RelativePath = "test", + ActionDescriptor = context.Actions[0], + } ); + + context.Results.Add( + new() + { + HttpMethod = "GET", + RelativePath = "test", + ActionDescriptor = context.Actions[1], + } ); + + var apiExplorer = new VersionedApiDescriptionProvider( + Mock.Of(), + NewModelMetadataProvider(), + Options.Create( new ApiExplorerOptions() { GroupNameFormat = "'v'VVV" } ) ); + + // act + apiExplorer.OnProvidersExecuted( context ); + + // assert + context.Results.Should().BeEquivalentTo( + new[] + { + new + { + GroupName = "v1", + ActionDescriptor = @implicit, + Properties = new Dictionary() { [typeof( ApiVersion )] = new ApiVersion( 1.0 ) }, + }, + new + { + GroupName = "v2", + ActionDescriptor = @explicit, + Properties = new Dictionary() { [typeof( ApiVersion )] = new ApiVersion( 2.0 ) }, + }, + }, + options => options.ExcludingMissingMembers() ); + } + private static IModelMetadataProvider NewModelMetadataProvider() { var provider = new Mock(); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersionModelBinderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersionModelBinderTest.cs index 76326328..a636c419 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersionModelBinderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersionModelBinderTest.cs @@ -5,7 +5,6 @@ namespace Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; public class ApiVersionModelBinderTest { @@ -59,7 +58,7 @@ private static ModelBindingContext NewModelBindingContext( ApiVersion apiVersion bindingContext.SetupGet( bc => bc.HttpContext ).Returns( httpContext ); bindingContext.SetupProperty( bc => bc.Result ); - bindingContext.SetupProperty( bc => bc.ValidationState, new ValidationStateDictionary() ); + bindingContext.SetupProperty( bc => bc.ValidationState, [] ); return bindingContext.Object; } diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersioningApplicationModelProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersioningApplicationModelProviderTest.cs index f0c56c8c..0d4c2ff9 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersioningApplicationModelProviderTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApiVersioningApplicationModelProviderTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dneutral + namespace Asp.Versioning; using Asp.Versioning.ApplicationModels; diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/ApiBehaviorSpecificationTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/ApiBehaviorSpecificationTest.cs index 8976977a..336a7ba1 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/ApiBehaviorSpecificationTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/ApiBehaviorSpecificationTest.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA1812 namespace Asp.Versioning.ApplicationModels; @@ -32,13 +33,13 @@ public void is_satisfied_by_should_return_expected_result( Type controllerType, private sealed class ApiBehaviorController : ControllerBase { [HttpGet] - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } [Route( "/" )] private sealed class NonApiBehaviorController : Controller { [HttpGet] - public IActionResult Index() => View(); + public ViewResult Index() => View(); } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/DefaultApiControllerFilterTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/DefaultApiControllerFilterTest.cs index a156f8cc..b662cf76 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/DefaultApiControllerFilterTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ApplicationModels/DefaultApiControllerFilterTest.cs @@ -17,9 +17,9 @@ public void apply_should_not_filter_list_without_specifications() var attributes = Array.Empty(); var controllers = new List() { - new ControllerModel( controllerType, attributes ), - new ControllerModel( controllerType, attributes ), - new ControllerModel( controllerType, attributes ), + new( controllerType, attributes ), + new( controllerType, attributes ), + new( controllerType, attributes ), }; // act @@ -44,9 +44,9 @@ public void apply_should_filter_controllers() var attributes = Array.Empty(); var controllers = new List() { - new ControllerModel( controllerType, attributes ), - new ControllerModel( controllerBaseType, attributes ), - new ControllerModel( controllerType, attributes ), + new( controllerType, attributes ), + new( controllerBaseType, attributes ), + new( controllerType, attributes ), }; // act diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/Asp.Versioning.Mvc.Tests.csproj b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/Asp.Versioning.Mvc.Tests.csproj index 4102cf9d..c2fee0ef 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/Asp.Versioning.Mvc.Tests.csproj +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/Asp.Versioning.Mvc.Tests.csproj @@ -1,7 +1,7 @@  - net7.0 + $(DefaultTargetFramework) Asp.Versioning diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs index 301385cd..e06a3e0c 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.Tests/ReportApiVersionsAttributeTest.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dneutral + namespace Asp.Versioning; using Microsoft.AspNetCore.Http; @@ -8,6 +10,7 @@ namespace Asp.Versioning; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; public class ReportApiVersionsAttributeTest { @@ -65,7 +68,7 @@ public async Task on_action_executing_should_not_add_headers_for_versionX2Dneutr private static ActionExecutingContext CreateContext( ApiVersionMetadata metadata, - ICollection<(Func Callback, object State)> onStartResponse ) + List<(Func Callback, object State)> onStartResponse ) { var headers = new HeaderDictionary(); var response = new Mock(); @@ -80,12 +83,14 @@ private static ActionExecutingContext CreateContext( var actionArguments = new Dictionary(); var controller = default( object ); var endpoint = new Endpoint( c => Task.CompletedTask, new( new[] { metadata } ), "Test" ); + var options = Options.Create( new ApiVersioningOptions() ); + var reporter = new DefaultApiVersionReporter( new SunsetPolicyManager( options ) ); endpointFeature.SetupProperty( f => f.Endpoint, endpoint ); versioningFeature.SetupProperty( f => f.RequestedApiVersion, new ApiVersion( 1.0 ) ); features.Set( endpointFeature.Object ); features.Set( versioningFeature.Object ); - serviceProvider.Setup( sp => sp.GetService( typeof( IReportApiVersions ) ) ).Returns( new DefaultApiVersionReporter() ); + serviceProvider.Setup( sp => sp.GetService( typeof( IReportApiVersions ) ) ).Returns( reporter ); response.SetupGet( r => r.Headers ).Returns( headers ); response.SetupGet( r => r.HttpContext ).Returns( () => httpContext.Object ); response.Setup( r => r.OnStarting( It.IsAny>(), It.IsAny() ) ) diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs index 98cdde75..cc75c6e5 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiInformation.cs @@ -22,10 +22,10 @@ public ApiInformation( SunsetPolicy sunsetPolicy, IReadOnlyDictionary openApiDocumentUrls ) { - SupportedApiVersions = supportedVersions ?? throw new ArgumentNullException( nameof( supportedVersions ) ); - DeprecatedApiVersions = deprecatedVersions ?? throw new ArgumentNullException( nameof( deprecatedVersions ) ); - SunsetPolicy = sunsetPolicy ?? throw new ArgumentNullException( nameof( sunsetPolicy ) ); - OpenApiDocumentUrls = openApiDocumentUrls ?? throw new ArgumentNullException( nameof( openApiDocumentUrls ) ); + SupportedApiVersions = supportedVersions ?? throw new System.ArgumentNullException( nameof( supportedVersions ) ); + DeprecatedApiVersions = deprecatedVersions ?? throw new System.ArgumentNullException( nameof( deprecatedVersions ) ); + SunsetPolicy = sunsetPolicy ?? throw new System.ArgumentNullException( nameof( sunsetPolicy ) ); + OpenApiDocumentUrls = openApiDocumentUrls ?? throw new System.ArgumentNullException( nameof( openApiDocumentUrls ) ); } private ApiInformation() diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs index 7a1db927..449e249d 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiNotificationContext.cs @@ -16,8 +16,8 @@ public class ApiNotificationContext /// The requested API version. public ApiNotificationContext( HttpResponseMessage response, ApiVersion apiVersion ) { - Response = response ?? throw new ArgumentNullException( nameof( response ) ); - ApiVersion = apiVersion ?? throw new ArgumentNullException( nameof( apiVersion ) ); + Response = response ?? throw new System.ArgumentNullException( nameof( response ) ); + ApiVersion = apiVersion ?? throw new System.ArgumentNullException( nameof( apiVersion ) ); } /// diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs index 2e0582af..6f940aee 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs @@ -1,17 +1,24 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -namespace Asp.Versioning.Http; - +#pragma warning disable IDE0079 #pragma warning disable CA1815 // Override equals and operator equals on value types +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; /// @@ -25,47 +32,75 @@ public ApiVersionEnumerator( string headerName, IApiVersionParser? parser = default ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } - - if ( string.IsNullOrEmpty( headerName ) ) - { - throw new ArgumentNullException( nameof( headerName ) ); - } - - this.values = - response.Headers.TryGetValues( headerName, out var values ) - ? values - : Enumerable.Empty(); + ArgumentNullException.ThrowIfNull( response ); + ArgumentException.ThrowIfNullOrEmpty( headerName ); + 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/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs index 88f09625..5595251b 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHandler.cs @@ -30,10 +30,10 @@ public ApiVersionHandler( ApiVersion apiVersion, IApiNotification? notification = default, IApiVersionParser? parser = default, - ApiVersionHeaderEnumerable? enumerable = default) + ApiVersionHeaderEnumerable? enumerable = default ) { - this.apiVersionWriter = apiVersionWriter ?? throw new ArgumentNullException( nameof( apiVersionWriter ) ); - this.apiVersion = apiVersion ?? throw new ArgumentNullException( nameof( apiVersion ) ); + this.apiVersionWriter = apiVersionWriter ?? throw new System.ArgumentNullException( nameof( apiVersionWriter ) ); + this.apiVersion = apiVersion ?? throw new System.ArgumentNullException( nameof( apiVersion ) ); this.notification = notification ?? ApiNotification.None; this.parser = parser ?? ApiVersionParser.Default; this.enumerable = enumerable ?? new(); @@ -67,15 +67,12 @@ protected override async Task SendAsync( HttpRequestMessage /// True if the requested API has been deprecated; otherwise, false. protected virtual bool IsDeprecatedApi( HttpResponseMessage response ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } + ArgumentNullException.ThrowIfNull( response ); foreach ( var reportedApiVersion in enumerable.Deprecated( response, parser ) ) { // don't use '==' operator because a derived type may not overload it - if ( apiVersion.CompareTo( reportedApiVersion ) == 0 ) + if ( apiVersion.Equals( reportedApiVersion ) ) { return true; } @@ -91,10 +88,7 @@ protected virtual bool IsDeprecatedApi( HttpResponseMessage response ) /// True if the requested API has a newer, supported version than the one requested; otherwise, false. protected virtual bool IsNewApiAvailable( HttpResponseMessage response ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } + ArgumentNullException.ThrowIfNull( response ); foreach ( var reportedApiVersion in enumerable.Supported( response, parser ) ) { diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs index f1d7aa79..32dc0c69 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. namespace Asp.Versioning.Http; -#pragma warning disable CA1815 // Override equals and operator equals on value types /// /// Represents the enumerable object used to create API version enumerators. @@ -24,15 +23,11 @@ public ApiVersionHeaderEnumerable( string supportedHeaderName = ApiSupportedVersions, string deprecatedHeaderName = ApiDeprecatedVersions ) { - if ( string.IsNullOrEmpty( apiSupportedVersionsName = supportedHeaderName ) ) - { - throw new ArgumentNullException( nameof( supportedHeaderName ) ); - } + ArgumentException.ThrowIfNullOrEmpty( supportedHeaderName ); + ArgumentException.ThrowIfNullOrEmpty( deprecatedHeaderName ); - if ( string.IsNullOrEmpty( apiDeprecatedVersionsName = deprecatedHeaderName ) ) - { - throw new ArgumentNullException( nameof( deprecatedHeaderName ) ); - } + apiSupportedVersionsName = supportedHeaderName; + apiDeprecatedVersionsName = deprecatedHeaderName; } /// diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionWriter.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionWriter.cs index 3c0ab935..09beac30 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionWriter.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionWriter.cs @@ -11,30 +11,27 @@ public static class ApiVersionWriter /// Returns a new API version writer that is a combination of the specified set. /// /// The primary API version writer. - /// An array of the other + /// An array of the other /// API version writers to combine. /// A new, combined API version writer. public static IApiVersionWriter Combine( IApiVersionWriter apiVersionWriter, - params IApiVersionWriter[] otherApiVersionwriters ) + params IApiVersionWriter[] otherApiVersionWriters ) { - if ( apiVersionWriter == null ) - { - throw new ArgumentNullException( nameof( apiVersionWriter ) ); - } + ArgumentNullException.ThrowIfNull( apiVersionWriter ); int count; IApiVersionWriter[] apiVersionWriters; - if ( otherApiVersionwriters is null || ( count = otherApiVersionwriters.Length ) == 0 ) + if ( otherApiVersionWriters is null || ( count = otherApiVersionWriters.Length ) == 0 ) { - apiVersionWriters = new[] { apiVersionWriter }; + apiVersionWriters = [apiVersionWriter]; } else { apiVersionWriters = new IApiVersionWriter[count + 1]; apiVersionWriters[0] = apiVersionWriter; - System.Array.Copy( otherApiVersionwriters, 0, apiVersionWriters, 1, count ); + System.Array.Copy( otherApiVersionWriters, 0, apiVersionWriters, 1, count ); } return new CombinedApiVersionWriter( apiVersionWriters ); @@ -52,19 +49,14 @@ public static IApiVersionWriter Combine( IEnumerable apiVersi if ( writers is null || writers.Length == 0 ) { - throw new ArgumentException( SR.ZeroApiVersionWriters, nameof( apiVersionWriters ) ); + throw new System.ArgumentException( SR.ZeroApiVersionWriters, nameof( apiVersionWriters ) ); } return new CombinedApiVersionWriter( writers ); } - private sealed class CombinedApiVersionWriter : IApiVersionWriter + private sealed class CombinedApiVersionWriter( IApiVersionWriter[] apiVersionWriters ) : IApiVersionWriter { - private readonly IApiVersionWriter[] apiVersionWriters; - - public CombinedApiVersionWriter( IApiVersionWriter[] apiVersionWriters ) => - this.apiVersionWriters = apiVersionWriters; - public void Write( HttpRequestMessage request, ApiVersion apiVersion ) { for ( var i = 0; i < apiVersionWriters.Length; i++ ) 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 42d38f19..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,25 +1,33 @@  - 7.0.0 - 7.0.0.0 - net7.0;netstandard1.1;netstandard2.0 + 8.1.0 + 8.1.0.0 + $(DefaultTargetFramework);netstandard1.1;netstandard2.0 Asp.Versioning.Http API Versioning Client Extensions The HTTP client extensions library for API versioning. Asp;AspNet;AspNetCore;Versioning;Http + + true + + - - + + - - + + + + + + @@ -27,11 +35,18 @@ - - + + + + + + + + + diff --git a/src/Client/src/Asp.Versioning.Http.Client/HeaderApiVersionWriter.cs b/src/Client/src/Asp.Versioning.Http.Client/HeaderApiVersionWriter.cs index 3735420d..5055db6a 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/HeaderApiVersionWriter.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/HeaderApiVersionWriter.cs @@ -15,26 +15,15 @@ public sealed class HeaderApiVersionWriter : IApiVersionWriter /// The HTTP header name to write the API version to. public HeaderApiVersionWriter( string headerName ) { - if ( string.IsNullOrEmpty( headerName ) ) - { - throw new ArgumentNullException( headerName ); - } - + ArgumentException.ThrowIfNullOrEmpty( headerName ); this.headerName = headerName; } /// public void Write( HttpRequestMessage request, ApiVersion apiVersion ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( request ); + ArgumentNullException.ThrowIfNull( apiVersion ); var headers = request.Headers; diff --git a/src/Client/src/Asp.Versioning.Http.Client/MediaTypeApiVersionWriter.cs b/src/Client/src/Asp.Versioning.Http.Client/MediaTypeApiVersionWriter.cs index c9901c23..792c6379 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/MediaTypeApiVersionWriter.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/MediaTypeApiVersionWriter.cs @@ -24,26 +24,15 @@ public sealed class MediaTypeApiVersionWriter : IApiVersionWriter /// The name of the media type parameter to write the API version to. public MediaTypeApiVersionWriter( string parameterName ) { - if ( string.IsNullOrEmpty( parameterName ) ) - { - throw new ArgumentNullException( parameterName ); - } - + ArgumentException.ThrowIfNullOrEmpty( parameterName ); this.parameterName = parameterName; } /// public void Write( HttpRequestMessage request, ApiVersion apiVersion ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( request ); + ArgumentNullException.ThrowIfNull( apiVersion ); UpdateAccept( request, apiVersion ); diff --git a/src/Client/src/Asp.Versioning.Http.Client/QueryStringApiVersionWriter.cs b/src/Client/src/Asp.Versioning.Http.Client/QueryStringApiVersionWriter.cs index eb72b775..caaba7d0 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/QueryStringApiVersionWriter.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/QueryStringApiVersionWriter.cs @@ -23,26 +23,15 @@ public sealed class QueryStringApiVersionWriter : IApiVersionWriter /// The query string parameter name to write the API version to. public QueryStringApiVersionWriter( string parameterName ) { - if ( string.IsNullOrEmpty( parameterName ) ) - { - throw new ArgumentNullException( parameterName ); - } - + ArgumentException.ThrowIfNullOrEmpty( parameterName ); this.parameterName = parameterName; } /// public void Write( HttpRequestMessage request, ApiVersion apiVersion ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( request ); + ArgumentNullException.ThrowIfNull( apiVersion ); if ( request.RequestUri is not Uri url || url.Query.Contains( parameterName, OrdinalIgnoreCase ) ) diff --git a/src/Client/src/Asp.Versioning.Http.Client/SR.Designer.cs b/src/Client/src/Asp.Versioning.Http.Client/SR.Designer.cs index 8bdaa5bd..85db8768 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/SR.Designer.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/SR.Designer.cs @@ -61,6 +61,15 @@ internal SR() { } } + /// + /// Looks up a localized string similar to The value cannot be an empty string.. + /// + internal static string Argument_EmptyString { + get { + return ResourceManager.GetString("Argument_EmptyString", resourceCulture); + } + } + /// /// Looks up a localized string similar to At least one IApiVersionWriter must be specified.. /// diff --git a/src/Client/src/Asp.Versioning.Http.Client/SR.resx b/src/Client/src/Asp.Versioning.Http.Client/SR.resx index d283a951..4f8009c1 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/SR.resx +++ b/src/Client/src/Asp.Versioning.Http.Client/SR.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The value cannot be an empty string. + At least one IApiVersionWriter must be specified. diff --git a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs index dc38f355..b42c2d28 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpClientExtensions.cs @@ -5,6 +5,9 @@ namespace System.Net.Http; using Asp.Versioning; using Asp.Versioning.Http; using static System.Net.HttpStatusCode; +#if NETSTANDARD +using ArgumentNullException = Backport.ArgumentNullException; +#endif /// /// Provides extension methods for . @@ -58,10 +61,7 @@ public static async Task GetApiInformationAsync( ApiVersionHeaderEnumerable? enumerable = default, CancellationToken cancellationToken = default ) { - if ( client == null ) - { - throw new ArgumentNullException( nameof( client ) ); - } + ArgumentNullException.ThrowIfNull( client ); using var request = new HttpRequestMessage( HttpMethod.Options, requestUrl ); var response = await client.SendAsync( request, cancellationToken ).ConfigureAwait( false ); diff --git a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs index 5c70fd20..0a430a32 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/System.Net.Http/HttpResponseMessageExtensions.cs @@ -5,6 +5,9 @@ namespace System.Net.Http; using Asp.Versioning; using System.Globalization; using static System.StringComparison; +#if NETSTANDARD +using ArgumentNullException = Backport.ArgumentNullException; +#endif /// /// Provides extension methods for . @@ -21,10 +24,7 @@ public static class HttpResponseMessageExtensions /// A new sunset policy. public static SunsetPolicy ReadSunsetPolicy( this HttpResponseMessage response ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } + ArgumentNullException.ThrowIfNull( response ); var headers = response.Headers; var date = default( DateTimeOffset ); @@ -80,10 +80,7 @@ public static IReadOnlyDictionary GetOpenApiDocumentUrls( this HttpResponseMessage response, IApiVersionParser? parser = default ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } + ArgumentNullException.ThrowIfNull( response ); var urls = default( Dictionary ); @@ -102,7 +99,7 @@ public static IReadOnlyDictionary GetOpenApiDocumentUrls( } var key = GetApiVersionExtension( link, ref parser ); - urls ??= new(); + urls ??= []; urls[key] = link.LinkTarget; } diff --git a/src/Client/src/Asp.Versioning.Http.Client/UrlSegmentApiVersionWriter.cs b/src/Client/src/Asp.Versioning.Http.Client/UrlSegmentApiVersionWriter.cs index 4d8ac553..7237924b 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/UrlSegmentApiVersionWriter.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/UrlSegmentApiVersionWriter.cs @@ -18,26 +18,15 @@ public sealed class UrlSegmentApiVersionWriter : IApiVersionWriter /// The replacement token to write the API version to. public UrlSegmentApiVersionWriter( string token ) { - if ( string.IsNullOrEmpty( token ) ) - { - throw new ArgumentNullException( token ); - } - + ArgumentException.ThrowIfNullOrEmpty( token ); this.token = token; } /// public void Write( HttpRequestMessage request, ApiVersion apiVersion ) { - if ( request == null ) - { - throw new ArgumentNullException( nameof( request ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( request ); + ArgumentNullException.ThrowIfNull( apiVersion ); if ( request.RequestUri is not Uri url ) { diff --git a/src/Client/src/Asp.Versioning.Http.Client/net7.0/ApiVersionHandlerLogger{T}.cs b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ApiVersionHandlerLogger{T}.cs similarity index 80% rename from src/Client/src/Asp.Versioning.Http.Client/net7.0/ApiVersionHandlerLogger{T}.cs rename to src/Client/src/Asp.Versioning.Http.Client/net#.0/ApiVersionHandlerLogger{T}.cs index 0de53c22..4f8a8b86 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/net7.0/ApiVersionHandlerLogger{T}.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ApiVersionHandlerLogger{T}.cs @@ -23,18 +23,15 @@ public class ApiVersionHandlerLogger : ApiNotification /// The enumerable used to enumerate API versions. public ApiVersionHandlerLogger( ILogger logger, IApiVersionParser parser, ApiVersionHeaderEnumerable enumerable ) { - this.logger = logger ?? throw new ArgumentNullException( nameof( logger ) ); - this.parser = parser ?? throw new ArgumentNullException( nameof( parser ) ); - this.enumerable = enumerable ?? throw new ArgumentNullException( nameof( enumerable ) ); + this.logger = logger ?? throw new System.ArgumentNullException( nameof( logger ) ); + this.parser = parser ?? throw new System.ArgumentNullException( nameof( parser ) ); + this.enumerable = enumerable ?? throw new System.ArgumentNullException( nameof( enumerable ) ); } /// protected override void OnApiDeprecated( ApiNotificationContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var requestUrl = context.Response.RequestMessage!.RequestUri!; var apiVersion = context.ApiVersion; @@ -46,10 +43,7 @@ protected override void OnApiDeprecated( ApiNotificationContext context ) /// protected override void OnNewApiAvailable( ApiNotificationContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var requestUrl = context.Response.RequestMessage!.RequestUri!; var currentApiVersion = context.ApiVersion; diff --git a/src/Client/src/Asp.Versioning.Http.Client/net7.0/DependencyInjection/IHttpClientBuilderExtensions.cs b/src/Client/src/Asp.Versioning.Http.Client/net#.0/DependencyInjection/IHttpClientBuilderExtensions.cs similarity index 95% rename from src/Client/src/Asp.Versioning.Http.Client/net7.0/DependencyInjection/IHttpClientBuilderExtensions.cs rename to src/Client/src/Asp.Versioning.Http.Client/net#.0/DependencyInjection/IHttpClientBuilderExtensions.cs index 37f744eb..08f24e42 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/net7.0/DependencyInjection/IHttpClientBuilderExtensions.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/net#.0/DependencyInjection/IHttpClientBuilderExtensions.cs @@ -103,10 +103,7 @@ public static IHttpClientBuilder AddApiVersion( ApiVersion apiVersion, IApiVersionWriter? apiVersionWriter = default ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } + ArgumentNullException.ThrowIfNull( builder ); var services = builder.Services; @@ -131,7 +128,7 @@ private static ApiVersionHandler NewApiVersionHandler( return new( writer, apiVersion, notification, parser ); } - private static IApiNotification? BuildFallbackNotification( + private static ApiVersionHandlerLogger? BuildFallbackNotification( IServiceProvider serviceProvider, IApiVersionParser? parser ) { @@ -145,9 +142,6 @@ private static ApiVersionHandler NewApiVersionHandler( var enumerable = serviceProvider.GetService(); - return new ApiVersionHandlerLogger( - logger, - parser ?? ApiVersionParser.Default, - enumerable ?? new() ); + return new( logger, parser ?? ApiVersionParser.Default, enumerable ?? new() ); } } \ No newline at end of file diff --git a/src/Client/src/Asp.Versioning.Http.Client/net7.0/ILoggerExtensions.cs b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ILoggerExtensions.cs similarity index 99% rename from src/Client/src/Asp.Versioning.Http.Client/net7.0/ILoggerExtensions.cs rename to src/Client/src/Asp.Versioning.Http.Client/net#.0/ILoggerExtensions.cs index 6a575ca7..a7535807 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/net7.0/ILoggerExtensions.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/net#.0/ILoggerExtensions.cs @@ -74,7 +74,7 @@ private static string[] FormatLinks( SunsetPolicy sunsetPolicy ) { if ( !sunsetPolicy.HasLinks ) { - return Array.Empty(); + return []; } // (<Language>[,<Language>]): <Url> 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 diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/Asp.Versioning.Http.Client.Tests.csproj b/src/Client/test/Asp.Versioning.Http.Client.Tests/Asp.Versioning.Http.Client.Tests.csproj index a298c9b6..5612c3c3 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/Asp.Versioning.Http.Client.Tests.csproj +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/Asp.Versioning.Http.Client.Tests.csproj @@ -1,19 +1,19 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFrameworks>net7.0;net452;net472</TargetFrameworks> + <TargetFrameworks>$(DefaultTargetFramework);net452;net472</TargetFrameworks> <RootNamespace>Asp.Versioning.Http</RootNamespace> </PropertyGroup> <ItemGroup Condition=" '$(TargetFramework)' == 'net452' "> - <Compile Remove="net7.0\**\*.cs" /> - <None Include="net7.0\**\*.cs" /> + <Compile Remove="net#.0\**\*.cs" /> + <None Include="net#.0\**\*.cs" /> </ItemGroup> <ItemGroup Condition="'$(TargetFramework)' != 'net452'"> - <PackageReference Include="MELT" Version="0.8.0" /> + <PackageReference Include="MELT" Version="0.9.0" /> </ItemGroup> - + <ItemGroup> <ProjectReference Include="..\..\src\Asp.Versioning.Http.Client\Asp.Versioning.Http.Client.csproj" /> </ItemGroup> diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/net7.0/ApiVersionHandlerLoggerTTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs similarity index 100% rename from src/Client/test/Asp.Versioning.Http.Client.Tests/net7.0/ApiVersionHandlerLoggerTTest.cs rename to src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/ApiVersionHandlerLoggerTTest.cs diff --git a/src/Client/test/Asp.Versioning.Http.Client.Tests/net7.0/DependencyInjection/IHttpClientBuilderExtensionsTest.cs b/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/DependencyInjection/IHttpClientBuilderExtensionsTest.cs similarity index 99% rename from src/Client/test/Asp.Versioning.Http.Client.Tests/net7.0/DependencyInjection/IHttpClientBuilderExtensionsTest.cs rename to src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/DependencyInjection/IHttpClientBuilderExtensionsTest.cs index db8c33fa..afd2f19c 100644 --- a/src/Client/test/Asp.Versioning.Http.Client.Tests/net7.0/DependencyInjection/IHttpClientBuilderExtensionsTest.cs +++ b/src/Client/test/Asp.Versioning.Http.Client.Tests/net#.0/DependencyInjection/IHttpClientBuilderExtensionsTest.cs @@ -90,8 +90,6 @@ public void add_api_version_should_register_transient_header_enumerable() result1.Should().NotBeSameAs( result2 ); } -#pragma warning disable CA1812 - private sealed class LastHandler : DelegatingHandler { public HttpRequestMessage Request { get; private set; } diff --git a/src/Common/src/Common.ApiExplorer/ApiExplorerOptions.cs b/src/Common/src/Common.ApiExplorer/ApiExplorerOptions.cs index 6cf69948..2cd629ce 100644 --- a/src/Common/src/Common.ApiExplorer/ApiExplorerOptions.cs +++ b/src/Common/src/Common.ApiExplorer/ApiExplorerOptions.cs @@ -2,11 +2,19 @@ namespace Asp.Versioning.ApiExplorer; +#if NETFRAMEWORK +using HttpRequest = System.Net.Http.HttpRequestMessage; +#else +using Microsoft.AspNetCore.Http; +#endif + /// <summary> /// Represents the possible API versioning options for the API explorer. /// </summary> public partial class ApiExplorerOptions { + private IApiVersionSelector? apiVersionSelector; + /// <summary> /// Gets or sets the format used to create group names from API versions. /// </summary> diff --git a/src/Common/src/Common.Backport/ArgumentException.cs b/src/Common/src/Common.Backport/ArgumentException.cs new file mode 100644 index 00000000..5f52d82e --- /dev/null +++ b/src/Common/src/Common.Backport/ArgumentException.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +// REF: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/ArgumentException.cs +namespace Backport; + +using System.Runtime.CompilerServices; + +internal static class ArgumentException +{ + /// <summary>Throws an exception if <paramref name="argument"/> is null or empty.</summary> + /// <param name="argument">The string argument to validate as non-null and non-empty.</param> + /// <param name="paramName">The name of the parameter with which <paramref name="argument"/> corresponds.</param> + /// <exception cref="ArgumentNullException"><paramref name="argument"/> is null.</exception> + /// <exception cref="ArgumentException"><paramref name="argument"/> is empty.</exception> + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static void ThrowIfNullOrEmpty( string? argument, [CallerArgumentExpression( nameof( argument ) )] string? paramName = null ) + { + if ( string.IsNullOrEmpty( argument ) ) + { + ThrowNullOrEmptyException( argument, paramName ); + } + } + + [DoesNotReturn] + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static void ThrowNullOrEmptyException( string? argument, string? paramName ) + { + ArgumentNullException.ThrowIfNull( argument, paramName ); + throw new System.ArgumentException( BackportSR.Argument_EmptyString, paramName ); + } +} \ No newline at end of file diff --git a/src/Common/src/Common.Backport/ArgumentNullException.cs b/src/Common/src/Common.Backport/ArgumentNullException.cs new file mode 100644 index 00000000..4b74beaf --- /dev/null +++ b/src/Common/src/Common.Backport/ArgumentNullException.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +// REF: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs +namespace Backport; + +using System.Runtime.CompilerServices; + +internal static class ArgumentNullException +{ + /// <summary>Throws an <see cref="System.ArgumentNullException"/> if <paramref name="argument"/> is null.</summary> + /// <param name="argument">The reference type argument to validate as non-null.</param> + /// <param name="paramName">The name of the parameter with which <paramref name="argument"/> corresponds.</param> + [MethodImpl( MethodImplOptions.AggressiveInlining )] + public static void ThrowIfNull( [NotNull] object? argument, [CallerArgumentExpression( nameof( argument ) )] string? paramName = null ) + { + if ( argument is null ) + { + throw new System.ArgumentNullException( paramName ); + } + } +} \ No newline at end of file diff --git a/src/Common/src/Common.Backport/BitOperations.cs b/src/Common/src/Common.Backport/BitOperations.cs index bbbf1cdc..316bfb02 100644 --- a/src/Common/src/Common.Backport/BitOperations.cs +++ b/src/Common/src/Common.Backport/BitOperations.cs @@ -1,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 + // REF: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Numerics/BitOperations.cs #pragma warning disable CS3019 // CLS compliance checking will not be performed because it is not visible from outside this assembly diff --git a/src/Common/src/Common.Backport/CallerArgumentExpressionAttribute.cs b/src/Common/src/Common.Backport/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..329f7fe1 --- /dev/null +++ b/src/Common/src/Common.Backport/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +// REF: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CallerArgumentExpressionAttribute.cs +namespace System.Runtime.CompilerServices +{ + [AttributeUsage( AttributeTargets.Parameter, AllowMultiple = false, Inherited = false )] + internal sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute( string parameterName ) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } + } +} \ No newline at end of file diff --git a/src/Common/src/Common.Backport/HashCode.cs b/src/Common/src/Common.Backport/HashCode.cs index 1069aa84..d9d2794a 100644 --- a/src/Common/src/Common.Backport/HashCode.cs +++ b/src/Common/src/Common.Backport/HashCode.cs @@ -1,11 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable IDE0007 // Use implicit type #pragma warning disable IDE0079 // Remove unnecessary suppression -#pragma warning disable CA1065 // Do not raise exceptions in unexpected locations -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -#pragma warning disable CA1815 // Override equals and operator equals on value types -#pragma warning disable CA2231 // Overload operator equals on overriding value type Equals +#pragma warning disable IDE0251 // Make member 'readonly' #pragma warning disable SA1108 // Block statements should not contain embedded comments #pragma warning disable SA1132 // Do not combine fields #pragma warning disable SA1200 // Using directives should be placed correctly @@ -66,8 +64,6 @@ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT using System.Numerics; using System.Runtime.CompilerServices; -#pragma warning disable CA1066 // Implement IEquatable when overriding Object.Equals - namespace System { internal struct HashCode @@ -100,10 +96,7 @@ private static uint GenerateGlobalSeed() var epoch = new DateTime(2000, 1, 1); var seed = (int) Math.Ceiling(DateTime.Now.Subtract( epoch ).TotalSeconds); var random = new Random( seed ); - -#pragma warning disable CA5394 // Do not use insecure randomness random.NextBytes( data ); -#pragma warning restore CA5394 // Do not use insecure randomness #endif return BitConverter.ToUInt32( data, 0 ); } diff --git a/src/Common/src/Common.Backport/NullableAttributes.cs b/src/Common/src/Common.Backport/NullableAttributes.cs new file mode 100644 index 00000000..2cadf664 --- /dev/null +++ b/src/Common/src/Common.Backport/NullableAttributes.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +#pragma warning disable IDE0060 +#pragma warning disable IDE0079 +#pragma warning disable SA1402 +#pragma warning disable SA1649 + +// REF: https://github.com/dotnet/runtime/blob/1c8d37af80667daffb3cb80ce0fe915621e8f039/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs +// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage( AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false )] +internal sealed class AllowNullAttribute : Attribute { } + +[AttributeUsage( AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false )] +internal sealed class DisallowNullAttribute : Attribute { } + +[AttributeUsage( AttributeTargets.Method, Inherited = false )] +internal sealed class DoesNotReturnAttribute : Attribute { } + +[AttributeUsage( AttributeTargets.Parameter )] +internal sealed class DoesNotReturnIfAttribute : Attribute +{ + public DoesNotReturnIfAttribute( bool parameterValue ) { } + + public bool ParameterValue => default; +} + +[AttributeUsage( AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Struct, Inherited = false, AllowMultiple = false )] +internal sealed class ExcludeFromCodeCoverageAttribute : Attribute { } + +[AttributeUsage( AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false )] +internal sealed class MaybeNullAttribute : Attribute { } + +[AttributeUsage( AttributeTargets.Parameter )] +internal sealed class MaybeNullWhenAttribute : Attribute +{ + public MaybeNullWhenAttribute( bool returnValue ) { } + + public bool ReturnValue => default; +} + +[AttributeUsage( AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false )] +internal sealed class NotNullAttribute : Attribute { } + +[AttributeUsage( AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false )] +internal sealed class NotNullIfNotNullAttribute : Attribute +{ + public NotNullIfNotNullAttribute( string parameterName ) { } + + public string ParameterName => default!; +} + +[AttributeUsage( AttributeTargets.Parameter )] +internal sealed class NotNullWhenAttribute : Attribute +{ + public NotNullWhenAttribute( bool returnValue ) { } + + public bool ReturnValue => default; +} \ No newline at end of file diff --git a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder.cs b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder.cs index 7e751efe..c72ece92 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder.cs @@ -18,7 +18,7 @@ public class ActionApiVersionConventionBuilder : ActionApiVersionConventionBuild /// <param name="controllerBuilder">The <see cref="ControllerApiVersionConventionBuilder">controller builder</see> /// the action builder belongs to.</param> public ActionApiVersionConventionBuilder( ControllerApiVersionConventionBuilder controllerBuilder ) - : base( ( controllerBuilder ?? throw new ArgumentNullException( nameof( controllerBuilder ) ) ).NamingConvention ) + : base( ( controllerBuilder ?? throw new System.ArgumentNullException( nameof( controllerBuilder ) ) ).NamingConvention ) { ControllerBuilder = controllerBuilder; } diff --git a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs index d391caad..99aa8402 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderBase.cs @@ -29,7 +29,7 @@ public abstract partial class ActionApiVersionConventionBuilderBase : ApiVersion /// Gets the collection of API versions mapped to the current action. /// </summary> /// <value>A <see cref="ICollection{T}">collection</see> of mapped <see cref="ApiVersion">API versions</see>.</value> - protected ICollection<ApiVersion> MappedVersions => mapped ??= new(); + protected ICollection<ApiVersion> MappedVersions => mapped ??= []; /// <summary> /// Gets the controller naming convention associated with the builder. @@ -40,10 +40,7 @@ public abstract partial class ActionApiVersionConventionBuilderBase : ApiVersion /// <inheritdoc /> protected override void MergeAttributesWithConventions( IReadOnlyList<object> attributes ) { - if ( attributes == null ) - { - throw new ArgumentNullException( nameof( attributes ) ); - } + ArgumentNullException.ThrowIfNull( attributes ); base.MergeAttributesWithConventions( attributes ); diff --git a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection.cs b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection.cs index c0d226e7..f2673f0d 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection.cs @@ -36,7 +36,7 @@ protected internal virtual ActionApiVersionConventionBuilder GetOrAdd( MethodInf if ( actionBuilderMappings is null ) { mapping = new( actionMethod, new( controllerBuilder ) ); - actionBuilderMappings = new() { mapping }; + actionBuilderMappings = [mapping]; return mapping.Builder; } @@ -64,12 +64,7 @@ protected internal virtual ActionApiVersionConventionBuilder GetOrAdd( MethodInf /// <param name="actionMethod">The controller action method to get the convention builder for.</param> /// <param name="actionBuilder">The <see cref="ActionApiVersionConventionBuilder">controller action convention builder</see> or <c>null</c>.</param> /// <returns>True if the <paramref name="actionBuilder">action builder</paramref> is successfully retrieved; otherwise, false.</returns> - public virtual bool TryGetValue( - MethodInfo? actionMethod, -#if !NETFRAMEWORK - [MaybeNullWhen( false )] -#endif - out ActionApiVersionConventionBuilder actionBuilder ) + public virtual bool TryGetValue( MethodInfo? actionMethod, [MaybeNullWhen( false )] out ActionApiVersionConventionBuilder actionBuilder ) { if ( actionBuilderMappings == null || actionMethod == null ) { diff --git a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection{T}.cs b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection{T}.cs index 3547e92d..ff19f2b7 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection{T}.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilderCollection{T}.cs @@ -45,7 +45,7 @@ protected internal virtual ActionApiVersionConventionBuilder<T> GetOrAdd( Method if ( actionBuilderMappings is null ) { mapping = new( actionMethod, new( controllerBuilder ) ); - actionBuilderMappings = new() { mapping }; + actionBuilderMappings = [mapping]; return mapping.Builder; } @@ -73,12 +73,7 @@ protected internal virtual ActionApiVersionConventionBuilder<T> GetOrAdd( Method /// <param name="actionMethod">The controller action method to get the convention builder for.</param> /// <param name="actionBuilder">The <see cref="ActionApiVersionConventionBuilder{T}">controller action convention builder</see> or <c>null</c>.</param> /// <returns>True if the <paramref name="actionBuilder">action builder</paramref> is successfully retrieved; otherwise, false.</returns> - public virtual bool TryGetValue( - MethodInfo? actionMethod, -#if !NETFRAMEWORK - [MaybeNullWhen( false )] -#endif - out ActionApiVersionConventionBuilder<T> actionBuilder ) + public virtual bool TryGetValue( MethodInfo? actionMethod, [MaybeNullWhen( false )] out ActionApiVersionConventionBuilder<T> actionBuilder ) { if ( actionBuilderMappings == null || actionMethod == null ) { diff --git a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder{T}.cs b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder{T}.cs index 26a83013..0531a70a 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder{T}.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionApiVersionConventionBuilder{T}.cs @@ -31,7 +31,7 @@ public class ActionApiVersionConventionBuilder<T> : /// <param name="controllerBuilder">The <see cref="ControllerApiVersionConventionBuilder{T}">controller builder</see> /// the action builder belongs to.</param> public ActionApiVersionConventionBuilder( ControllerApiVersionConventionBuilder<T> controllerBuilder ) - : base( ( controllerBuilder ?? throw new ArgumentNullException( nameof( controllerBuilder ) ) ).NamingConvention ) + : base( ( controllerBuilder ?? throw new System.ArgumentNullException( nameof( controllerBuilder ) ) ).NamingConvention ) { ControllerBuilder = controllerBuilder; } @@ -116,9 +116,11 @@ public virtual ActionApiVersionConventionBuilder<T> AdvertisesDeprecatedApiVersi return this; } +#pragma warning disable IDE0079 #pragma warning disable CA1033 // Interface methods should be callable by child types Type IActionConventionBuilder.ControllerType => typeof( T ); #pragma warning restore CA1033 // Interface methods should be callable by child types +#pragma warning restore IDE0079 void IDeclareApiVersionConventionBuilder.IsApiVersionNeutral() => IsApiVersionNeutral(); diff --git a/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs b/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs index c4c82a76..98cb8aa3 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs @@ -35,16 +35,8 @@ public static IActionConventionBuilder<TController> Action<TController>( where TController : notnull #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( actionExpression == null ) - { - throw new ArgumentNullException( nameof( actionExpression ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( actionExpression ); return builder.Action( actionExpression.ExtractMethod() ); } @@ -65,16 +57,8 @@ public static IActionConventionBuilder<TController> Action<TController, TResult> where TController : notnull #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( actionExpression == null ) - { - throw new ArgumentNullException( nameof( actionExpression ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( actionExpression ); return builder.Action( actionExpression.ExtractMethod() ); } @@ -91,11 +75,7 @@ public static IActionConventionBuilder<TController> Action<TController, TResult> /// methods that have the <see cref="NonActionAttribute"/> applied will also be ignored.</remarks> public static IActionConventionBuilder Action( this IActionConventionBuilder builder, string methodName, params Type[] argumentTypes ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); var method = ActionMethodResolver.Resolve( builder.ControllerType, methodName, argumentTypes ); return builder.Action( method ); } diff --git a/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs b/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs index 367f4e8d..3c0fa420 100644 --- a/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs +++ b/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs @@ -20,7 +20,7 @@ internal static MethodInfo Resolve( Type controllerType, string methodName, Type switch ( methods.Length ) { case 0: - throw new MissingMethodException( string.Format( CultureInfo.CurrentCulture, MvcSR.ActionMethodNotFound, methodName ) ); + throw new MissingMethodException( string.Format( CultureInfo.CurrentCulture, MvcFormat.ActionMethodNotFound, methodName ) ); case 1: return methods[0]; } @@ -33,7 +33,7 @@ internal static MethodInfo Resolve( Type controllerType, string methodName, Type return methods[0]; } - throw new AmbiguousMatchException( string.Format( CultureInfo.CurrentCulture, MvcSR.AmbiguousActionMethod, methodName ) ); + throw new AmbiguousMatchException( string.Format( CultureInfo.CurrentCulture, MvcFormat.AmbiguousActionMethod, methodName ) ); } private static bool IsAction( MethodInfo method ) => diff --git a/src/Common/src/Common.Mvc/Conventions/ApiVersionConventionBuilder.cs b/src/Common/src/Common.Mvc/Conventions/ApiVersionConventionBuilder.cs index 1f94e935..6a18e4f8 100644 --- a/src/Common/src/Common.Mvc/Conventions/ApiVersionConventionBuilder.cs +++ b/src/Common/src/Common.Mvc/Conventions/ApiVersionConventionBuilder.cs @@ -40,13 +40,13 @@ public partial class ApiVersionConventionBuilder : IApiVersionConventionBuilder /// Gets a collection of controller convention builders. /// </summary> /// <value>A <see cref="IDictionary{TKey, TValue}">collection</see> of <see cref="IControllerConventionBuilder">controller convention builders</see>.</value> - protected IDictionary<Type, IControllerConventionBuilder> ControllerConventionBuilders => controllerConventionBuilders ??= new(); + protected IDictionary<Type, IControllerConventionBuilder> ControllerConventionBuilders => controllerConventionBuilders ??= []; /// <summary> /// Gets a collection of controller conventions. /// </summary> /// <value>A <see cref="IList{T}">list</see> of <see cref="IControllerConvention">controller conventions</see>.</value> - protected IList<IControllerConvention> ControllerConventions => controllerConventions ??= new(); + protected IList<IControllerConvention> ControllerConventions => controllerConventions ??= []; /// <inheritdoc /> public virtual int Count => @@ -77,7 +77,7 @@ public virtual IControllerConventionBuilder<TController> Controller<TController> // this should only ever happen if a subclass overrides Controller(Type) and adds a // IControllerConventionBuilder that is not covariant with IControllerConventionBuilder<TController> - var message = string.Format( CultureInfo.CurrentCulture, MvcSR.ConventionStyleMismatch, key.Name ); + var message = string.Format( CultureInfo.CurrentCulture, MvcFormat.ConventionStyleMismatch, key.Name ); throw new InvalidOperationException( message ); } @@ -100,10 +100,7 @@ public virtual IControllerConventionBuilder Controller( Type controllerType ) /// <inheritdoc /> public virtual bool ApplyTo( ControllerModel controller ) { - if ( controller == null ) - { - throw new ArgumentNullException( nameof( controller ) ); - } + ArgumentNullException.ThrowIfNull( controller ); IControllerConventionBuilder? builder; bool hasExplicitConventions; diff --git a/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder.cs b/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder.cs index e1f1c1ec..fedac8b2 100644 --- a/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder.cs +++ b/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder.cs @@ -44,7 +44,7 @@ public ControllerApiVersionConventionBuilder( Type controllerType, IControllerNa if ( !webApiController.IsAssignableFrom( controllerType ) ) { var message = string.Format( CultureInfo.CurrentCulture, MvcSR.RequiredInterfaceNotImplemented, controllerType, webApiController ); - throw new ArgumentException( message, nameof( controllerType ) ); + throw new System.ArgumentException( message, nameof( controllerType ) ); } #endif ControllerType = controllerType; @@ -127,12 +127,7 @@ public virtual ControllerApiVersionConventionBuilder AdvertisesDeprecatedApiVers /// <param name="method">The <see cref="MethodInfo">method</see> representing the action to retrieve the convention for.</param> /// <param name="convention">The retrieved <see cref="IApiVersionConvention{T}">convention</see> or <c>null</c>.</param> /// <returns>True if the convention was successfully retrieved; otherwise, false.</returns> - protected override bool TryGetConvention( - MethodInfo method, -#if !NETFRAMEWORK - [MaybeNullWhen( false )] -#endif - out IApiVersionConvention<ActionModel> convention ) + protected override bool TryGetConvention( MethodInfo method, [MaybeNullWhen( false )] out IApiVersionConvention<ActionModel> convention ) { if ( actionBuilders is not null && actionBuilders.TryGetValue( method, out var actionBuilder ) ) diff --git a/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder{T}.cs b/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder{T}.cs index ea1719c0..6babc238 100644 --- a/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder{T}.cs +++ b/src/Common/src/Common.Mvc/Conventions/ControllerApiVersionConventionBuilder{T}.cs @@ -120,12 +120,7 @@ public virtual ControllerApiVersionConventionBuilder<T> AdvertisesDeprecatedApiV /// <param name="method">The <see cref="MethodInfo">method</see> representing the action to retrieve the convention for.</param> /// <param name="convention">The retrieved <see cref="IApiVersionConvention{T}">convention</see> or <c>null</c>.</param> /// <returns>True if the convention was successfully retrieved; otherwise, false.</returns> - protected override bool TryGetConvention( - MethodInfo method, -#if !NETFRAMEWORK - [MaybeNullWhen( false )] -#endif - out IApiVersionConvention<ActionModel> convention ) + protected override bool TryGetConvention( MethodInfo method, [MaybeNullWhen( false )] out IApiVersionConvention<ActionModel> convention ) { if ( actionBuilders is not null && actionBuilders.TryGetValue( method, out var builder ) ) { @@ -136,9 +131,11 @@ protected override bool TryGetConvention( return false; } +#pragma warning disable IDE0079 #pragma warning disable CA1033 // Interface methods should be callable by child types Type IControllerConventionBuilder.ControllerType => typeof( T ); #pragma warning restore CA1033 // Interface methods should be callable by child types +#pragma warning restore IDE0079 void IDeclareApiVersionConventionBuilder.IsApiVersionNeutral() => IsApiVersionNeutral(); diff --git a/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs b/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs index 562fb144..328b8b70 100644 --- a/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs +++ b/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs @@ -35,16 +35,8 @@ public static IActionConventionBuilder<TController> Action<TController>( where TController : notnull #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( actionExpression == null ) - { - throw new ArgumentNullException( nameof( actionExpression ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( actionExpression ); return builder.Action( actionExpression.ExtractMethod() ); } @@ -65,16 +57,8 @@ public static IActionConventionBuilder<TController> Action<TController, TResult> where TController : notnull #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( actionExpression == null ) - { - throw new ArgumentNullException( nameof( actionExpression ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( actionExpression ); return builder.Action( actionExpression.ExtractMethod() ); } @@ -91,11 +75,7 @@ public static IActionConventionBuilder<TController> Action<TController, TResult> /// methods that have the <see cref="NonActionAttribute"/> applied will also be ignored.</remarks> public static IActionConventionBuilder Action( this IControllerConventionBuilder builder, string methodName, params Type[] argumentTypes ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); var method = ActionMethodResolver.Resolve( builder.ControllerType, methodName, argumentTypes ); return builder.Action( method ); } diff --git a/src/Common/src/Common.Mvc/Conventions/ExpressionExtensions.cs b/src/Common/src/Common.Mvc/Conventions/ExpressionExtensions.cs index c3211e2a..5f10baa1 100644 --- a/src/Common/src/Common.Mvc/Conventions/ExpressionExtensions.cs +++ b/src/Common/src/Common.Mvc/Conventions/ExpressionExtensions.cs @@ -15,7 +15,7 @@ internal static MethodInfo ExtractMethod<TDelegate>( this Expression<TDelegate> return methodCall.Method; } - var message = string.Format( CultureInfo.CurrentCulture, MvcSR.InvalidActionMethodExpression, expression ); + var message = string.Format( CultureInfo.CurrentCulture, MvcFormat.InvalidActionMethodExpression, expression ); throw new InvalidOperationException( message ); } } \ No newline at end of file diff --git a/src/Common/src/Common.Mvc/Conventions/VersionByNamespaceConvention.cs b/src/Common/src/Common.Mvc/Conventions/VersionByNamespaceConvention.cs index a4103d0c..28a96884 100644 --- a/src/Common/src/Common.Mvc/Conventions/VersionByNamespaceConvention.cs +++ b/src/Common/src/Common.Mvc/Conventions/VersionByNamespaceConvention.cs @@ -35,15 +35,8 @@ public class VersionByNamespaceConvention : IControllerConvention /// <inheritdoc /> public virtual bool Apply( IControllerConventionBuilder builder, ControllerModel controller ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( controller == null ) - { - throw new ArgumentNullException( nameof( controller ) ); - } + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( controller ); var type = controller.ControllerType; var versions = parser.Parse( type ); @@ -55,7 +48,7 @@ public virtual bool Apply( IControllerConventionBuilder builder, ControllerModel case 1: break; default: - var message = string.Format( CultureInfo.CurrentCulture, MvcSR.MultipleApiVersionsInferredFromNamespaces, type.Namespace ); + var message = string.Format( CultureInfo.CurrentCulture, MvcFormat.MultipleApiVersionsInferredFromNamespaces, type.Namespace ); throw new InvalidOperationException( message ); } diff --git a/src/Common/src/Common.Mvc/ReportApiVersionsAttribute.cs b/src/Common/src/Common.Mvc/ReportApiVersionsAttribute.cs index 96efacdf..142ad4e4 100644 --- a/src/Common/src/Common.Mvc/ReportApiVersionsAttribute.cs +++ b/src/Common/src/Common.Mvc/ReportApiVersionsAttribute.cs @@ -23,11 +23,14 @@ public sealed partial class ReportApiVersionsAttribute : ActionFilterAttribute /// </summary> public ReportApiVersionsAttribute() { } + // cannot use attribute syntax, but this allows the attribute to be instantiated just like + // any other class implementing IActionFilter. the parameterless constructor uses DI +#pragma warning disable IDE0079 +#pragma warning disable CA1019 // Define accessors for attribute arguments + /// <summary> /// Initializes a new instance of the <see cref="ReportApiVersionsAttribute"/> class. /// </summary> /// <param name="reportApiVersions">The <see cref="IReportApiVersions">object</see> used to report API versions.</param> -#pragma warning disable CA1019 // Define accessors for attribute arguments public ReportApiVersionsAttribute( IReportApiVersions reportApiVersions ) => this.reportApiVersions = reportApiVersions; -#pragma warning restore CA1019 // Define accessors for attribute arguments } \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs b/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs index c90cafb5..45acaa44 100644 --- a/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptions.cs @@ -3,6 +3,7 @@ namespace Asp.Versioning.ApiExplorer; using Asp.Versioning.Conventions; +using Asp.Versioning.OData; /// <summary> /// Represents the possible API versioning options for an OData API explorer. @@ -44,4 +45,13 @@ public ODataQueryOptionsConventionBuilder QueryOptions /// </summary> /// <value>One or more <see cref="ODataMetadataOptions"/> values.</value> public ODataMetadataOptions MetadataOptions { get; set; } = ODataMetadataOptions.None; + + /// <summary> + /// Gets the builder used to create ad hoc versioned Entity Data Models (EDMs). + /// </summary> + /// <value>The associated <see cref="VersionedODataModelBuilder">model builder</see>.</value> +#if !NETFRAMEWORK + [CLSCompliant( false )] +#endif + public VersionedODataModelBuilder AdHocModelBuilder { get; } } \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/DefaultODataQueryOptionDescriptionProvider.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/DefaultODataQueryOptionDescriptionProvider.cs index 0e3edf1a..d13b6d75 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/DefaultODataQueryOptionDescriptionProvider.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/DefaultODataQueryOptionDescriptionProvider.cs @@ -18,6 +18,11 @@ namespace Asp.Versioning.Conventions; using static Microsoft.AspNetCore.OData.Query.AllowedQueryOptions; #endif using static System.Globalization.CultureInfo; +#if NETFRAMEWORK +using Fmt = Asp.Versioning.ODataExpSR; +#else +using Fmt = Asp.Versioning.Format; +#endif /// <summary> /// Represents the default <see cref="IODataQueryOptionDescriptionProvider">OData query option description provider.</see>. @@ -36,7 +41,7 @@ public virtual string Describe( AllowedQueryOptions queryOption, ODataQueryOptio if ( ( queryOption < Filter || queryOption > Supported ) || ( queryOption != Filter && ( (int) queryOption % 2 != 0 ) ) ) { - throw new ArgumentException( ODataExpSR.MultipleQueryOptionsNotAllowed, nameof( queryOption ) ); + throw new System.ArgumentException( ODataExpSR.MultipleQueryOptionsNotAllowed, nameof( queryOption ) ); } return queryOption switch @@ -48,13 +53,15 @@ public virtual string Describe( AllowedQueryOptions queryOption, ODataQueryOptio Top => DescribeTop( context ), Skip => DescribeSkip( context ), Count => DescribeCount( context ), - _ => throw new ArgumentException( + _ => throw new System.ArgumentException( string.Format( CurrentCulture, - ODataExpSR.UnsupportedQueryOption, -#pragma warning disable CA1308 // Normalize strings to uppercase + Fmt.UnsupportedQueryOption, +#pragma warning disable IDE0079 +#pragma warning disable CA1308 // Normalize strings to uppercase (proper casing is lowercase) queryOption.ToString().ToLowerInvariant() ), -#pragma warning restore CA1308 +#pragma warning restore CA1308 // Normalize strings to uppercase +#pragma warning restore IDE0079 nameof( queryOption ) ), }; } @@ -66,10 +73,7 @@ public virtual string Describe( AllowedQueryOptions queryOption, ODataQueryOptio /// <returns>The query option description.</returns> protected virtual string DescribeFilter( ODataQueryOptionDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var description = new StringBuilder(); @@ -78,7 +82,7 @@ protected virtual string DescribeFilter( ODataQueryOptionDescriptionContext cont if ( context.MaxNodeCount > 1 ) { description.Append( Space ) - .AppendFormat( CurrentCulture, ODataExpSR.MaxExpressionDesc, context.MaxNodeCount ); + .AppendFormat( CurrentCulture, Fmt.MaxExpressionDesc, context.MaxNodeCount ); } AppendAllowedOptions( description, context ); @@ -88,7 +92,7 @@ protected virtual string DescribeFilter( ODataQueryOptionDescriptionContext cont description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedFilterProperties ) ); } @@ -102,10 +106,7 @@ protected virtual string DescribeFilter( ODataQueryOptionDescriptionContext cont /// <returns>The query option description.</returns> protected virtual string DescribeExpand( ODataQueryOptionDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); bool hasMaxExpansionDepth; @@ -120,7 +121,7 @@ protected virtual string DescribeExpand( ODataQueryOptionDescriptionContext cont if ( hasMaxExpansionDepth ) { description.Append( Space ) - .AppendFormat( CurrentCulture, ODataExpSR.MaxDepthDesc, context.MaxExpansionDepth ); + .AppendFormat( CurrentCulture, Fmt.MaxDepthDesc, context.MaxExpansionDepth ); } if ( context.AllowedExpandProperties.Count > 0 ) @@ -128,7 +129,7 @@ protected virtual string DescribeExpand( ODataQueryOptionDescriptionContext cont description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedExpandProperties ) ); } @@ -142,10 +143,7 @@ protected virtual string DescribeExpand( ODataQueryOptionDescriptionContext cont /// <returns>The query option description.</returns> protected virtual string DescribeSelect( ODataQueryOptionDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); if ( context.AllowedSelectProperties.Count <= 0 ) { @@ -157,7 +155,7 @@ protected virtual string DescribeSelect( ODataQueryOptionDescriptionContext cont .Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedSelectProperties ) ) .ToString(); } @@ -169,10 +167,7 @@ protected virtual string DescribeSelect( ODataQueryOptionDescriptionContext cont /// <returns>The query option description.</returns> protected virtual string DescribeOrderBy( ODataQueryOptionDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); bool hasMaxOrderByNodeCount; @@ -187,7 +182,7 @@ protected virtual string DescribeOrderBy( ODataQueryOptionDescriptionContext con if ( hasMaxOrderByNodeCount ) { description.Append( Space ) - .AppendFormat( CurrentCulture, ODataExpSR.MaxExpressionDesc, context.MaxOrderByNodeCount ); + .AppendFormat( CurrentCulture, Fmt.MaxExpressionDesc, context.MaxOrderByNodeCount ); } if ( context.AllowedOrderByProperties.Count > 0 ) @@ -195,7 +190,7 @@ protected virtual string DescribeOrderBy( ODataQueryOptionDescriptionContext con description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedOrderByProperties ) ); } @@ -209,10 +204,7 @@ protected virtual string DescribeOrderBy( ODataQueryOptionDescriptionContext con /// <returns>The query option description.</returns> protected virtual string DescribeTop( ODataQueryOptionDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); if ( context.MaxTop.NoLimitOrNone() ) { @@ -222,7 +214,7 @@ protected virtual string DescribeTop( ODataQueryOptionDescriptionContext context return GetOrCreateBuilder() .Append( ODataExpSR.TopQueryOptionDesc ) .Append( Space ) - .AppendFormat( CurrentCulture, ODataExpSR.MaxValueDesc, context.MaxTop ) + .AppendFormat( CurrentCulture, Fmt.MaxValueDesc, context.MaxTop ) .ToString(); } @@ -233,10 +225,7 @@ protected virtual string DescribeTop( ODataQueryOptionDescriptionContext context /// <returns>The query option description.</returns> protected virtual string DescribeSkip( ODataQueryOptionDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); if ( context.MaxSkip.NoLimitOrNone() ) { @@ -246,7 +235,7 @@ protected virtual string DescribeSkip( ODataQueryOptionDescriptionContext contex return GetOrCreateBuilder() .Append( ODataExpSR.SkipQueryOptionDesc ) .Append( Space ) - .AppendFormat( CurrentCulture, ODataExpSR.MaxValueDesc, context.MaxSkip ) + .AppendFormat( CurrentCulture, Fmt.MaxValueDesc, context.MaxSkip ) .ToString(); } @@ -265,7 +254,7 @@ private static void AppendAllowedOptions( StringBuilder description, ODataQueryO description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedLogicalOperatorsDesc, + Fmt.AllowedLogicalOperatorsDesc, string.Join( ", ", EnumerateLogicalOperators( context.AllowedLogicalOperators ) ) ); @@ -277,7 +266,7 @@ private static void AppendAllowedOptions( StringBuilder description, ODataQueryO description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedArithmeticOperatorsDesc, + Fmt.AllowedArithmeticOperatorsDesc, string.Join( ", ", EnumerateArithmeticOperators( context.AllowedArithmeticOperators ) ) ); @@ -286,13 +275,15 @@ private static void AppendAllowedOptions( StringBuilder description, ODataQueryO if ( context.AllowedFunctions != AllowedFunctions.None && context.AllowedFunctions != AllowedFunctions.AllFunctions ) { +#pragma warning disable IDE0079 +#pragma warning disable CA1308 // Normalize strings to uppercase (proper casing is lowercase) description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedFunctionsDesc, -#pragma warning disable CA1308 // Normalize strings to uppercase + Fmt.AllowedFunctionsDesc, context.AllowedFunctions.ToString().ToLowerInvariant() ); -#pragma warning restore CA1308 +#pragma warning restore CA1308 // Normalize strings to uppercase +#pragma warning restore IDE0079 } } diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/IODataActionQueryOptionsConventionBuilder{T}.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/IODataActionQueryOptionsConventionBuilder{T}.cs index 38bf2258..0a8835e7 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/IODataActionQueryOptionsConventionBuilder{T}.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/IODataActionQueryOptionsConventionBuilder{T}.cs @@ -1,10 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable IDE0079 // Remove unnecessary suppression -#pragma warning disable SA1001 // Commas should be spaced correctly +#pragma warning disable IDE0079 +#pragma warning disable SA1001 namespace Asp.Versioning.Conventions; -#pragma warning restore IDE0079 // Remove unnecessary suppression using System.Reflection; #if NETFRAMEWORK diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs new file mode 100644 index 00000000..a9c96f59 --- /dev/null +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.Conventions; + +using Asp.Versioning; +using Asp.Versioning.OData; +#if NETFRAMEWORK +using Microsoft.AspNet.OData.Builder; +#else +using Microsoft.OData.ModelBuilder; +using System.Buffers; +#endif + +/// <summary> +/// Represents an OData model bound settings <see cref="IModelConfiguration">model configuration</see> +/// that is also an <see cref="IODataQueryOptionsConvention">OData query options convention</see>. +/// </summary> +public sealed partial class ImplicitModelBoundSettingsConvention : IModelConfiguration, IODataQueryOptionsConvention +{ + private readonly HashSet<Type> types = []; + + /// <inheritdoc /> + public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix ) + { + ArgumentNullException.ThrowIfNull( builder ); + + if ( types.Count == 0 ) + { + return; + } + + if ( GetExistingTypes( builder ) is HashSet<Type> existingTypes ) + { + types.ExceptWith( existingTypes ); + } + + if ( types.Count == 0 ) + { + return; + } + + // model configurations are applied unordered, which could matter. + // defer implicit registrations in the model until all other model + // configurations have been applied, if possible + if ( builder is ODataConventionModelBuilder modelBuilder ) + { + modelBuilder.OnModelCreating += OnModelCreating; + } + else + { + OnModelCreating( builder ); + } + } + + private static HashSet<Type>? GetExistingTypes( ODataModelBuilder builder ) + { + HashSet<Type> types; + + if ( builder.StructuralTypes is ICollection<StructuralTypeConfiguration> collection ) + { + var count = collection.Count; + + if ( count == 0 ) + { + return default; + } + +#if NETFRAMEWORK + var array = new StructuralTypeConfiguration[count]; + types = []; +#else + var pool = ArrayPool<StructuralTypeConfiguration>.Shared; + var array = pool.Rent( count ); + + types = new( capacity: count ); +#endif + + collection.CopyTo( array, 0 ); + + for ( var i = 0; i < count; i++ ) + { + types.Add( array[i].ClrType ); + } + +#if !NETFRAMEWORK + pool.Return( array, clearArray: true ); +#endif + + return types; + } + + using var structuralTypes = builder.StructuralTypes.GetEnumerator(); + + if ( !structuralTypes.MoveNext() ) + { + return default; + } + + types = [structuralTypes.Current.ClrType]; + + while ( structuralTypes.MoveNext() ) + { + types.Add( structuralTypes.Current.ClrType ); + } + + return types; + } + + private void OnModelCreating( ODataModelBuilder builder ) + { + foreach ( var type in types ) + { + builder.AddComplexType( type ); + } + } +} \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder.cs index 3a1215b9..1da8b0b5 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder.cs @@ -61,11 +61,7 @@ public ODataActionQueryOptionsConventionBuilder( ODataControllerQueryOptionsConv /// <returns>The original <see cref="ODataActionQueryOptionsConventionBuilder"/>.</returns> public virtual ODataActionQueryOptionsConventionBuilder Use( ODataValidationSettings validationSettings ) { - if ( validationSettings == null ) - { - throw new ArgumentNullException( nameof( validationSettings ) ); - } - + ArgumentNullException.ThrowIfNull( validationSettings ); ValidationSettings.CopyFrom( validationSettings ); return this; } @@ -209,10 +205,7 @@ public virtual ODataActionQueryOptionsConventionBuilder AllowFilter( int maxNode /// <returns>The original <see cref="ODataActionQueryOptionsConventionBuilder"/>.</returns> public virtual ODataActionQueryOptionsConventionBuilder AllowOrderBy( int maxNodeCount, IEnumerable<string> properties ) { - if ( properties == null ) - { - throw new ArgumentNullException( nameof( properties ) ); - } + ArgumentNullException.ThrowIfNull( properties ); ValidationSettings.AllowedQueryOptions |= OrderBy; diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection.cs index 5f60f7c2..c9a26f99 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection.cs @@ -33,7 +33,7 @@ protected internal virtual ODataActionQueryOptionsConventionBuilder GetOrAdd( Me if ( actionBuilderMappings == null ) { var builder = new ODataActionQueryOptionsConventionBuilder( controllerBuilder ); - actionBuilderMappings = new() { new( actionMethod, builder ) }; + actionBuilderMappings = [new( actionMethod, builder )]; return builder; } @@ -60,12 +60,7 @@ protected internal virtual ODataActionQueryOptionsConventionBuilder GetOrAdd( Me /// <param name="actionMethod">The controller action method to get the convention builder for.</param> /// <param name="actionBuilder">The <see cref="ODataActionQueryOptionsConventionBuilder">controller action convention builder</see> or <c>null</c>.</param> /// <returns>True if the <paramref name="actionBuilder">action builder</paramref> is successfully retrieved; otherwise, false.</returns> - public virtual bool TryGetValue( - MethodInfo? actionMethod, -#if !NETFRAMEWORK - [NotNullWhen( true )] -#endif - out ODataActionQueryOptionsConventionBuilder? actionBuilder ) + public virtual bool TryGetValue( MethodInfo? actionMethod, [NotNullWhen( true )] out ODataActionQueryOptionsConventionBuilder? actionBuilder ) { if ( actionMethod == null || actionBuilderMappings == null || actionBuilderMappings.Count == 0 ) { diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection{T}.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection{T}.cs index ccce51af..11addf0d 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection{T}.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderCollection{T}.cs @@ -44,7 +44,7 @@ protected internal virtual ODataActionQueryOptionsConventionBuilder<T> GetOrAdd( if ( actionBuilderMappings == null ) { var builder = new ODataActionQueryOptionsConventionBuilder<T>( controllerBuilder ); - actionBuilderMappings = new() { new( actionMethod, builder ) }; + actionBuilderMappings = [new( actionMethod, builder )]; return builder; } @@ -71,12 +71,7 @@ protected internal virtual ODataActionQueryOptionsConventionBuilder<T> GetOrAdd( /// <param name="actionMethod">The controller action method to get the convention builder for.</param> /// <param name="actionBuilder">The <see cref="ODataActionQueryOptionsConventionBuilder{T}">controller action convention builder</see> or <c>null</c>.</param> /// <returns>True if the <paramref name="actionBuilder">action builder</paramref> is successfully retrieved; otherwise, false.</returns> - public virtual bool TryGetValue( - MethodInfo? actionMethod, -#if !NETFRAMEWORK - [NotNullWhen( true )] -#endif - out ODataActionQueryOptionsConventionBuilder<T>? actionBuilder ) + public virtual bool TryGetValue( MethodInfo? actionMethod, [NotNullWhen( true )] out ODataActionQueryOptionsConventionBuilder<T>? actionBuilder ) { if ( actionMethod == null || actionBuilderMappings == null || actionBuilderMappings.Count == 0 ) { diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs index 9804be5d..bcd941a0 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -#pragma warning disable IDE0079 // Remove unnecessary suppression -#pragma warning disable SA1001 // Commas should be spaced correctly +#pragma warning disable IDE0079 +#pragma warning disable SA1001 namespace Asp.Versioning.Conventions; @@ -39,11 +39,7 @@ public static ODataActionQueryOptionsConventionBuilder AllowOrderBy( int maxNodeCount, params string[] properties ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.AllowOrderBy( maxNodeCount, properties.AsEnumerable() ); } @@ -58,11 +54,7 @@ public static ODataActionQueryOptionsConventionBuilder AllowOrderBy( this ODataActionQueryOptionsConventionBuilder builder, IEnumerable<string> properties ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.AllowOrderBy( default, properties ); } @@ -77,11 +69,7 @@ public static ODataActionQueryOptionsConventionBuilder AllowOrderBy( this ODataActionQueryOptionsConventionBuilder builder, params string[] properties ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.AllowOrderBy( default, properties.AsEnumerable() ); } @@ -103,11 +91,7 @@ public static ODataActionQueryOptionsConventionBuilder<T> AllowOrderBy<T>( , IHttpController #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.AllowOrderBy( maxNodeCount, properties.AsEnumerable() ); } @@ -127,11 +111,7 @@ public static ODataActionQueryOptionsConventionBuilder<T> AllowOrderBy<T>( , IHttpController #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.AllowOrderBy( default, properties ); } @@ -151,11 +131,7 @@ public static ODataActionQueryOptionsConventionBuilder<T> AllowOrderBy<T>( , IHttpController #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); return builder.AllowOrderBy( default, properties.AsEnumerable() ); } @@ -174,16 +150,8 @@ public static ODataActionQueryOptionsConventionBuilder<TController> Action<TCont , IHttpController #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( actionExpression == null ) - { - throw new ArgumentNullException( nameof( actionExpression ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( actionExpression ); return builder.Action( actionExpression.ExtractMethod() ); } @@ -203,16 +171,8 @@ public static ODataActionQueryOptionsConventionBuilder<TController> Action<TCont , IHttpController #endif { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( actionExpression == null ) - { - throw new ArgumentNullException( nameof( actionExpression ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( actionExpression ); return builder.Action( actionExpression.ExtractMethod() ); } @@ -232,15 +192,8 @@ public static ODataActionQueryOptionsConventionBuilder Action( string methodName, params Type[] argumentTypes ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - if ( argumentTypes == null ) - { - throw new ArgumentNullException( nameof( argumentTypes ) ); - } + ArgumentNullException.ThrowIfNull( builder ); + ArgumentNullException.ThrowIfNull( argumentTypes ); string message; var methods = builder.ControllerType @@ -251,7 +204,7 @@ public static ODataActionQueryOptionsConventionBuilder Action( switch ( methods.Length ) { case 0: - message = string.Format( CultureInfo.CurrentCulture, ODataExpSR.ActionMethodNotFound, methodName ); + message = string.Format( CultureInfo.CurrentCulture, Format.ActionMethodNotFound, methodName ); throw new MissingMethodException( message ); case 1: return builder.Action( methods[0] ); @@ -265,7 +218,7 @@ public static ODataActionQueryOptionsConventionBuilder Action( return builder.Action( methods[0] ); } - message = string.Format( CultureInfo.CurrentCulture, ODataExpSR.AmbiguousActionMethod, methodName ); + message = string.Format( CultureInfo.CurrentCulture, Format.AmbiguousActionMethod, methodName ); throw new AmbiguousMatchException( message ); } @@ -310,7 +263,7 @@ private static MethodInfo ExtractMethod<TDelegate>( this Expression<TDelegate> e return methodCall.Method; } - var message = string.Format( CultureInfo.CurrentCulture, ODataExpSR.InvalidActionMethodExpression, expression ); + var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidActionMethodExpression, expression ); throw new InvalidOperationException( message ); } } \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder{T}.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder{T}.cs index e75a0243..31dbc4c2 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder{T}.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilder{T}.cs @@ -32,9 +32,11 @@ public class ODataActionQueryOptionsConventionBuilder<T> : IODataActionQueryOptionsConventionBuilder<T> where T : notnull #if NETFRAMEWORK +#pragma warning disable IDE0079 #pragma warning disable SA1001 // Commas should be spaced correctly , IHttpController #pragma warning restore SA1001 // Commas should be spaced correctly +#pragma warning restore IDE0079 #endif { /// <summary> @@ -66,11 +68,7 @@ public ODataActionQueryOptionsConventionBuilder( ODataControllerQueryOptionsConv /// <returns>The original <see cref="ODataActionQueryOptionsConventionBuilder{T}"/>.</returns> public virtual ODataActionQueryOptionsConventionBuilder<T> Use( ODataValidationSettings validationSettings ) { - if ( validationSettings == null ) - { - throw new ArgumentNullException( nameof( validationSettings ) ); - } - + ArgumentNullException.ThrowIfNull( validationSettings ); ValidationSettings.CopyFrom( validationSettings ); return this; } @@ -214,10 +212,7 @@ public virtual ODataActionQueryOptionsConventionBuilder<T> AllowFilter( int maxN /// <returns>The original <see cref="ODataActionQueryOptionsConventionBuilder{T}"/>.</returns> public virtual ODataActionQueryOptionsConventionBuilder<T> AllowOrderBy( int maxNodeCount, IEnumerable<string> properties ) { - if ( properties == null ) - { - throw new ArgumentNullException( nameof( properties ) ); - } + ArgumentNullException.ThrowIfNull( properties ); ValidationSettings.AllowedQueryOptions |= OrderBy; diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs index 1ee25007..fd973fe6 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs @@ -24,6 +24,7 @@ namespace Asp.Versioning.Conventions; internal sealed partial class ODataAttributeVisitor { + private static readonly char[] Comma = [',']; private readonly ODataQueryOptionDescriptionContext context; internal ODataAttributeVisitor( @@ -38,14 +39,14 @@ internal ODataAttributeVisitor( internal void Visit( ApiDescription apiDescription ) { - VisitAction( apiDescription.ActionDescriptor ); - var modelType = context.ReturnType; if ( modelType != null ) { VisitModel( modelType ); } + + VisitAction( apiDescription.ActionDescriptor ); } private void VisitModel( IEdmStructuredType modelType ) @@ -75,11 +76,11 @@ private void VisitModel( IEdmStructuredType modelType ) VisitMaxTop( querySettings ); } - private void VisitEnableQuery( IReadOnlyList<EnableQueryAttribute> 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]; @@ -124,7 +125,7 @@ private void VisitEnableQuery( IReadOnlyList<EnableQueryAttribute> attributes ) continue; } - var properties = attribute.AllowedOrderByProperties.Split( new[] { ',' }, RemoveEmptyEntries ); + var properties = attribute.AllowedOrderByProperties.Split( Comma, RemoveEmptyEntries ); var allowedOrderByProperties = context.AllowedOrderByProperties; var comparer = StringComparer.OrdinalIgnoreCase; diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionConvention.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionConvention.cs index 32e8a620..40077d14 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionConvention.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionConvention.cs @@ -41,7 +41,7 @@ public void ApplyTo( ApiDescription apiDescription ) convention!.ApplyTo( apiDescription ); } - private static IODataQueryOptionsConvention ImplicitActionConvention( ODataQueryOptionSettings settings ) + private static ODataValidationSettingsConvention ImplicitActionConvention( ODataQueryOptionSettings settings ) { var validationSettings = new ODataValidationSettings() { @@ -51,6 +51,6 @@ private static IODataQueryOptionsConvention ImplicitActionConvention( ODataQuery AllowedQueryOptions = AllowedQueryOptions.None, }; - return new ODataValidationSettingsConvention( validationSettings, settings ); + return new( validationSettings, settings ); } } \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder.cs index 6a327147..a22d953b 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder.cs @@ -27,8 +27,8 @@ public ODataControllerQueryOptionsConventionBuilder( Type controllerType ) if ( !webApiController.IsAssignableFrom( controllerType ) ) { - var message = string.Format( CultureInfo.CurrentCulture, ODataExpSR.RequiredInterfaceNotImplemented, controllerType, webApiController ); - throw new ArgumentException( message ); + var message = string.Format( CultureInfo.CurrentCulture, Format.RequiredInterfaceNotImplemented, controllerType, webApiController ); + throw new System.ArgumentException( message ); } #endif ControllerType = controllerType; diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder{T}.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder{T}.cs index 3d5322bd..8f51e97d 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder{T}.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataControllerQueryOptionsConventionBuilder{T}.cs @@ -20,9 +20,11 @@ public class ODataControllerQueryOptionsConventionBuilder<T> : IODataActionQueryOptionsConventionBuilder<T> where T : notnull #if NETFRAMEWORK +#pragma warning disable IDE0079 #pragma warning disable SA1001 // Commas should be spaced correctly , IHttpController #pragma warning restore SA1001 // Commas should be spaced correctly +#pragma warning restore IDE0079 #endif { private ODataActionQueryOptionsConventionBuilderCollection<T>? actionBuilders; diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs index c766317d..5486f49f 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionDescriptionContext.cs @@ -34,7 +34,7 @@ public partial class ODataQueryOptionDescriptionContext /// </summary> /// <param name="apiDescription">The associated <see cref="ApiDescription">API description</see>.</param> public ODataQueryOptionDescriptionContext( ApiDescription apiDescription ) => - ApiDescription = apiDescription ?? throw new ArgumentNullException( nameof( apiDescription ) ); + ApiDescription = apiDescription ?? throw new System.ArgumentNullException( nameof( apiDescription ) ); /// <summary> /// Initializes a new instance of the <see cref="ODataQueryOptionDescriptionContext"/> class. @@ -46,16 +46,14 @@ protected internal ODataQueryOptionDescriptionContext( ApiDescription apiDescription, ODataValidationSettings validationSettings ) { - if ( validationSettings == null ) - { - throw new ArgumentNullException( nameof( validationSettings ) ); - } + ArgumentNullException.ThrowIfNull( apiDescription ); + ArgumentNullException.ThrowIfNull( validationSettings ); - ApiDescription = apiDescription ?? throw new ArgumentNullException( nameof( apiDescription ) ); + ApiDescription = apiDescription; AllowedArithmeticOperators = validationSettings.AllowedArithmeticOperators; AllowedFunctions = validationSettings.AllowedFunctions; AllowedLogicalOperators = validationSettings.AllowedLogicalOperators; - allowedOrderByProperties = validationSettings.AllowedOrderByProperties.ToList(); + allowedOrderByProperties = [.. validationSettings.AllowedOrderByProperties]; MaxOrderByNodeCount = validationSettings.MaxOrderByNodeCount; MaxAnyAllExpressionDepth = validationSettings.MaxAnyAllExpressionDepth; MaxNodeCount = validationSettings.MaxNodeCount; @@ -92,25 +90,25 @@ protected internal ODataQueryOptionDescriptionContext( /// Gets the names of properties that can be selected. /// </summary> /// <value>A <see cref="IList{T}">list</see> of selectable property names.</value> - public IList<string> AllowedSelectProperties => allowedSelectProperties ??= new(); + public IList<string> AllowedSelectProperties => allowedSelectProperties ??= []; /// <summary> /// Gets the names of properties that can be expanded. /// </summary> /// <value>A <see cref="IList{T}">list</see> of expandable property names.</value> - public IList<string> AllowedExpandProperties => allowedExpandProperties ??= new(); + public IList<string> AllowedExpandProperties => allowedExpandProperties ??= []; /// <summary> /// Gets the names of properties that can be filtered. /// </summary> /// <value>A <see cref="IList{T}">list</see> of filterable property names.</value> - public IList<string> AllowedFilterProperties => allowedFilterProperties ??= new(); + public IList<string> AllowedFilterProperties => allowedFilterProperties ??= []; /// <summary> /// Gets the names of properties that can be sorted. /// </summary> /// <value>A <see cref="IList{T}">list</see> of sortable property names.</value> - public IList<string> AllowedOrderByProperties => allowedOrderByProperties ??= new(); + public IList<string> AllowedOrderByProperties => allowedOrderByProperties ??= []; /// <summary> /// Gets or sets the maximum number of expressions that can be present in the $orderby query option. diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs index bf2c4b6b..8d0921f9 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataQueryOptionsConventionBuilder.cs @@ -43,13 +43,13 @@ public IODataQueryOptionDescriptionProvider DescriptionProvider /// </summary> /// <value>A <see cref="IDictionary{TKey, TValue}">collection</see> of /// <see cref="IODataQueryOptionsConventionBuilder">controller convention builders</see>.</value> - protected IDictionary<Type, IODataQueryOptionsConventionBuilder> ConventionBuilders => conventionBuilders ??= new(); + protected IDictionary<Type, IODataQueryOptionsConventionBuilder> ConventionBuilders => conventionBuilders ??= []; /// <summary> /// Gets a collection of OData query option conventions. /// </summary> /// <value>A <see cref="IList{T}">list</see> of <see cref="IODataQueryOptionsConvention">OData query option conventions</see>.</value> - protected IList<IODataQueryOptionsConvention> Conventions => conventions ??= new(); + protected IList<IODataQueryOptionsConvention> Conventions => conventions ??= []; /// <summary> /// Gets or creates the convention builder for the specified controller. @@ -59,9 +59,11 @@ public IODataQueryOptionDescriptionProvider DescriptionProvider public virtual ODataControllerQueryOptionsConventionBuilder<TController> Controller<TController>() where TController : notnull #if NETFRAMEWORK +#pragma warning disable IDE0079 #pragma warning disable SA1001 // Commas should be spaced correctly , IHttpController #pragma warning restore SA1001 // Commas should be spaced correctly +#pragma warning restore IDE0079 #endif { var key = typeof( TController ); @@ -78,7 +80,7 @@ public virtual ODataControllerQueryOptionsConventionBuilder<TController> Control return typedBuilder; } - var message = string.Format( CultureInfo.CurrentCulture, ODataExpSR.ConventionStyleMismatch, key.Name ); + var message = string.Format( CultureInfo.CurrentCulture, Format.ConventionStyleMismatch, key.Name ); throw new InvalidOperationException( message ); } @@ -89,10 +91,7 @@ public virtual ODataControllerQueryOptionsConventionBuilder<TController> Control /// <returns>A new or existing <see cref="ODataControllerQueryOptionsConventionBuilder"/>.</returns> public virtual ODataControllerQueryOptionsConventionBuilder Controller( Type controllerType ) { - if ( controllerType == null ) - { - throw new ArgumentNullException( nameof( controllerType ) ); - } + ArgumentNullException.ThrowIfNull( controllerType ); if ( !ConventionBuilders.TryGetValue( controllerType, out var builder ) ) { @@ -106,7 +105,7 @@ public virtual ODataControllerQueryOptionsConventionBuilder Controller( Type con return typedBuilder; } - var message = string.Format( CultureInfo.CurrentCulture, ODataExpSR.ConventionStyleMismatch, controllerType.Name ); + var message = string.Format( CultureInfo.CurrentCulture, Format.ConventionStyleMismatch, controllerType.Name ); throw new InvalidOperationException( message ); } @@ -124,10 +123,7 @@ public virtual ODataControllerQueryOptionsConventionBuilder Controller( Type con /// <param name="queryOptionSettings">The <see cref="ODataQueryOptionSettings">settings</see> used to apply OData query option conventions.</param> public virtual void ApplyTo( IEnumerable<ApiDescription> apiDescriptions, ODataQueryOptionSettings queryOptionSettings ) { - if ( apiDescriptions == null ) - { - throw new ArgumentNullException( nameof( apiDescriptions ) ); - } + ArgumentNullException.ThrowIfNull( apiDescriptions ); var controllerConventions = default( Dictionary<Type, IODataQueryOptionsConvention> ); @@ -135,7 +131,7 @@ public virtual void ApplyTo( IEnumerable<ApiDescription> apiDescriptions, ODataQ { var controller = GetController( description ); - if ( !controller.IsODataController() && !IsODataLike( description ) ) + if ( !controller.IsODataController() && !description.IsODataLike() ) { continue; } @@ -150,7 +146,7 @@ public virtual void ApplyTo( IEnumerable<ApiDescription> apiDescriptions, ODataQ } convention = builder.Build( queryOptionSettings ); - controllerConventions ??= new(); + controllerConventions ??= []; controllerConventions.Add( controller, convention ); } diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs index 3901386d..686a1953 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs @@ -139,9 +139,11 @@ private static bool IsSupported( string? httpMethod ) private string GetName( AllowedQueryOptions option ) { -#pragma warning disable CA1308 // Normalize strings to uppercase +#pragma warning disable IDE0079 +#pragma warning disable CA1308 // Normalize strings to uppercase (proper casing is lowercase) var name = option.ToString().ToLowerInvariant(); -#pragma warning restore CA1308 +#pragma warning restore CA1308 // Normalize strings to uppercase +#pragma warning restore IDE0079 return Settings.NoDollarPrefix ? name : name.Insert( 0, "$" ); } 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 30a3796d..16ef9c9c 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 @@ -53,7 +53,7 @@ internal static class EdmExtensions #if NETFRAMEWORK private static string Requalify( string edmFullName, string @namespace ) => @namespace + edmFullName.Substring( 3 ); #else - private static string Requalify( string edmFullName, string @namespace ) => string.Concat( @namespace.AsSpan(), edmFullName.AsSpan().Slice( 3 ) ); + private static string Requalify( string edmFullName, string @namespace ) => string.Concat( @namespace.AsSpan(), edmFullName.AsSpan()[3..] ); #endif private static Type? GetTypeFromAssembly( string edmFullName, string assemblyName ) diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/ClassSignature.cs b/src/Common/src/Common.OData.ApiExplorer/OData/ClassSignature.cs index 749a5cfc..5dc090ea 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/ClassSignature.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/ClassSignature.cs @@ -12,7 +12,7 @@ internal sealed class ClassSignature : IEquatable<ClassSignature> private static readonly ConstructorInfo newOriginalType = typeof( OriginalTypeAttribute ).GetConstructors()[0]; private int? hashCode; - internal ClassSignature( Type originalType, IEnumerable<ClassProperty> properties, ApiVersion apiVersion ) + internal ClassSignature( string name, Type originalType, IEnumerable<ClassProperty> properties, ApiVersion apiVersion ) { var attributeBuilders = new List<CustomAttributeBuilder>() { @@ -21,7 +21,7 @@ internal ClassSignature( Type originalType, IEnumerable<ClassProperty> propertie attributeBuilders.AddRange( originalType.DeclaredAttributes() ); - Name = originalType.FullName!; + Name = name; Attributes = attributeBuilders.ToArray(); Properties = properties.ToArray(); ApiVersion = apiVersion; diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs index 461d6cf0..ce5a1010 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs @@ -24,68 +24,87 @@ namespace Asp.Versioning.OData; /// </summary> public sealed class DefaultModelTypeBuilder : IModelTypeBuilder { + /* design: there is typically a 1:1 relationship between an edm and api version. odata model bound settings + * are realized as an annotation in the edm. this can result in two sets of pairs where one edm is the + * standard mapping and the other is ad hoc for the purposes of query option settings. aside for the bucket + * they land in, there is no difference in how types will be mapped; however if the wrong edm from the + * incorrect bucket is picked, then the type mapping will fail. the model type builder detects if a model + * is ad hoc. if it is, then it will recursively create a private instance of itself to handle the ad hoc + * bucket. normal odata cannot opt out of this process because the explored type must match the edm. a type + * mapped via an ad hoc edm is not really odata so it should opt out by default because without an edm + * there is not away to control member serialization/deserialization easily. such cases will typically + * create a type-per-version, as is common for non-odata, which negates the need for model substitution. + * a user can opt into ad hoc model substitution if they have a way to deal with member filtering. + */ + private static Type? ienumerableOfT; - private ConcurrentDictionary<ApiVersion, ModuleBuilder>? modules; - private ConcurrentDictionary<ApiVersion, IDictionary<EdmTypeKey, Type>>? generatedEdmTypesPerVersion; - private ConcurrentDictionary<ApiVersion, ConcurrentDictionary<EdmTypeKey, Type>>? generatedActionParamsPerVersion; + private readonly bool adHoc; + private readonly bool excludeAdHocModels; + private DefaultModelTypeBuilder? adHocBuilder; + private ConcurrentDictionary<EdmModelKey, ModuleBuilder>? modules; + private ConcurrentDictionary<EdmModelKey, IDictionary<EdmTypeKey, Type>>? generatedEdmTypesPerVersion; + private ConcurrentDictionary<EdmModelKey, ConcurrentDictionary<EdmTypeKey, Type>>? generatedActionParamsPerVersion; + + private DefaultModelTypeBuilder( bool excludeAdHocModels, bool adHoc ) + { + this.adHoc = adHoc; + this.excludeAdHocModels = excludeAdHocModels; + } + + /// <summary> + /// Initializes a new instance of the <see cref="DefaultModelTypeBuilder"/> class. + /// </summary> + /// <param name="includeAdHocModels">Indicates whether types from an ad hoc Entity + /// Data Model (EDM) should be included.</param> + public DefaultModelTypeBuilder( bool includeAdHocModels = false ) => excludeAdHocModels = !includeAdHocModels; /// <inheritdoc /> public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) { - if ( model == null ) - { - throw new ArgumentNullException( nameof( model ) ); - } + ArgumentNullException.ThrowIfNull( model ); - if ( structuredType == null ) + if ( model.IsAdHoc() ) { - throw new ArgumentNullException( nameof( structuredType ) ); + if ( excludeAdHocModels ) + { + return clrType; + } + else if ( !adHoc ) + { + adHocBuilder ??= new( excludeAdHocModels, adHoc: true ); + return adHocBuilder.NewStructuredType( model, structuredType, clrType, apiVersion ); + } } - if ( clrType == null ) - { - throw new ArgumentNullException( nameof( clrType ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( structuredType ); + ArgumentNullException.ThrowIfNull( clrType ); + ArgumentNullException.ThrowIfNull( apiVersion ); generatedEdmTypesPerVersion ??= new(); - var typeKey = new EdmTypeKey( structuredType, apiVersion ); - var edmTypes = generatedEdmTypesPerVersion.GetOrAdd( apiVersion, key => GenerateTypesForEdmModel( model, key ) ); + var edmTypes = generatedEdmTypesPerVersion.GetOrAdd( new( model, apiVersion ), key => GenerateTypesForEdmModel( model, key.ApiVersion ) ); - return edmTypes[typeKey]; + return edmTypes[new( structuredType, apiVersion )]; } /// <inheritdoc /> public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ) { - if ( model == null ) - { - throw new ArgumentNullException( nameof( model ) ); - } + ArgumentNullException.ThrowIfNull( model ); - if ( action == null ) + if ( !adHoc && model.IsAdHoc() ) { - throw new ArgumentNullException( nameof( action ) ); + adHocBuilder ??= new( excludeAdHocModels, adHoc: true ); + return adHocBuilder.NewActionParameters( model, action, controllerName, apiVersion ); } - if ( string.IsNullOrEmpty( controllerName ) ) - { - throw new ArgumentNullException( nameof( controllerName ) ); - } - - if ( apiVersion == null ) - { - throw new ArgumentNullException( nameof( apiVersion ) ); - } + ArgumentNullException.ThrowIfNull( action ); + ArgumentException.ThrowIfNullOrEmpty( controllerName ); + ArgumentNullException.ThrowIfNull( apiVersion ); generatedActionParamsPerVersion ??= new(); - var paramTypes = generatedActionParamsPerVersion.GetOrAdd( apiVersion, _ => new ConcurrentDictionary<EdmTypeKey, Type>() ); + var paramTypes = generatedActionParamsPerVersion.GetOrAdd( new( model, apiVersion ), _ => new() ); var fullTypeName = $"{controllerName}.{action.Namespace}.{controllerName}{action.Name}Parameters"; var key = new EdmTypeKey( fullTypeName, apiVersion ); var type = paramTypes.GetOrAdd( key, _ => @@ -93,7 +112,7 @@ public Type NewActionParameters( IEdmModel model, IEdmAction action, string cont var context = new TypeSubstitutionContext( model, this, apiVersion ); var properties = action.Parameters.Where( p => p.Name != "bindingParameter" ).Select( p => new ClassProperty( p, context ) ); var signature = new ClassSignature( fullTypeName, properties, apiVersion ); - var moduleBuilder = ( modules ??= new() ).GetOrAdd( apiVersion, CreateModuleForApiVersion ); + var moduleBuilder = ( modules ??= new() ).GetOrAdd( new( model, apiVersion ), CreateModuleForApiVersion ); return CreateTypeFromSignature( moduleBuilder, signature ); } ); @@ -101,9 +120,9 @@ public Type NewActionParameters( IEdmModel model, IEdmAction action, string cont return type; } - private IDictionary<EdmTypeKey, Type> GenerateTypesForEdmModel( IEdmModel model, ApiVersion apiVersion ) + private Dictionary<EdmTypeKey, Type> GenerateTypesForEdmModel( IEdmModel model, ApiVersion apiVersion ) { - ModuleBuilder NewModuleBuilder() => ( modules ??= new() ).GetOrAdd( apiVersion, CreateModuleForApiVersion ); + ModuleBuilder NewModuleBuilder() => ( modules ??= new() ).GetOrAdd( new( model, apiVersion ), CreateModuleForApiVersion ); var context = new BuilderContext( model, apiVersion, NewModuleBuilder ); @@ -176,8 +195,8 @@ private static Type GenerateTypeIfNeeded( IEdmStructuredType structuredType, Bui private static Tuple<bool, bool> BuildSignatureProperties( Type clrType, - IReadOnlyDictionary<string, IEdmProperty> structuralProperties, - IReadOnlyDictionary<PropertyInfo, IEdmProperty> mappedClrProperties, + Dictionary<string, IEdmProperty> structuralProperties, + Dictionary<PropertyInfo, IEdmProperty> mappedClrProperties, List<ClassProperty> properties, List<PropertyDependency> dependentProperties, BuilderContext context ) @@ -289,7 +308,7 @@ private static Type ResolveType( return type; } - var signature = new ClassSignature( clrType, properties, apiVersion ); + var signature = new ClassSignature( typeKey.FullName, clrType, properties, apiVersion ); if ( hasUnfinishedTypes ) { @@ -322,8 +341,7 @@ private static Type ResolveType( } [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static Type MakeEnumerable( Type itemType ) => - ( ienumerableOfT ??= typeof( IEnumerable<> ) ).MakeGenericType( itemType ); + private static Type MakeEnumerable( Type itemType ) => ( ienumerableOfT ??= typeof( IEnumerable<> ) ).MakeGenericType( itemType ); [MethodImpl( MethodImplOptions.AggressiveInlining )] private static Type CreateTypeFromSignature( ModuleBuilder moduleBuilder, ClassSignature @class ) => @@ -352,7 +370,7 @@ private static TypeBuilder CreateTypeBuilderFromSignature( ModuleBuilder moduleB return typeBuilder; } - private static IDictionary<EdmTypeKey, Type> ResolveDependencies( BuilderContext context ) + private static Dictionary<EdmTypeKey, Type> ResolveDependencies( BuilderContext context ) { var edmTypes = context.EdmTypes; @@ -389,7 +407,11 @@ private static IDictionary<EdmTypeKey, Type> ResolveDependencies( BuilderContext return edmTypes; } - private static PropertyBuilder AddProperty( TypeBuilder addTo, Type shouldBeAdded, string name, IReadOnlyList<CustomAttributeBuilder> customAttributes ) + private static PropertyBuilder AddProperty( + TypeBuilder addTo, + Type shouldBeAdded, + string name, + IReadOnlyList<CustomAttributeBuilder> customAttributes ) { const MethodAttributes propertyMethodAttributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; var field = addTo.DefineField( "_" + name, shouldBeAdded, FieldAttributes.Private ); @@ -418,7 +440,7 @@ private static PropertyBuilder AddProperty( TypeBuilder addTo, Type shouldBeAdde return propertyBuilder; } - private static AssemblyName NewAssemblyName(ApiVersion apiVersion) + private static AssemblyName NewAssemblyName( ApiVersion apiVersion, bool adHoc ) { // this is not strictly necessary, but it makes debugging a bit easier as each // assembly-qualified type name provides visibility as to which api version a @@ -455,20 +477,26 @@ private static AssemblyName NewAssemblyName(ApiVersion apiVersion) } name.Insert( 0, 'V' ) - .Append( NewGuid().ToString( "n", InvariantCulture ) ) - .Append( ".DynamicModels" ); + .Append( NewGuid().ToString( "n", InvariantCulture ) ); + + if ( adHoc ) + { + name.Append( ".AdHoc" ); + } + + name.Append( ".DynamicModels" ); return new( name.ToString() ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static ModuleBuilder CreateModuleForApiVersion( ApiVersion apiVersion ) + private ModuleBuilder CreateModuleForApiVersion( EdmModelKey key ) { - var name = NewAssemblyName( apiVersion ); + var assemblyName = NewAssemblyName( key.ApiVersion, adHoc ); #if NETFRAMEWORK - var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( name, Run ); + var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( assemblyName, Run ); #else - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( name, Run ); + var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( assemblyName, Run ); #endif return assemblyBuilder.DefineDynamicModule( "<module>" ); } @@ -492,11 +520,11 @@ internal BuilderContext( IEdmModel edmModel, ApiVersion apiVersion, Func<ModuleB internal IEdmModel EdmModel { get; } - internal IDictionary<EdmTypeKey, Type> EdmTypes { get; } = new Dictionary<EdmTypeKey, Type>(); + internal Dictionary<EdmTypeKey, Type> EdmTypes { get; } = []; - internal ISet<EdmTypeKey> VisitedEdmTypes => visitedEdmTypes ??= new(); + internal HashSet<EdmTypeKey> VisitedEdmTypes => visitedEdmTypes ??= []; - internal IList<PropertyDependency> Dependencies => dependencies ??= new(); + internal List<PropertyDependency> Dependencies => dependencies ??= []; internal bool HasDependencies => dependencies != null && dependencies.Count > 0; } diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/EdmModelKey.cs b/src/Common/src/Common.OData.ApiExplorer/OData/EdmModelKey.cs new file mode 100644 index 00000000..3fd0380a --- /dev/null +++ b/src/Common/src/Common.OData.ApiExplorer/OData/EdmModelKey.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.OData; + +using Microsoft.OData.Edm; + +internal readonly struct EdmModelKey : IEquatable<EdmModelKey> +{ + private readonly int hashCode; + + public readonly IEdmModel EdmModel; + public readonly ApiVersion ApiVersion; + + internal EdmModelKey( IEdmModel model, ApiVersion apiVersion ) => + hashCode = HashCode.Combine( ( EdmModel = model ).GetHashCode(), ApiVersion = apiVersion ); + + public static bool operator ==( EdmModelKey obj, EdmModelKey other ) => obj.Equals( other ); + + public static bool operator !=( EdmModelKey obj, EdmModelKey other ) => !obj.Equals( other ); + + public override int GetHashCode() => hashCode; + + public override bool Equals( object? obj ) => obj is EdmModelKey other && Equals( other ); + + public bool Equals( EdmModelKey other ) => hashCode == other.hashCode; +} \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/EdmTypeKey.cs b/src/Common/src/Common.OData.ApiExplorer/OData/EdmTypeKey.cs index 7c0301a8..4fd693d1 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/EdmTypeKey.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/EdmTypeKey.cs @@ -8,14 +8,17 @@ namespace Asp.Versioning.OData; { private readonly int hashCode; + public readonly string FullName; + public readonly ApiVersion ApiVersion; + internal EdmTypeKey( IEdmStructuredType type, ApiVersion apiVersion ) => - hashCode = HashCode.Combine( type.FullTypeName(), apiVersion ); + hashCode = HashCode.Combine( FullName = type.FullTypeName(), ApiVersion = apiVersion ); internal EdmTypeKey( IEdmTypeReference type, ApiVersion apiVersion ) => - hashCode = HashCode.Combine( type.FullName(), apiVersion ); + hashCode = HashCode.Combine( FullName = type.FullName(), ApiVersion = apiVersion ); internal EdmTypeKey( string fullTypeName, ApiVersion apiVersion ) => - hashCode = HashCode.Combine( fullTypeName, apiVersion ); + hashCode = HashCode.Combine( FullName = fullTypeName, ApiVersion = apiVersion ); public static bool operator ==( EdmTypeKey obj, EdmTypeKey other ) => obj.Equals( other ); diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/ODataValue{T}.cs b/src/Common/src/Common.OData.ApiExplorer/OData/ODataValue{T}.cs index 20a2e31a..284fb87b 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/ODataValue{T}.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/ODataValue{T}.cs @@ -23,6 +23,6 @@ public class ODataValue<T> #if NETFRAMEWORK public T Value { get; set; } = default!; #else - required public T Value { get; set; } + public required T Value { get; set; } #endif } \ No newline at end of file diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/PropertyDependency.cs b/src/Common/src/Common.OData.ApiExplorer/OData/PropertyDependency.cs index 562229bd..bf84b902 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/PropertyDependency.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/PropertyDependency.cs @@ -4,7 +4,7 @@ namespace Asp.Versioning.OData; using System.Reflection.Emit; -internal class PropertyDependency +internal sealed class PropertyDependency { internal PropertyDependency( EdmTypeKey dependentOnTypeKey, diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs index fb13bbe8..a32cd16a 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs @@ -3,11 +3,13 @@ namespace Asp.Versioning.OData; #if NETFRAMEWORK +using Microsoft.OData.Edm; using System.Net.Http; using System.Web.Http; #else using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Results; +using Microsoft.OData.Edm; #endif using System.Reflection; using System.Reflection.Emit; @@ -41,25 +43,17 @@ public static partial class TypeExtensions /// provided <paramref name="context"/>.</returns> public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContext context ) { - if ( type == null ) - { - throw new ArgumentNullException( nameof( type ) ); - } - - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( type ); + ArgumentNullException.ThrowIfNull( context ); var openTypes = new Stack<Type>(); var apiVersion = context.ApiVersion; var resolver = new StructuredTypeResolver( context.Model ); + IEdmStructuredType? structuredType; if ( IsSubstitutableGeneric( type, openTypes, out var innerType ) ) { - var structuredType = resolver.GetStructuredType( innerType! ); - - if ( structuredType == null ) + if ( ( structuredType = resolver.GetStructuredType( innerType! ) ) == null ) { return type; } @@ -74,14 +68,9 @@ public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContex return CloseGeneric( openTypes, newType ); } - if ( CanBeSubstituted( type ) ) + if ( CanBeSubstituted( type ) && ( structuredType = resolver.GetStructuredType( type ) ) != null ) { - var structuredType = resolver.GetStructuredType( type ); - - if ( structuredType != null ) - { - type = context.ModelTypeBuilder.NewStructuredType( context.Model, structuredType, type, apiVersion ); - } + type = context.ModelTypeBuilder.NewStructuredType( context.Model, structuredType, type, apiVersion ); } return type; @@ -125,10 +114,10 @@ internal static IEnumerable<CustomAttributeBuilder> DeclaredAttributes( this Mem yield return new CustomAttributeBuilder( ctor, ctorArgs, - namedProperties.ToArray(), - propertyValues.ToArray(), - namedFields.ToArray(), - fieldValues.ToArray() ); + [.. namedProperties], + [.. propertyValues], + [.. namedFields], + [.. fieldValues] ); } } @@ -242,23 +231,17 @@ private static Type CloseGeneric( Stack<Type> openTypes, Type innerType ) return type; } - private static bool CanBeSubstituted( Type type ) - { - return Type.GetTypeCode( type ) == TypeCode.Object && - !type.IsValueType && - !type.Equals( ActionResultType ) && + [MethodImpl( MethodImplOptions.AggressiveInlining )] + private static bool CanBeSubstituted( Type type ) => + Type.GetTypeCode( type ) == TypeCode.Object && + !type.IsValueType && + !type.Equals( ActionResultType ) && #if NETFRAMEWORK - !type.Equals( HttpResponseType ) && + !type.Equals( HttpResponseType ) && #endif - !type.IsODataActionParameters(); - } + !type.IsODataActionParameters(); - internal static bool IsEnumerable( - this Type type, -#if !NETFRAMEWORK - [NotNullWhen( true )] -#endif - out Type? itemType ) + internal static bool IsEnumerable( this Type type, [NotNullWhen( true )] out Type? itemType ) { var types = new Queue<Type>(); @@ -295,6 +278,7 @@ internal static bool IsEnumerable( return false; } + [MethodImpl( MethodImplOptions.AggressiveInlining )] private static bool IsSingleResult( this Type type ) => type.Is( SingleResultOfT ); private static bool IsODataValue( this Type? type ) @@ -323,6 +307,7 @@ private static bool IsODataValue( this Type? type ) private static bool Is( this Type type, Type typeDefinition ) => type.IsGenericType && type.GetGenericTypeDefinition().Equals( typeDefinition ); + [MethodImpl( MethodImplOptions.AggressiveInlining )] private static bool ShouldExtractInnerType( this Type type ) => type.IsDelta() || #if !NETFRAMEWORK diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs b/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs index baad2af6..b47ae724 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/TypeSubstitutionContext.cs @@ -36,7 +36,7 @@ public class TypeSubstitutionContext /// Gets API version associated with the source model. /// </summary> /// <value>The associated <see cref="ApiVersion">API version</see>.</value> - public ApiVersion ApiVersion => apiVersion ??= Model.GetAnnotationValue<ApiVersionAnnotation>( Model )?.ApiVersion ?? ApiVersion.Neutral; + public ApiVersion ApiVersion => apiVersion ??= Model.GetApiVersion() ?? ApiVersion.Neutral; /// <summary> /// Gets the model type builder used to create substitution types. diff --git a/src/Common/src/Common.OData/Microsoft.OData.Edm/IEdmModelExtensions.cs b/src/Common/src/Common.OData/Microsoft.OData.Edm/IEdmModelExtensions.cs new file mode 100644 index 00000000..04d14dd6 --- /dev/null +++ b/src/Common/src/Common.OData/Microsoft.OData.Edm/IEdmModelExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Microsoft.OData.Edm; + +using Asp.Versioning; +using Asp.Versioning.OData; + +/// <summary> +/// Provides extension methods for <see cref="IEdmModel"/>. +/// </summary> +public static class IEdmModelExtensions +{ + /// <summary> + /// Gets the API version associated with the Entity Data Model (EDM). + /// </summary> + /// <param name="model">The extended <see cref="IEdmModel">EDM</see>.</param> + /// <returns>The associated <see cref="ApiVersion">API version</see> or <c>null</c>.</returns> + public static ApiVersion? GetApiVersion( this IEdmModel model ) => + model.GetAnnotationValue<ApiVersionAnnotation>( model )?.ApiVersion; + + /// <summary> + /// Gets a value indicating whether the Entity Data Model (EDM) is for defined ad hoc usage. + /// </summary> + /// <param name="model">The extended <see cref="IEdmModel">EDM</see>.</param> + /// <returns>True if the EDM is defined for ad hoc usage; otherwise, false.</returns> + public static bool IsAdHoc( this IEdmModel model ) => + model.GetAnnotationValue<AdHocAnnotation>( model ) is not null; +} \ No newline at end of file diff --git a/src/Common/src/Common.OData/OData/AdHocAnnotation.cs b/src/Common/src/Common.OData/OData/AdHocAnnotation.cs new file mode 100644 index 00000000..c787bacc --- /dev/null +++ b/src/Common/src/Common.OData/OData/AdHocAnnotation.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning.OData; + +/// <summary> +/// Represents an annotation for ad hoc usage. +/// </summary> +public sealed class AdHocAnnotation +{ + /// <summary> + /// Gets a singleton instance of the annotation. + /// </summary> + /// <value>A singleton <see cref="AdHocAnnotation">annotation</see> instance.</value> + public static AdHocAnnotation Instance { get; } = new(); +} \ No newline at end of file diff --git a/src/Common/src/Common.OData/OData/ODataId.cs b/src/Common/src/Common.OData/OData/ODataId.cs index 4206e994..e4aa3fd2 100644 --- a/src/Common/src/Common.OData/OData/ODataId.cs +++ b/src/Common/src/Common.OData/OData/ODataId.cs @@ -21,6 +21,6 @@ public class ODataId #if NETFRAMEWORK public Uri Value { get; set; } = default!; #else - required public Uri Value { get; set; } + public required Uri Value { get; set; } #endif } \ No newline at end of file diff --git a/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs b/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs index c54ad30f..a18cc8dc 100644 --- a/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs +++ b/src/Common/src/Common.OData/OData/VersionedODataModelBuilder.cs @@ -19,9 +19,12 @@ public partial class VersionedODataModelBuilder /// <summary> /// Gets or sets the factory method used to create model builders. /// </summary> - /// <value>The factory <see cref="Func{TResult}">method</see> used to create <see cref="ODataModelBuilder">model builders</see>.</value> - /// <remarks>The default implementation creates default instances of the <see cref="ODataConventionModelBuilder"/> class.</remarks> - public Func<ODataModelBuilder> ModelBuilderFactory { get; set; } = () => new ODataConventionModelBuilder().EnableLowerCamelCase(); + /// <value>The factory <see cref="Func{TResult}">method</see> used to create + /// <see cref="ODataModelBuilder">model builders</see>.</value> + /// <remarks>The default implementation creates default instances of the + /// <see cref="ODataConventionModelBuilder"/> class.</remarks> + public Func<ODataModelBuilder> ModelBuilderFactory { get; set; } = + static () => new ODataConventionModelBuilder().EnableLowerCamelCase(); /// <summary> /// Gets or sets the default model configuration. @@ -34,7 +37,7 @@ public partial class VersionedODataModelBuilder /// Gets the list of model configurations associated with the builder. /// </summary> /// <value>A <see cref="IList{T}">list</see> of model configurations associated with the builder.</value> - public IList<IModelConfiguration> ModelConfigurations => modelConfigurations ??= new(); + public IList<IModelConfiguration> ModelConfigurations => modelConfigurations ??= []; /// <summary> /// Gets or sets the action that is invoked after the <see cref="IEdmModel">EDM model</see> has been created. @@ -97,7 +100,7 @@ private IReadOnlyList<IModelConfiguration> GetMergedConfigurations() private void BuildModelPerApiVersion( IReadOnlyList<ApiVersion> apiVersions, IReadOnlyList<IModelConfiguration> configurations, - ICollection<IEdmModel> models, + List<IEdmModel> models, string? routePrefix ) { for ( var i = 0; i < apiVersions.Count; i++ ) @@ -110,11 +113,10 @@ private void BuildModelPerApiVersion( configurations[j].Apply( builder, apiVersion, routePrefix ); } + const int EntityContainerOnly = 1; var model = builder.GetEdmModel(); var container = model.EntityContainer; - var empty = !container.EntitySets().Any() && - !container.Singletons().Any() && - !container.OperationImports().Any(); + var empty = model.SchemaElements.Count() == EntityContainerOnly; if ( empty ) { diff --git a/src/Common/src/Common.ProblemDetails/ProblemDetailsDefaults.cs b/src/Common/src/Common.ProblemDetails/ProblemDetailsDefaults.cs index 7b0f8258..bc668a05 100644 --- a/src/Common/src/Common.ProblemDetails/ProblemDetailsDefaults.cs +++ b/src/Common/src/Common.ProblemDetails/ProblemDetailsDefaults.cs @@ -48,6 +48,9 @@ public static class ProblemDetailsDefaults "Ambiguous API version", "AmbiguousApiVersion" ); +#pragma warning disable IDE0079 +#pragma warning disable CA1034 // Nested types should not be visible + /// <summary> /// Represents the default problem details media type. /// </summary> diff --git a/src/Common/src/Common/ApiVersionReader.cs b/src/Common/src/Common/ApiVersionReader.cs index 4bc670c1..0fc2f51f 100644 --- a/src/Common/src/Common/ApiVersionReader.cs +++ b/src/Common/src/Common/ApiVersionReader.cs @@ -16,6 +16,14 @@ namespace Asp.Versioning; #endif public static class ApiVersionReader { + private static IApiVersionReader? @default; + + /// <summary> + /// Gets the default API version reader. + /// </summary> + /// <value>The default <see cref="IApiVersionReader"/>.</value> + public static IApiVersionReader Default => @default ??= Combine( new QueryStringApiVersionReader(), new UrlSegmentApiVersionReader() ); + /// <summary> /// Returns a new API version reader that is a combination of the specified set. /// </summary> @@ -27,17 +35,14 @@ public static IApiVersionReader Combine( IApiVersionReader apiVersionReader, params IApiVersionReader[] otherApiVersionReaders ) { - if ( apiVersionReader == null ) - { - throw new ArgumentNullException( nameof( apiVersionReader ) ); - } + ArgumentNullException.ThrowIfNull( apiVersionReader ); int count; IApiVersionReader[] apiVersionReaders; if ( otherApiVersionReaders is null || ( count = otherApiVersionReaders.Length ) == 0 ) { - apiVersionReaders = new[] { apiVersionReader }; + apiVersionReaders = [apiVersionReader]; } else { @@ -61,7 +66,7 @@ public static IApiVersionReader Combine( IEnumerable<IApiVersionReader> apiVersi if ( readers is null || readers.Length == 0 ) { - throw new ArgumentException( CommonSR.ZeroApiVersionReaders, nameof( apiVersionReaders ) ); + throw new System.ArgumentException( CommonSR.ZeroApiVersionReaders, nameof( apiVersionReaders ) ); } return new CombinedApiVersionReader( readers ); @@ -110,7 +115,7 @@ public IReadOnlyList<string> Read( HttpRequest request ) if ( versions == null ) { - return version == null ? Array.Empty<string>() : new[] { version }; + return version == null ? [] : [version]; } return versions.ToArray(); diff --git a/src/Common/src/Common/ApiVersioningOptions.cs b/src/Common/src/Common/ApiVersioningOptions.cs index 3b0c8c98..82c1cfce 100644 --- a/src/Common/src/Common/ApiVersioningOptions.cs +++ b/src/Common/src/Common/ApiVersioningOptions.cs @@ -3,7 +3,9 @@ namespace Asp.Versioning; using Asp.Versioning.Routing; -using static Asp.Versioning.ApiVersionReader; +#if NETFRAMEWORK +using System.Net; +#endif /// <summary> /// Represents the possible options for API versioning. @@ -45,7 +47,7 @@ public partial class ApiVersioningOptions /// <value>True if the a default API version should be assumed when a client does not /// provide an API version; otherwise, false. The default value is <c>false</c>.</value> /// <remarks>When a default API version is assumed, the version used is based up the - /// result of the <see cref="IApiVersionSelector.SelectVersion"/> method.</remarks> + /// result from <see cref="IApiVersionSelector"/>.</remarks> public bool AssumeDefaultVersionWhenUnspecified { get; set; } /// <summary> @@ -70,7 +72,7 @@ public partial class ApiVersioningOptions #endif public IApiVersionReader ApiVersionReader { - get => apiVersionReader ??= Combine( new QueryStringApiVersionReader(), new UrlSegmentApiVersionReader() ); + get => apiVersionReader ??= Versioning.ApiVersionReader.Default; set => apiVersionReader = value; } @@ -100,4 +102,35 @@ public IApiVersioningPolicyBuilder Policies get => apiVersioningPolicyBuilder ??= new ApiVersioningPolicyBuilder(); set => apiVersioningPolicyBuilder = value; } + + /// <summary> + /// Gets or sets the HTTP status code used for unsupported versions of an API. + /// </summary> + /// <value>The HTTP status code. The default value is 400 (Bad Request).</value> + /// <remarks> + /// <para>While any HTTP status code can be provided, the following are the most sensible:</para> + /// <list type="table"> + /// <listheader> + /// <term>Status</term> + /// <description>Description</description> + /// </listheader> + /// <item> + /// <term>400 (Bad Request)</term> + /// <description>The API doesn't support this version</description> + /// </item> + /// <item> + /// <term>404 (Not Found)</term> + /// <description>The API doesn't exist</description> + /// </item> + /// <item> + /// <term>501 (Not Implemented)</term> + /// <description>The API isn't implemented</description> + /// </item> + /// </list> + /// </remarks> +#if NETFRAMEWORK + public HttpStatusCode UnsupportedApiVersionStatusCode { get; set; } = HttpStatusCode.BadRequest; +#else + public int UnsupportedApiVersionStatusCode { get; set; } = 400; +#endif } \ No newline at end of file diff --git a/src/Common/src/Common/ApiVersioningPolicyBuilder.cs b/src/Common/src/Common/ApiVersioningPolicyBuilder.cs index c0894ab6..2eb50a29 100644 --- a/src/Common/src/Common/ApiVersioningPolicyBuilder.cs +++ b/src/Common/src/Common/ApiVersioningPolicyBuilder.cs @@ -27,13 +27,13 @@ public virtual ISunsetPolicyBuilder Sunset( string? name, ApiVersion? apiVersion { if ( string.IsNullOrEmpty( name ) && apiVersion == null ) { - var message = string.Format( CultureInfo.CurrentCulture, CommonSR.InvalidPolicyKey, nameof( name ), nameof( apiVersion ) ); - throw new ArgumentException( message ); + var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidPolicyKey, nameof( name ), nameof( apiVersion ) ); + throw new System.ArgumentException( message ); } var key = new PolicyKey( name, apiVersion ); - sunsetPolicies ??= new(); + sunsetPolicies ??= []; if ( !sunsetPolicies.TryGetValue( key, out var builder ) ) { diff --git a/src/Common/src/Common/CollectionExtensions.cs b/src/Common/src/Common/CollectionExtensions.cs index 157ac438..4c6f1e4e 100644 --- a/src/Common/src/Common/CollectionExtensions.cs +++ b/src/Common/src/Common/CollectionExtensions.cs @@ -7,10 +7,7 @@ internal static partial class CollectionExtensions internal static bool TryGetValue<TKey, TValue>( this IDictionary<TKey, object?> dictionary, TKey key, -#if !NETFRAMEWORK - [MaybeNullWhen( false )] -#endif - out TValue value ) + [MaybeNullWhen( false )] out TValue value ) where TKey : notnull { if ( dictionary.TryGetValue( key, out var val ) && val is TValue v ) diff --git a/src/Common/src/Common/CurrentImplementationApiVersionSelector.cs b/src/Common/src/Common/CurrentImplementationApiVersionSelector.cs index 1fd29702..8cd7510f 100644 --- a/src/Common/src/Common/CurrentImplementationApiVersionSelector.cs +++ b/src/Common/src/Common/CurrentImplementationApiVersionSelector.cs @@ -34,10 +34,7 @@ public class CurrentImplementationApiVersionSelector : IApiVersionSelector /// <remarks>This method always returns the default <see cref="ApiVersion.Default">API version</see>.</remarks> public virtual ApiVersion SelectVersion( HttpRequest request, ApiVersionModel model ) { - if ( model == null ) - { - throw new ArgumentNullException( nameof( model ) ); - } + ArgumentNullException.ThrowIfNull( model ); return model.ImplementedApiVersions.Count switch { diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs index e877d8cd..72ed1408 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -7,7 +7,6 @@ namespace Asp.Versioning; using HttpResponse = System.Net.Http.HttpResponseMessage; #else using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; #endif using static Asp.Versioning.ApiVersionMapping; @@ -23,12 +22,14 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions private const string ApiDeprecatedVersions = "api-deprecated-versions"; private const string Sunset = nameof( Sunset ); private const string Link = nameof( Link ); + private readonly ISunsetPolicyManager sunsetPolicyManager; private readonly string apiSupportedVersionsName; private readonly string apiDeprecatedVersionsName; /// <summary> /// Initializes a new instance of the <see cref="DefaultApiVersionReporter"/> class. /// </summary> + /// <param name="sunsetPolicyManager">The <see cref="ISunsetPolicyManager">manager</see> used to resolve sunset policies.</param> /// <param name="supportedHeaderName">The HTTP header name used for supported API versions. /// The default value is "api-supported-versions".</param> /// <param name="deprecatedHeaderName">THe HTTP header name used for deprecated API versions. @@ -36,21 +37,19 @@ public sealed partial class DefaultApiVersionReporter : IReportApiVersions /// <param name="mapping">One or more of API versioning mappings. The default value is /// <see cref="ApiVersionMapping.Explicit"/> and <see cref="ApiVersionMapping.Implicit"/>.</param> public DefaultApiVersionReporter( + ISunsetPolicyManager sunsetPolicyManager, string supportedHeaderName = ApiSupportedVersions, string deprecatedHeaderName = ApiDeprecatedVersions, ApiVersionMapping mapping = Explicit | Implicit ) { - Mapping = mapping; - - if ( string.IsNullOrEmpty( apiSupportedVersionsName = supportedHeaderName ) ) - { - throw new ArgumentNullException( nameof( supportedHeaderName ) ); - } + ArgumentNullException.ThrowIfNull( sunsetPolicyManager ); + ArgumentException.ThrowIfNullOrEmpty( supportedHeaderName ); + ArgumentException.ThrowIfNullOrEmpty( deprecatedHeaderName ); - if ( string.IsNullOrEmpty( apiDeprecatedVersionsName = deprecatedHeaderName ) ) - { - throw new ArgumentNullException( nameof( deprecatedHeaderName ) ); - } + this.sunsetPolicyManager = sunsetPolicyManager; + apiSupportedVersionsName = supportedHeaderName; + apiDeprecatedVersionsName = deprecatedHeaderName; + Mapping = mapping; } /// <inheritdoc /> @@ -59,15 +58,8 @@ public DefaultApiVersionReporter( /// <inheritdoc /> public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) { - if ( response == null ) - { - throw new ArgumentNullException( nameof( response ) ); - } - - if ( apiVersionModel == null ) - { - throw new ArgumentNullException( nameof( apiVersionModel ) ); - } + ArgumentNullException.ThrowIfNull( response ); + ArgumentNullException.ThrowIfNull( apiVersionModel ); if ( apiVersionModel.IsApiVersionNeutral ) { @@ -79,18 +71,6 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) AddApiVersionHeader( headers, apiSupportedVersionsName, apiVersionModel.SupportedApiVersions ); AddApiVersionHeader( headers, apiDeprecatedVersionsName, apiVersionModel.DeprecatedApiVersions ); -#if NETFRAMEWORK - var statusCode = (int) response.StatusCode; -#else - var context = response.HttpContext; - var statusCode = response.StatusCode; -#endif - - if ( statusCode < 200 || statusCode > 299 ) - { - return; - } - #if NETFRAMEWORK if ( response.RequestMessage is not HttpRequestMessage request || request.GetActionDescriptor()?.GetApiVersionMetadata() is not ApiVersionMetadata metadata ) @@ -98,23 +78,20 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) return; } - var name = metadata.Name; - var policyManager = request.GetConfiguration().DependencyResolver.GetSunsetPolicyManager(); var version = request.GetRequestedApiVersion(); #else + var context = response.HttpContext; + if ( context.GetEndpoint()?.Metadata.GetMetadata<ApiVersionMetadata>() is not ApiVersionMetadata metadata ) { return; } - var name = metadata.Name; - var policyManager = context.RequestServices.GetRequiredService<ISunsetPolicyManager>(); var version = context.GetRequestedApiVersion(); #endif + var name = metadata.Name; - if ( policyManager.TryGetPolicy( name, version, out var policy ) || - ( !string.IsNullOrEmpty( name ) && policyManager.TryGetPolicy( name, out policy ) ) || - ( version != null && policyManager.TryGetPolicy( version, out policy ) ) ) + if ( sunsetPolicyManager.TryResolvePolicy( name, version, out var policy ) ) { response.WriteSunsetPolicy( policy ); } diff --git a/src/Common/src/Common/HeaderApiVersionReader.cs b/src/Common/src/Common/HeaderApiVersionReader.cs index 8b80ec88..2bffbf31 100644 --- a/src/Common/src/Common/HeaderApiVersionReader.cs +++ b/src/Common/src/Common/HeaderApiVersionReader.cs @@ -22,7 +22,7 @@ public HeaderApiVersionReader() { } /// </summary> /// <param name="headerNames">A <see cref="IEnumerable{T}">sequence</see> of HTTP header names to read the API version from.</param> public HeaderApiVersionReader( IEnumerable<string> headerNames ) => - HeaderNames.AddRange( headerNames ?? throw new ArgumentNullException( nameof( headerNames ) ) ); + HeaderNames.AddRange( headerNames ?? throw new System.ArgumentNullException( nameof( headerNames ) ) ); /// <summary> /// Initializes a new instance of the <see cref="HeaderApiVersionReader"/> class. @@ -31,10 +31,7 @@ public HeaderApiVersionReader( IEnumerable<string> headerNames ) => /// <param name="otherHeaderNames">An array of other HTTP header names to read the API version from.</param> public HeaderApiVersionReader( string headerName, params string[] otherHeaderNames ) { - if ( string.IsNullOrEmpty( headerName ) ) - { - throw new ArgumentNullException( headerName ); - } + ArgumentException.ThrowIfNullOrEmpty( headerName ); HeaderNames.Add( headerName ); @@ -65,10 +62,7 @@ public HeaderApiVersionReader( string headerName, params string[] otherHeaderNam /// <param name="context">The <see cref="IApiVersionParameterDescriptionContext">context</see> used to add API version parameter descriptions.</param> public virtual void AddParameters( IApiVersionParameterDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var count = HeaderNames.Count; #if NETFRAMEWORK diff --git a/src/Common/src/Common/IApiVersionSelector.cs b/src/Common/src/Common/IApiVersionSelector.cs index 84eca910..3063fe95 100644 --- a/src/Common/src/Common/IApiVersionSelector.cs +++ b/src/Common/src/Common/IApiVersionSelector.cs @@ -11,10 +11,7 @@ namespace Asp.Versioning; /// <summary> /// Defines the behavior of an API version selector. /// </summary> -#if !NETFRAMEWORK -[CLSCompliant( false )] -#endif -public interface IApiVersionSelector +public partial interface IApiVersionSelector { /// <summary> /// Selects an API version given the specified HTTP request and API version information. diff --git a/src/Common/src/Common/LowestImplementedApiVersionSelector.cs b/src/Common/src/Common/LowestImplementedApiVersionSelector.cs index ff39576a..49d58355 100644 --- a/src/Common/src/Common/LowestImplementedApiVersionSelector.cs +++ b/src/Common/src/Common/LowestImplementedApiVersionSelector.cs @@ -34,10 +34,7 @@ public class LowestImplementedApiVersionSelector : IApiVersionSelector /// <remarks>This method always returns the default <see cref="ApiVersion.Default">API version</see>.</remarks> public virtual ApiVersion SelectVersion( HttpRequest request, ApiVersionModel model ) { - if ( model == null ) - { - throw new ArgumentNullException( nameof( model ) ); - } + ArgumentNullException.ThrowIfNull( model ); return model.ImplementedApiVersions.Count switch { diff --git a/src/Common/src/Common/MediaTypeApiVersionReader.cs b/src/Common/src/Common/MediaTypeApiVersionReader.cs index ecd7dacd..4f01f4f0 100644 --- a/src/Common/src/Common/MediaTypeApiVersionReader.cs +++ b/src/Common/src/Common/MediaTypeApiVersionReader.cs @@ -28,11 +28,7 @@ public partial class MediaTypeApiVersionReader : IApiVersionReader /// <param name="parameterName">The name of the media type parameter to read the API version from.</param> public MediaTypeApiVersionReader( string parameterName ) { - if ( string.IsNullOrEmpty( parameterName ) ) - { - throw new ArgumentNullException( parameterName ); - } - + ArgumentException.ThrowIfNullOrEmpty( parameterName ); ParameterName = parameterName; } @@ -53,10 +49,7 @@ public MediaTypeApiVersionReader( string parameterName ) /// quality parameter.</remarks> protected virtual string? ReadAcceptHeader( ICollection<MediaTypeWithQualityHeaderValue> accept ) { - if ( accept == null ) - { - throw new ArgumentNullException( nameof( accept ) ); - } + ArgumentNullException.ThrowIfNull( accept ); var count = accept.Count; @@ -103,10 +96,7 @@ public MediaTypeApiVersionReader( string parameterName ) /// <returns>The API version read or <c>null</c>.</returns> protected virtual string? ReadContentTypeHeader( MediaTypeHeaderValue contentType ) { - if ( contentType == null ) - { - throw new ArgumentNullException( nameof( contentType ) ); - } + ArgumentNullException.ThrowIfNull( contentType ); #if NETFRAMEWORK var parameters = contentType.Parameters.ToArray(); var count = parameters.Length; @@ -137,11 +127,7 @@ public MediaTypeApiVersionReader( string parameterName ) /// <param name="context">The <see cref="IApiVersionParameterDescriptionContext">context</see> used to add API version parameter descriptions.</param> public virtual void AddParameters( IApiVersionParameterDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } - + ArgumentNullException.ThrowIfNull( context ); context.AddParameter( ParameterName, MediaTypeParameter ); } diff --git a/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs b/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs index d1d34217..af03ef2f 100644 --- a/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs +++ b/src/Common/src/Common/MediaTypeApiVersionReaderBuilder.cs @@ -2,25 +2,40 @@ #pragma warning disable IDE0079 #pragma warning disable SA1121 - -namespace Asp.Versioning; +#pragma warning disable SA1135 +#pragma warning disable SA1200 #if NETFRAMEWORK using System.Net.Http.Headers; +using HttpRequest = System.Net.Http.HttpRequestMessage; +using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeWithQualityHeaderValue; #else using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using System.Collections.Frozen; +#endif + +namespace Asp.Versioning; + +#if !NETFRAMEWORK using System.Buffers; #endif using System.Runtime.CompilerServices; using System.Text.RegularExpressions; #if NETFRAMEWORK -using HttpRequest = System.Net.Http.HttpRequestMessage; -using Str = System.String; -using StrComparer = System.StringComparer; +using FrozenSet = HashSet<string>; #else -using MediaTypeWithQualityHeaderValue = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; -using Str = Microsoft.Extensions.Primitives.StringSegment; -using StrComparer = Microsoft.Extensions.Primitives.StringSegmentComparer; +using FrozenSet = FrozenSet<StringSegment>; +#endif +using ReaderCallback = Func<IReadOnlyList<MediaTypeHeaderValue>, IReadOnlyList<string>>; +using SelectorCallback = Func<HttpRequest, IReadOnlyList<string>, IReadOnlyList<string>>; +#if NETFRAMEWORK +using Str = String; +using StrComparer = StringComparer; +#else +using Str = StringSegment; +using StrComparer = StringSegmentComparer; #endif using static Asp.Versioning.ApiVersionParameterLocation; using static System.StringComparison; @@ -33,8 +48,8 @@ public partial class MediaTypeApiVersionReaderBuilder private HashSet<string>? parameters; private HashSet<Str>? included; private HashSet<Str>? excluded; - private Func<HttpRequest, IReadOnlyList<string>, IReadOnlyList<string>>? select; - private List<Func<IReadOnlyList<MediaTypeWithQualityHeaderValue>, IReadOnlyList<string>>>? readers; + private SelectorCallback? select; + private List<ReaderCallback>? readers; /// <summary> /// Adds the name of a media type parameter to be read. @@ -114,7 +129,7 @@ public virtual MediaTypeApiVersionReaderBuilder Match( [StringSyntax( StringSynt [CLSCompliant( false )] #endif #pragma warning disable CA1716 // Identifiers should not match keywords - public virtual MediaTypeApiVersionReaderBuilder Select( Func<HttpRequest, IReadOnlyList<string>, IReadOnlyList<string>> selector ) + public virtual MediaTypeApiVersionReaderBuilder Select( SelectorCallback selector ) #pragma warning restore CA1716 // Identifiers should not match keywords { select = selector; @@ -130,11 +145,19 @@ public virtual MediaTypeApiVersionReaderBuilder Select( Func<HttpRequest, IReadO #endif public virtual IApiVersionReader Build() => new BuiltMediaTypeApiVersionReader( - parameters?.ToArray() ?? Array.Empty<string>(), - included ?? EmptyCollection(), - excluded ?? EmptyCollection(), + parameters?.ToArray() ?? [], +#if NET45 + included ?? [], + excluded ?? [], +#elif NETFRAMEWORK + included ?? new( capacity: 0 ), + excluded ?? new( capacity: 0 ), +#else + included?.ToFrozenSet( included.Comparer ) ?? FrozenSet<Str>.Empty, + excluded?.ToFrozenSet( excluded.Comparer ) ?? FrozenSet<Str>.Empty, +#endif select ?? DefaultSelector, - readers ?? EmptyList() ); + readers?.ToArray() ?? [] ); /// <summary> /// Adds a function used to read the an API version from one or more media types. @@ -144,35 +167,38 @@ public virtual IApiVersionReader Build() => #if !NETFRAMEWORK [CLSCompliant( false )] #endif - protected void AddReader( Func<IReadOnlyList<MediaTypeWithQualityHeaderValue>, IReadOnlyList<string>> reader ) + protected void AddReader( ReaderCallback reader ) { - if ( reader is null ) - { - throw new ArgumentNullException( nameof( reader ) ); - } + ArgumentNullException.ThrowIfNull( reader ); - readers ??= new(); + readers ??= []; readers.Add( reader ); } [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static ICollection<Str> EmptyCollection() => Array.Empty<Str>(); + private static IReadOnlyList<string> DefaultSelector( HttpRequest request, IReadOnlyList<string> versions ) => versions; [MethodImpl( MethodImplOptions.AggressiveInlining )] - private static IReadOnlyList<Func<IReadOnlyList<MediaTypeWithQualityHeaderValue>, IReadOnlyList<string>>> EmptyList() => - Array.Empty<Func<IReadOnlyList<MediaTypeWithQualityHeaderValue>, IReadOnlyList<string>>>(); + private static string[] ToArray( ref string? version, List<string>? versions ) + { + if ( version is null ) + { + return []; + } - private static IReadOnlyList<string> DefaultSelector( HttpRequest request, IReadOnlyList<string> versions ) => versions; + return versions is null ? [version] : [.. versions]; + } - private static IReadOnlyList<string> ReadMediaType( - IReadOnlyList<MediaTypeWithQualityHeaderValue> mediaTypes, + private static string[] ReadMediaType( + IReadOnlyList<MediaTypeHeaderValue> mediaTypes, string pattern ) { var version = default( string ); var versions = default( List<string> ); var regex = default( Regex ); + var count = mediaTypes.Count; - for ( var i = 0; i < mediaTypes.Count; i++ ) + for ( var i = 0; i < count; i++ ) { var mediaType = mediaTypes[i].MediaType; @@ -201,7 +227,7 @@ private static IReadOnlyList<string> ReadMediaType( } else if ( versions == null ) { - versions = new( capacity: mediaTypes.Count - i + 1 ) + versions = new( capacity: count - i + 1 ) { version, value, @@ -216,22 +242,18 @@ private static IReadOnlyList<string> ReadMediaType( } } - if ( version is null ) - { - return Array.Empty<string>(); - } - - return versions is null ? new[] { version } : versions.ToArray(); + return ToArray( ref version, versions ); } - private static IReadOnlyList<string> ReadMediaTypeParameter( - IReadOnlyList<MediaTypeWithQualityHeaderValue> mediaTypes, + private static string[] ReadMediaTypeParameter( + IReadOnlyList<MediaTypeHeaderValue> mediaTypes, string parameterName ) { var version = default( string ); var versions = default( List<string> ); + var count = mediaTypes.Count; - for ( var i = 0; i < mediaTypes.Count; i++ ) + for ( var i = 0; i < count; i++ ) { var mediaType = mediaTypes[i]; @@ -254,7 +276,7 @@ private static IReadOnlyList<string> ReadMediaTypeParameter( } else if ( versions == null ) { - versions = new( capacity: mediaTypes.Count - i + 1 ) + versions = new( capacity: count - i + 1 ) { version, value, @@ -267,28 +289,23 @@ private static IReadOnlyList<string> ReadMediaTypeParameter( } } - if ( version is null ) - { - return Array.Empty<string>(); - } - - return versions is null ? new[] { version } : versions.ToArray(); + return ToArray( ref version, versions ); } private sealed class BuiltMediaTypeApiVersionReader : IApiVersionReader { - private readonly IReadOnlyList<string> parameters; - private readonly ICollection<Str> included; - private readonly ICollection<Str> excluded; - private readonly Func<HttpRequest, IReadOnlyList<string>, IReadOnlyList<string>> selector; - private readonly IReadOnlyList<Func<IReadOnlyList<MediaTypeWithQualityHeaderValue>, IReadOnlyList<string>>> readers; + private readonly string[] parameters; + private readonly FrozenSet included; + private readonly FrozenSet excluded; + private readonly SelectorCallback selector; + private readonly ReaderCallback[] readers; internal BuiltMediaTypeApiVersionReader( - IReadOnlyList<string> parameters, - ICollection<Str> included, - ICollection<Str> excluded, - Func<HttpRequest, IReadOnlyList<string>, IReadOnlyList<string>> selector, - IReadOnlyList<Func<IReadOnlyList<MediaTypeWithQualityHeaderValue>, IReadOnlyList<string>>> readers ) + string[] parameters, + FrozenSet included, + FrozenSet excluded, + SelectorCallback selector, + ReaderCallback[] readers ) { this.parameters = parameters; this.included = included; @@ -299,18 +316,15 @@ internal BuiltMediaTypeApiVersionReader( public void AddParameters( IApiVersionParameterDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); - if ( parameters.Count == 0 ) + if ( parameters.Length == 0 ) { context.AddParameter( name: string.Empty, MediaTypeParameter ); } else { - for ( var i = 0; i < parameters.Count; i++ ) + for ( var i = 0; i < parameters.Length; i++ ) { context.AddParameter( parameters[i], MediaTypeParameter ); } @@ -319,7 +333,7 @@ public void AddParameters( IApiVersionParameterDescriptionContext context ) public IReadOnlyList<string> Read( HttpRequest request ) { - if ( readers.Count == 0 ) + if ( readers.Length == 0 ) { return Array.Empty<string>(); } @@ -327,22 +341,21 @@ public IReadOnlyList<string> Read( HttpRequest request ) #if NETFRAMEWORK var headers = request.Headers; var contentType = request.Content?.Headers.ContentType; - var accept = headers.Accept; #else var headers = request.GetTypedHeaders(); var contentType = headers.ContentType; - var accept = headers.Accept; #endif + var accept = headers.Accept; var version = default( string ); var versions = default( SortedSet<string> ); - var mediaTypes = default( List<MediaTypeWithQualityHeaderValue> ); + var mediaTypes = default( List<MediaTypeHeaderValue> ); if ( contentType != null ) { #if NETFRAMEWORK - mediaTypes = new() { MediaTypeWithQualityHeaderValue.Parse( contentType.ToString() ) }; + mediaTypes = [MediaTypeHeaderValue.Parse( contentType.ToString() )]; #else - mediaTypes = new() { contentType }; + mediaTypes = [contentType]; #endif } @@ -374,13 +387,13 @@ public IReadOnlyList<string> Read( HttpRequest request ) if ( versions == null ) { - return version == null ? Array.Empty<string>() : new[] { version }; + return version == null ? Array.Empty<string>() : [version]; } return selector( request, versions.ToArray() ); } - private void Filter( IList<MediaTypeWithQualityHeaderValue> mediaTypes ) + private void Filter( List<MediaTypeHeaderValue> mediaTypes ) { if ( excluded.Count > 0 ) { @@ -410,11 +423,11 @@ private void Filter( IList<MediaTypeWithQualityHeaderValue> mediaTypes ) } private void Read( - List<MediaTypeWithQualityHeaderValue> mediaTypes, + IReadOnlyList<MediaTypeHeaderValue> mediaTypes, ref string? version, ref SortedSet<string>? versions ) { - for ( var i = 0; i < readers.Count; i++ ) + for ( var i = 0; i < readers.Length; i++ ) { var result = readers[i]( mediaTypes ); diff --git a/src/Common/src/Common/MediaTypeApiVersionReaderBuilderExtensions.cs b/src/Common/src/Common/MediaTypeApiVersionReaderBuilderExtensions.cs index c91073b0..c1e99a54 100644 --- a/src/Common/src/Common/MediaTypeApiVersionReaderBuilderExtensions.cs +++ b/src/Common/src/Common/MediaTypeApiVersionReaderBuilderExtensions.cs @@ -17,12 +17,8 @@ public static class MediaTypeApiVersionReaderBuilderExtensions /// <exception cref="ArgumentNullException"><paramref name="builder"/> is <c>null</c>.</exception> public static T SelectFirstOrDefault<T>( this T builder ) where T : MediaTypeApiVersionReaderBuilder { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - builder.Select( static ( request, versions ) => versions.Count == 0 ? versions : new[] { versions[0] } ); + ArgumentNullException.ThrowIfNull( builder ); + builder.Select( static ( request, versions ) => versions.Count == 0 ? versions : [versions[0]] ); return builder; } @@ -36,12 +32,8 @@ public static T SelectFirstOrDefault<T>( this T builder ) where T : MediaTypeApi /// <exception cref="ArgumentNullException"><paramref name="builder"/> is <c>null</c>.</exception> public static T SelectLastOrDefault<T>( this T builder ) where T : MediaTypeApiVersionReaderBuilder { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - - builder.Select( static ( request, versions ) => versions.Count == 0 ? versions : new[] { versions[versions.Count - 1] } ); + ArgumentNullException.ThrowIfNull( builder ); + builder.Select( static ( request, versions ) => versions.Count == 0 ? versions : [versions[versions.Count - 1]] ); return builder; } } \ No newline at end of file diff --git a/src/Common/src/Common/PolicyKey.cs b/src/Common/src/Common/PolicyKey.cs index 618740b3..e51e2edf 100644 --- a/src/Common/src/Common/PolicyKey.cs +++ b/src/Common/src/Common/PolicyKey.cs @@ -19,11 +19,7 @@ public PolicyKey( string? name, ApiVersion? version ) public bool Equals( PolicyKey other ) => GetHashCode() == other.GetHashCode(); - public override bool Equals( -#if !NETFRAMEWORK - [NotNullWhen( true )] -#endif - object? obj ) => obj is PolicyKey other && Equals( other ); + public override bool Equals( [NotNullWhen( true )] object? obj ) => obj is PolicyKey other && Equals( other ); public override int GetHashCode() { diff --git a/src/Common/src/Common/QueryStringApiVersionReader.cs b/src/Common/src/Common/QueryStringApiVersionReader.cs index a32bde4c..942e0016 100644 --- a/src/Common/src/Common/QueryStringApiVersionReader.cs +++ b/src/Common/src/Common/QueryStringApiVersionReader.cs @@ -28,7 +28,9 @@ public partial class QueryStringApiVersionReader : IApiVersionReader /// <remarks>This constructor adds the "api-version" query string parameter if no other query parameter names are specified.</remarks> public QueryStringApiVersionReader( IEnumerable<string> parameterNames ) { - ParameterNames.AddRange( parameterNames ?? throw new ArgumentNullException( nameof( parameterNames ) ) ); + ArgumentNullException.ThrowIfNull( parameterNames ); + + ParameterNames.AddRange( parameterNames ); if ( ParameterNames.Count == 0 ) { @@ -43,10 +45,7 @@ public QueryStringApiVersionReader( IEnumerable<string> parameterNames ) /// <param name="otherParameterNames">An array of query string parameter names to read the API version from.</param> public QueryStringApiVersionReader( string parameterName, params string[] otherParameterNames ) { - if ( string.IsNullOrEmpty( parameterName ) ) - { - throw new ArgumentNullException( parameterName ); - } + ArgumentException.ThrowIfNullOrEmpty( parameterName ); ParameterNames.Add( parameterName ); @@ -77,10 +76,7 @@ public QueryStringApiVersionReader( string parameterName, params string[] otherP /// <param name="context">The <see cref="IApiVersionParameterDescriptionContext">context</see> used to add API version parameter descriptions.</param> public virtual void AddParameters( IApiVersionParameterDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var count = ParameterNames.Count; #if NETFRAMEWORK diff --git a/src/Common/src/Common/SunsetLinkBuilder.cs b/src/Common/src/Common/SunsetLinkBuilder.cs index 28985ed9..03f72175 100644 --- a/src/Common/src/Common/SunsetLinkBuilder.cs +++ b/src/Common/src/Common/SunsetLinkBuilder.cs @@ -26,7 +26,7 @@ public ILinkBuilder Language( string value ) } else if ( languages == null ) { - languages = new() { language, value }; + languages = [language, value]; } else { diff --git a/src/Common/src/Common/SunsetPolicyBuilder.cs b/src/Common/src/Common/SunsetPolicyBuilder.cs index 13d4aaf2..a6450b49 100644 --- a/src/Common/src/Common/SunsetPolicyBuilder.cs +++ b/src/Common/src/Common/SunsetPolicyBuilder.cs @@ -23,8 +23,8 @@ public SunsetPolicyBuilder( string? name, ApiVersion? apiVersion ) { if ( string.IsNullOrEmpty( name ) && apiVersion == null ) { - var message = string.Format( CultureInfo.CurrentCulture, CommonSR.InvalidPolicyKey, nameof( name ), nameof( apiVersion ) ); - throw new ArgumentException( message ); + var message = string.Format( CultureInfo.CurrentCulture, Format.InvalidPolicyKey, nameof( name ), nameof( apiVersion ) ); + throw new System.ArgumentException( message ); } Name = name; @@ -39,7 +39,7 @@ public SunsetPolicyBuilder( string? name, ApiVersion? apiVersion ) /// <inheritdoc /> public virtual void Per( SunsetPolicy policy ) => - sunsetPolicy = policy ?? throw new ArgumentNullException( nameof( policy ) ); + sunsetPolicy = policy ?? throw new System.ArgumentNullException( nameof( policy ) ); /// <inheritdoc /> public virtual ISunsetPolicyBuilder Effective( DateTimeOffset sunsetDate ) diff --git a/src/Common/src/Common/SunsetPolicyManager.cs b/src/Common/src/Common/SunsetPolicyManager.cs index 2c19a773..50fdf115 100644 --- a/src/Common/src/Common/SunsetPolicyManager.cs +++ b/src/Common/src/Common/SunsetPolicyManager.cs @@ -13,10 +13,7 @@ public partial class SunsetPolicyManager : ISunsetPolicyManager public virtual bool TryGetPolicy( string? name, ApiVersion? apiVersion, -#if !NETFRAMEWORK - [MaybeNullWhen( false )] -#endif - out SunsetPolicy sunsetPolicy ) + [MaybeNullWhen( false )] out SunsetPolicy sunsetPolicy ) { if ( string.IsNullOrEmpty( name ) && apiVersion == null ) { diff --git a/src/Common/src/Common/UrlSegmentApiVersionReader.cs b/src/Common/src/Common/UrlSegmentApiVersionReader.cs index 93d09853..234a41b1 100644 --- a/src/Common/src/Common/UrlSegmentApiVersionReader.cs +++ b/src/Common/src/Common/UrlSegmentApiVersionReader.cs @@ -22,11 +22,7 @@ public UrlSegmentApiVersionReader() { } /// <param name="context">The <see cref="IApiVersionParameterDescriptionContext">context</see> used to add API version parameter descriptions.</param> public virtual void AddParameters( IApiVersionParameterDescriptionContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } - + ArgumentNullException.ThrowIfNull( context ); context.AddParameter( name: string.Empty, Path ); } } \ No newline at end of file diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs index a33ba8f9..baef9fc0 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs @@ -30,7 +30,9 @@ public void action_should_call_action_on_controller_builder() controllerBuilder.Verify( cb => cb.Action( method ), Once() ); } +#pragma warning disable IDE0079 #pragma warning disable CA1034 // Nested types should not be visible + #if !NETFRAMEWORK [ApiController] #endif diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs index b3d912b5..ed3fcbbf 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs @@ -31,7 +31,9 @@ public void action_should_call_action_on_controller_builder() controllerBuilder.Verify( cb => cb.Action( method ), Once() ); } +#pragma warning disable IDE0079 #pragma warning disable CA1034 // Nested types should not be visible + #if !NETFRAMEWORK [ApiController] #endif diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ActionConventionBuilderExtensionsTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ActionConventionBuilderExtensionsTest.cs index 7b52367c..00096fce 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ActionConventionBuilderExtensionsTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ActionConventionBuilderExtensionsTest.cs @@ -107,8 +107,8 @@ public void action_should_throw_exception_when_method_does_not_exist() #pragma warning disable IDE0060 #pragma warning disable IDE0079 #pragma warning disable CA1034 // Nested types should not be visible -#pragma warning disable CA1822 // Mark members as static -#pragma warning disable CA2234 // Pass system uri objects instead of strings +#pragma warning disable CA1822 + #if !NETFRAMEWORK [ApiController] #endif diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ApiVersionConventionBuilderTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ApiVersionConventionBuilderTest.cs index 1808af99..eebd6c1c 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ApiVersionConventionBuilderTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ApiVersionConventionBuilderTest.cs @@ -1,12 +1,14 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +//// Ignore Spelling: Dtime + namespace Asp.Versioning.Conventions { #if NETFRAMEWORK using System.Web.Http; + using System.Web.Http.Results; using ControllerBase = System.Web.Http.ApiController; using ControllerModel = System.Web.Http.Controllers.HttpControllerDescriptor; - using IActionResult = System.Web.Http.IHttpActionResult; #else using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -121,11 +123,12 @@ private sealed class TestApiVersionConventionBuilder : ApiVersionConventionBuild internal IDictionary<Type, IControllerConventionBuilder> ProtectedControllerConventionBuilders => ControllerConventionBuilders; } +#pragma warning disable IDE0079 #pragma warning disable CA1812 private sealed class StubController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } } @@ -138,7 +141,7 @@ namespace v2 #endif internal sealed class UndecoratedController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } } } \ No newline at end of file diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs index 237e600b..a0845583 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTTest.cs @@ -3,8 +3,8 @@ namespace Asp.Versioning.Conventions; #if NETFRAMEWORK +using System.Web.Http.Results; using ControllerBase = System.Web.Http.ApiController; -using IActionResult = System.Web.Http.IHttpActionResult; #else using Microsoft.AspNetCore.Mvc; #endif @@ -74,6 +74,7 @@ private sealed class TestControllerApiVersionConventionBuilder : ControllerApiVe internal ActionApiVersionConventionBuilderCollection<ControllerBase> ProtectedActionBuilders => ActionBuilders; } +#pragma warning disable IDE0079 #pragma warning disable CA1812 #if !NETFRAMEWORK @@ -81,7 +82,7 @@ private sealed class TestControllerApiVersionConventionBuilder : ControllerApiVe #endif private sealed class UndecoratedController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } #if !NETFRAMEWORK @@ -93,6 +94,6 @@ private sealed class UndecoratedController : ControllerBase [AdvertiseApiVersions( "3.0-Beta", Deprecated = true )] private sealed class DecoratedController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } } \ No newline at end of file diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs index 0f59fffb..7c40b893 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ControllerApiVersionConventionBuilderTest.cs @@ -3,8 +3,8 @@ namespace Asp.Versioning.Conventions; #if NETFRAMEWORK +using System.Web.Http.Results; using ControllerBase = System.Web.Http.ApiController; -using IActionResult = System.Web.Http.IHttpActionResult; #else using Microsoft.AspNetCore.Mvc; #endif @@ -78,6 +78,7 @@ internal TestControllerApiVersionConventionBuilder( Type controllerType ) : base internal ActionApiVersionConventionBuilderCollection ProtectedActionBuilders => ActionBuilders; } +#pragma warning disable IDE0079 #pragma warning disable CA1812 #if !NETFRAMEWORK @@ -85,7 +86,7 @@ internal TestControllerApiVersionConventionBuilder( Type controllerType ) : base #endif private sealed class UndecoratedController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } #if !NETFRAMEWORK @@ -97,6 +98,6 @@ private sealed class UndecoratedController : ControllerBase [AdvertiseApiVersions( "3.0-Beta", Deprecated = true )] private sealed class DecoratedController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } } \ No newline at end of file diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ControllerConventionBuilderExtensionsTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ControllerConventionBuilderExtensionsTest.cs index c045c310..0d55d027 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ControllerConventionBuilderExtensionsTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ControllerConventionBuilderExtensionsTest.cs @@ -103,9 +103,9 @@ public void action_should_throw_exception_when_method_does_not_exist() #pragma warning disable IDE0060 #pragma warning disable IDE0079 +#pragma warning disable CA1822 #pragma warning disable CA1034 // Nested types should not be visible -#pragma warning disable CA1822 // Mark members as static -#pragma warning disable CA2234 // Pass system uri objects instead of strings + #if !NETFRAMEWORK [ApiController] #endif diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/DefaultControllerNameConventionTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/DefaultControllerNameConventionTest.cs index 2a4d0b8d..e261f4f9 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/DefaultControllerNameConventionTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/DefaultControllerNameConventionTest.cs @@ -35,14 +35,14 @@ public static IEnumerable<object[]> NormalizeNameData { get { - return new[] + return new object[][] { #if NETFRAMEWORK - new object[] { "ValuesController" }, - new object[] { "Values2Controller" }, + ["ValuesController"], + ["Values2Controller"], #else - new object[] { "Values" }, - new object[] { "Values2" }, + ["Values"], + ["Values2"], #endif }; } diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/GroupedControllerNameConventionTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/GroupedControllerNameConventionTest.cs index 36def81f..a56ad0c8 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/GroupedControllerNameConventionTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/GroupedControllerNameConventionTest.cs @@ -37,13 +37,13 @@ public static IEnumerable<object[]> NormalizeNameData { get { - return new[] + return new object[][] { - new object[] { "Values" }, + ["Values"], #if NETFRAMEWORK - new object[] { "ValuesController2" }, + ["ValuesController2"], #else - new object[] { "Values2" }, + ["Values2"], #endif }; } diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/OriginalControllerNameConventionTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/OriginalControllerNameConventionTest.cs index f6ef20c1..551c2149 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/OriginalControllerNameConventionTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/OriginalControllerNameConventionTest.cs @@ -35,12 +35,12 @@ public static IEnumerable<object[]> NormalizeNameData { get { - return new[] + return new object[][] { - new object[] { "Values" }, - new object[] { "Values2" }, + ["Values"], + ["Values2"], #if NETFRAMEWORK - new object[] { "ValuesController2" }, + ["ValuesController2"], #endif }; } diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderExtensionsTest.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderExtensionsTest.cs index f9c0d60b..6080579f 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderExtensionsTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderExtensionsTest.cs @@ -228,9 +228,10 @@ public void action_should_throw_exception_when_method_does_not_exist() actionConvention.Should().Throw<MissingMethodException>().And.Message.Should().Be( message ); } -#pragma warning disable IDE0060 // Remove unused parameter +#pragma warning disable IDE0060 +#pragma warning disable IDE0079 #pragma warning disable CA1034 // Nested types should not be visible -#pragma warning disable CA1822 // Mark members as static +#pragma warning disable CA1822 public sealed class StubController : ControllerBase { diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTTest.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTTest.cs index 6cb0b5af..5788c1a2 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTTest.cs @@ -239,6 +239,7 @@ public void action_should_call_action_on_controller_builder() controllerBuilder.Verify( cb => cb.Action( method ), Once() ); } +#pragma warning disable IDE0079 #pragma warning disable CA1034 // Nested types should not be visible public sealed class TestController : ControllerBase diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTest.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTest.cs index 69938e1f..80aa26ae 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderTest.cs @@ -239,6 +239,7 @@ public void action_should_call_action_on_controller_builder() controllerBuilder.Verify( cb => cb.Action( method ), Once() ); } +#pragma warning disable IDE0079 #pragma warning disable CA1034 // Nested types should not be visible public sealed class TestController : ControllerBase diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataQueryOptionsConventionBuilderTest.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataQueryOptionsConventionBuilderTest.cs index 6ca4f8a9..02e7ce5b 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataQueryOptionsConventionBuilderTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataQueryOptionsConventionBuilderTest.cs @@ -1,7 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. -namespace Asp.Versioning.Conventions; +//// Ignore Spelling: Dtime +namespace Asp.Versioning.Conventions; #if NETFRAMEWORK using Microsoft.AspNet.OData; @@ -112,6 +113,7 @@ private sealed class TestODataQueryOptionsConventionBuilder : ODataQueryOptionsC internal new IList<IODataQueryOptionsConvention> Conventions => base.Conventions; } +#pragma warning disable IDE0079 #pragma warning disable CA1034 // Nested types should not be visible public sealed class StubController : ODataController diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Company.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Company.cs index 531309ca..05f14b99 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Company.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Company.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA1002 // Do not expose generic lists #pragma warning disable CA2227 // Collection properties should be read only diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Contact.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Contact.cs index c341bf5c..c3f18d9d 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Contact.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Contact.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA1002 // Do not expose generic lists #pragma warning disable CA2227 // Collection properties should be read only 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 fa81ce4c..100af41f 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs @@ -383,7 +383,7 @@ public void substitute_should_get_attributes_from_property_that_has_attributes_t var property = substitutionType.GetRuntimeProperty( "Salary" ); var attributeWithParams = property.GetCustomAttribute<AllowedRolesAttribute>(); - attributeWithParams.AllowedRoles.Should().BeEquivalentTo( new[] { "Manager", "Employer" } ); + attributeWithParams.AllowedRoles.Should().BeEquivalentTo( ["Manager", "Employer"] ); } [Fact] diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Employer.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Employer.cs index b2391433..03cbd2a5 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Employer.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/Employer.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 #pragma warning disable CA2227 // Collection properties should be read only namespace Asp.Versioning.OData; diff --git a/src/Common/test/Common.OData.Tests/TestModelConfiguration.cs b/src/Common/test/Common.OData.Tests/TestModelConfiguration.cs index f176b7fc..b4e48c09 100644 --- a/src/Common/test/Common.OData.Tests/TestModelConfiguration.cs +++ b/src/Common/test/Common.OData.Tests/TestModelConfiguration.cs @@ -13,10 +13,14 @@ public class TestModelConfiguration : IModelConfiguration { public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix ) { - if ( builder == null ) +#if NETFRAMEWORK + if ( builder is null ) { throw new ArgumentNullException( nameof( builder ) ); } +#else + ArgumentNullException.ThrowIfNull( builder ); +#endif var tests = builder.EntitySet<TestEntity>( "Tests" ).EntityType; var neutralTests = builder.EntitySet<TestNeutralEntity>( "NeutralTests" ).EntityType; diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c2f1cdb2..d36d3be8 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,10 @@ <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> + <DefaultTargetFramework>net8.0</DefaultTargetFramework> + <DotNetReleaseBasePackageVersion>8.0</DotNetReleaseBasePackageVersion> + <DotNetReleasePackageVersion>$(DotNetReleaseBasePackageVersion).0</DotNetReleasePackageVersion> + <CurrentTfmFolder>.net</CurrentTfmFolder> <RootDir>$([MSBuild]::EnsureTrailingSlash($([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), .gitignore))))</RootDir> <BuildDir>$([MSBuild]::EnsureTrailingSlash($(RootDir)build))</BuildDir> <BackportDir>$([MSBuild]::EnsureTrailingSlash($([System.IO.Path]::Combine('$(RootDir)','src','Common','src','Common.Backport'))))</BackportDir> diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 15a2e2dc..53ff4535 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -21,6 +21,14 @@ <Import Project="$(BuildDir)test.targets" /> </ImportGroup> + <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> + <Using Include="System.Net.Http" /> + </ItemGroup> + + <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' "> + <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies.$(TargetFramework)" Version="1.0.3" PrivateAssets="All" /> + </ItemGroup> + <Target Name="GetTargetPath" Outputs="$(TargetPath)" /> <Target Name="GetTargetPaths" Outputs="@(TargetFiles)">