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/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 8616ce63..a3fc5fea 100644 --- a/build/test.targets +++ b/build/test.targets @@ -3,8 +3,13 @@ 6.8.0 - 4.18.3 - 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/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/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 dca602ca..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; @@ -92,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() ) @@ -105,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/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/AspNetCore/Directory.Build.props b/examples/AspNetCore/Directory.Build.props new file mode 100644 index 00000000..28e9057c --- /dev/null +++ b/examples/AspNetCore/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + + + 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/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/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/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/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 1fd344e5..4dd5e683 100644 --- a/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/ODataOpenApiExample/Program.cs @@ -80,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 => { @@ -102,6 +104,7 @@ options.SwaggerEndpoint( url, name ); } } ); +} app.UseHttpsRedirection(); app.UseAuthorization(); 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/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 7dc6c5c0..55e70b17 100644 --- a/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs +++ b/examples/AspNetCore/OData/SomeODataOpenApiExample/Program.cs @@ -64,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/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/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/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/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/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 acf998ed..909d8261 100644 --- a/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/MinimalOpenApiExample/Program.cs @@ -264,18 +264,20 @@ .Produces( 400 ); app.UseSwagger(); -app.UseSwaggerUI( - options => - { - var descriptions = app.DescribeApiVersions(); - - // build a swagger endpoint for each discovered API version - foreach ( var description in descriptions ) +if ( 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/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 64ad0ad1..454c60d7 100644 --- a/examples/AspNetCore/WebApi/OpenApiExample/Program.cs +++ b/examples/AspNetCore/WebApi/OpenApiExample/Program.cs @@ -57,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/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs index 195fa7ea..ca183af6 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/AdvertiseApiVersionsAttribute.cs @@ -1,5 +1,10 @@ // 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; 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 e7fe068d..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,7 +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 ) @@ -351,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 bbdff81b..90657500 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionAttribute.cs @@ -1,5 +1,10 @@ // 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; diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs index 9879d007..6ade14b6 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionFormatProvider.cs @@ -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 632500d0..00cccc5b 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelDebugView.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionModelDebugView.cs @@ -4,12 +4,9 @@ namespace Asp.Versioning; 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 f35292f9..97ec10bb 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionParser.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ApiVersionParser.cs @@ -187,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 @@ -338,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 04d6b034..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 cc4c27a3..375541ef 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionNeutral.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/IApiVersionNeutral.cs @@ -1,5 +1,8 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable IDE0079 +#pragma warning disable CA1040 + namespace Asp.Versioning; /// 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 eeec87a0..94088bc9 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/LinkHeaderValue.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/LinkHeaderValue.cs @@ -85,7 +85,7 @@ public StringSegment Language #endif if ( languages is null ) { - languages = new() { value }; + languages = [value]; } else if ( languages.Count == 0 ) { @@ -105,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. @@ -161,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 ) ) @@ -209,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; } @@ -302,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 ) @@ -404,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( '<' ); @@ -495,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 ); @@ -540,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() { @@ -572,11 +558,13 @@ public IEnumerator> GetEnumerator() } // REF: https://datatracker.ietf.org/doc/html/rfc8288#appendix-B.3 #9 +#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 var key = new StringSegment( remaining.Substring( start, end - start ).ToLowerInvariant() ); #endif +#pragma warning restore CA1308 // Normalize strings to uppercase start = end; ConsumeWhitespace(); diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs index 3d7050cc..e90f5c58 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/MapToApiVersionAttribute.cs @@ -1,5 +1,10 @@ // 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; diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs index d857297b..a4246749 100644 --- a/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs +++ b/src/Abstractions/src/Asp.Versioning.Abstractions/NamespaceParser.cs @@ -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/net7.0/ApiVersion.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/net#.0/ApiVersion.cs similarity index 73% 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 bd7c0682..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,6 +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/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 668e879c..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;net452;net472 + $(DefaultTargetFramework);net452;net472 Asp.Versioning - - - + + + - + 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 100% 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 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/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/ODataAcceptanceTest.cs b/src/AspNet/Acceptance/Asp.Versioning.WebApi.Acceptance.Tests/OData/ODataAcceptanceTest.cs index d69604f8..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; 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 index 2bcf1549..f6744216 100644 --- 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 @@ -64,7 +64,7 @@ private static IReadOnlyList FilterResults( continue; } - results ??= new(); + results ??= []; results.Add( apiDescription ); for ( var j = 0; j < conventions.Count; j++ ) @@ -73,7 +73,7 @@ private static IReadOnlyList FilterResults( } } - return results?.ToArray() ?? Array.Empty(); + return results?.ToArray() ?? []; } private static void ApplyAdHocEdm( 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 4c45eb70..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 @@ -78,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 ) { @@ -126,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() ) { @@ -168,10 +158,7 @@ protected override Collection ExploreRouteControllers( IHttpRoute route, ApiVersion apiVersion ) { - if ( controllerMappings == null ) - { - throw new ArgumentNullException( nameof( controllerMappings ) ); - } + ArgumentNullException.ThrowIfNull( controllerMappings ); Collection apiDescriptions; @@ -194,7 +181,7 @@ protected override Collection ExploreRouteControllers( return apiDescriptions; } - apiDescriptions = new(); + apiDescriptions = []; var modelSelector = Configuration.GetODataRootContainer( route ).GetRequiredService(); var edmModel = modelSelector.SelectModel( apiVersion ); @@ -260,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() 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 0cd36a0c..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 @@  - 7.0.0 - 7.0.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/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/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/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/Asp.Versioning.WebApi.OData.csproj b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData/Asp.Versioning.WebApi.OData.csproj index 87b75cd3..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 @@  - 7.0.0 - 7.0.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 f8f7e17b..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 ) { 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/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 b31d7496..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 ); 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 24d7a24e..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 @@ -17,15 +17,15 @@ namespace Asp.Versioning.Simulators.V1; [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/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 60292852..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" ) 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 a42188cc..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; 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 4544139c..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(); 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 5ded1914..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 @@  - 7.0.0 - 7.0.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/ApiVersionRequestProperties.cs b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApiVersionRequestProperties.cs index 284c66ba..0383fb9c 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApiVersionRequestProperties.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/ApiVersionRequestProperties.cs @@ -88,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 5da0add5..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 @@  - 7.0.0 - 7.0.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 0acd241f..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 ); @@ -304,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++ ) { @@ -335,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 ) { @@ -412,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 ) { @@ -485,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 dc05e257..caa7f0be 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DefaultApiVersionReporter.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/DefaultApiVersionReporter.cs @@ -10,11 +10,6 @@ namespace Asp.Versioning; /// public partial class DefaultApiVersionReporter { - private static DefaultApiVersionReporter? instance; - - internal static IReportApiVersions GetOrCreate( ISunsetPolicyManager sunsetPolicyManager ) => - instance ??= new( sunsetPolicyManager ); - 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 f8b86bcc..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.GetOrCreate( resolver.GetSunsetPolicyManager() ); - - 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 4afb0774..22fac02a 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/Dispatcher/HttpResponseExceptionFactory.cs @@ -36,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; 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 78f24891..413f20e0 100644 --- a/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReaderBuilder.cs +++ b/src/AspNet/WebApi/src/Asp.Versioning.WebApi/MediaTypeApiVersionReaderBuilder.cs @@ -22,23 +22,20 @@ public partial class MediaTypeApiVersionReaderBuilder /// The template syntax is the same used by route templates; however, constraints are not supported. public virtual MediaTypeApiVersionReaderBuilder Template( string template, string? parameterName = default ) { - 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 ) ); } } @@ -111,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/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/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 cd9ac2a7..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; @@ -73,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 ); @@ -122,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.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/Dispatcher/ApiVersionControllerSelectorTest.cs b/src/AspNet/WebApi/test/Asp.Versioning.WebApi.Tests/Dispatcher/ApiVersionControllerSelectorTest.cs index 25c3be0e..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; 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/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 4f14694a..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/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/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 versioned batch middleware/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given versioned batch middleware/when using a query string.cs index 6b63fd64..e85c1b9b 100644 --- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given versioned batch middleware/when using a query string.cs +++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given versioned batch middleware/when using a query string.cs @@ -1,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#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/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 560cd298..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,7 @@ // 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 4f48ea20..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 @@ -25,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 5d5655a9..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 @@ -22,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 b3fb9f01..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 @@ -25,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 c53de03f..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 @@ -22,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 940703e6..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; 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/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs index 06611d5b..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,7 +216,7 @@ 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.GetApiVersion(); @@ -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/ODataApiExplorerOptionsFactory.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs index ae057645..5e494688 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiExplorerOptionsFactory.cs @@ -69,8 +69,12 @@ public ODataApiExplorerOptionsFactory( } /// - protected override ODataApiExplorerOptions CreateInstance( string name ) => - new( new( CollateApiVersions( providers, Options ), 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, @@ -107,6 +111,6 @@ private static ODataApiVersionCollectionProvider CollateApiVersions( private sealed class ODataApiVersionCollectionProvider : IODataApiVersionCollectionProvider { - required public IReadOnlyList ApiVersions { get; set; } + 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 index 31824b08..47eac901 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs @@ -18,7 +18,7 @@ namespace Asp.Versioning.ApiExplorer; using Opts = Microsoft.Extensions.Options.Options; /// -/// Reprensents an API description provider for partial OData support. +/// Represents an API description provider for partial OData support. /// [CLSCompliant( false )] public class PartialODataDescriptionProvider : IApiDescriptionProvider @@ -87,9 +87,11 @@ protected ODataApiExplorerOptions Options /// public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context ) { + ArgumentNullException.ThrowIfNull( context ); + var results = FilterResults( context.Results, Conventions ); - if ( results.Count == 0 ) + if ( results.Length == 0 ) { return; } @@ -104,7 +106,7 @@ public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context odata.AddRouteComponents( model ); - for ( var j = 0; j < results.Count; j++ ) + for ( var j = 0; j < results.Length; j++ ) { var result = results[j]; var metadata = result.ActionDescriptor.GetApiVersionMetadata(); @@ -120,6 +122,8 @@ public virtual void OnProvidersExecuting( ApiDescriptionProviderContext context /// public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) { + ArgumentNullException.ThrowIfNull( context ); + var actions = context.Actions; for ( var i = 0; i < actions.Count; i++ ) @@ -141,20 +145,16 @@ private static int ODataOrder() => new ODataApiDescriptionProvider( new StubModelMetadataProvider(), new StubModelTypeBuilder(), - new OptionsFactory( - Enumerable.Empty>(), - Enumerable.Empty>() ), + new OptionsFactory( [], [] ), Opts.Create( new ODataApiExplorerOptions( - new( - new StubODataApiVersionCollectionProvider(), - Enumerable.Empty() ) ) ) ).Order; + new( new StubODataApiVersionCollectionProvider(), [] ) ) ) ).Order; [MethodImpl( MethodImplOptions.AggressiveInlining )] private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) => model.SetAnnotationValue( model, AdHocAnnotation.Instance ); - private static IReadOnlyList FilterResults( + private static ApiDescription[] FilterResults( IList results, IReadOnlyList conventions ) { @@ -189,7 +189,7 @@ private static IReadOnlyList FilterResults( } } - return filtered?.ToArray() ?? Array.Empty(); + return filtered?.ToArray() ?? []; } private sealed class StubModelMetadataProvider : IModelMetadataProvider 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 index 3800afd6..6ca75048 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -2,10 +2,7 @@ namespace Asp.Versioning.Conventions; -using Asp.Versioning; -using Asp.Versioning.OData; using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.OData.ModelBuilder; /// /// Provides additional implementation specific to ASP.NET Core. @@ -16,10 +13,7 @@ public partial class ImplicitModelBoundSettingsConvention /// public void ApplyTo( ApiDescription apiDescription ) { - if ( apiDescription == null ) - { - throw new ArgumentNullException( nameof( apiDescription ) ); - } + ArgumentNullException.ThrowIfNull( apiDescription ); var responses = apiDescription.SupportedResponseTypes; 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 6986e816..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,13 +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 ); + 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 d19c8ed5..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 @@ -24,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; } @@ -41,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; @@ -65,14 +57,12 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) 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 79bba8dd..8d90b695 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IApiVersioningBuilderExtensions.cs @@ -31,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; } @@ -49,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 ); @@ -109,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 ); } @@ -162,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 index cb86af84..16432665 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs @@ -61,10 +61,7 @@ internal static bool ConfigureDefaultFeatureProviders( this ApplicationPartManag /// The extended . public static void AddModelConfigurationsAsServices( this IServiceCollection services ) { - if ( services == null ) - { - throw new ArgumentNullException( nameof( services ) ); - } + ArgumentNullException.ThrowIfNull( services ); var partManager = services.GetOrCreateApplicationPartManager(); 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 7fb591e2..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,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/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 8f29de7b..93d92b9c 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/VersionedODataTemplateTranslator.cs @@ -17,15 +17,8 @@ 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(); 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 b1e7c390..a4b7bfd3 100644 --- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/DefaultMetadataMatcherPolicy.cs +++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Routing/DefaultMetadataMatcherPolicy.cs @@ -36,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; } @@ -51,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++ ) { @@ -70,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 ); @@ -89,7 +79,7 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints continue; } - edges ??= new(); + edges ??= []; edges.Add( endpoint ); var model = endpoint.Metadata.GetMetadata()!.Map( Explicit | Implicit ); @@ -131,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" ); 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 8ea4c5a2..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 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 511513b4..532abb00 100644 --- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs +++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs @@ -256,9 +256,9 @@ private static void AssertQueryOptionWithoutOData( ApiDescription description, s parameter.ModelMetadata.Description.Should().EndWith( suffix + '.' ); } - private void PrintGroup( IReadOnlyList items ) + private void PrintGroup( ApiDescription[] items ) { - for ( var i = 0; i < items.Count; i++ ) + for ( var i = 0; i < items.Length; i++ ) { var item = items[i]; console.WriteLine( $"[{item.GroupName}] {item.HttpMethod} {item.RelativePath}" ); diff --git a/src/AspNetCore/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 36ed3198..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", @@ -637,6 +687,7 @@ 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 { @@ -677,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] @@ -692,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 fcbda180..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 @@ -13,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 6542f84f..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 @@ -14,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 dd2f04ce..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 @@ -14,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 b1741080..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 @@ -14,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 3b9d0f45..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 @@ -14,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/Supplier.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/Simulators/Models/Supplier.cs index 19470142..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 @@ -8,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/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 index 0d728c19..85427ba8 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationCollection.cs @@ -17,8 +17,8 @@ public class ApiVersionMetadataCollationCollection : IList, /// public ApiVersionMetadataCollationCollection() { - items = new(); - groups = new(); + items = []; + groups = []; } /// @@ -47,7 +47,11 @@ ApiVersionMetadata IList.this[int index] /// 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 ); diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs index dc1dfa25..313c2f26 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/ApiVersionMetadataCollationContext.cs @@ -11,5 +11,5 @@ public class ApiVersionMetadataCollationContext /// Gets the read-only list of collation results. /// /// The read-only list of collation results. - public ApiVersionMetadataCollationCollection Results { get; } = new(); + 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 index 3ad5433a..e5ce20fb 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs @@ -12,15 +12,29 @@ namespace Asp.Versioning.ApiExplorer; public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider { private readonly EndpointDataSource endpointDataSource; + private readonly IEndpointInspector endpointInspector; private int version; /// /// Initializes a new instance of the class. /// /// The underlying endpoint data source. + [Obsolete( "Use the constructor that accepts IEndpointInspector. This constructor will be removed in a future version." )] public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource ) + : this( endpointDataSource, new DefaultEndpointInspector() ) { } + + /// + /// Initializes a new instance of the class. + /// + /// The underlying endpoint data source. + /// The endpoint inspector used to inspect endpoints. + public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource, IEndpointInspector endpointInspector ) { - this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) ); + ArgumentNullException.ThrowIfNull( endpointDataSource ); + ArgumentNullException.ThrowIfNull( endpointInspector ); + + this.endpointDataSource = endpointDataSource; + this.endpointInspector = endpointInspector; ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version ); } @@ -30,10 +44,7 @@ public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointD /// public void Execute( ApiVersionMetadataCollationContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var endpoints = endpointDataSource.Endpoints; @@ -41,7 +52,8 @@ public void Execute( ApiVersionMetadataCollationContext context ) { var endpoint = endpoints[i]; - if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item ) + if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item || + endpointInspector.IsControllerAction( endpoint ) ) { continue; } diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/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/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 8dd49c7d..8437429a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiVersioningFeature.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiVersioningFeature.cs @@ -34,11 +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,12 +54,16 @@ public string? RawRequestedApiVersion { 0 => default, 1 => values[0], +#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]; } } @@ -105,7 +105,7 @@ public ApiVersion? RequestedApiVersion if ( apiVersion is not null && ( rawApiVersions is null || rawApiVersions.Count == 0 ) ) { - rawApiVersions = new[] { apiVersion.ToString() }; + rawApiVersions = [apiVersion.ToString()]; } } } @@ -115,7 +115,7 @@ private static AmbiguousApiVersionException NewAmbiguousApiVersionException( IRe new( string.Format( CultureInfo.CurrentCulture, - CommonSR.MultipleDifferentApiVersionsRequested, - string.Join( ", ", values.ToArray(), 0, values.Count ) ), + 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/EndpointBuilderFinalizer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/EndpointBuilderFinalizer.cs index baf0fd12..250083cd 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/EndpointBuilderFinalizer.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/EndpointBuilderFinalizer.cs @@ -163,11 +163,11 @@ private static bool TryGetApiVersions( IList metadata, out ApiVersionBuc 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(), + None => supported ??= [], + Mapped => mapped ??= [], + Deprecated => deprecated ??= [], + Advertised => advertised ??= [], + Advertised | Deprecated => deprecatedAdvertised ??= [], _ => default, }; @@ -183,11 +183,11 @@ private static bool TryGetApiVersions( IList metadata, out ApiVersionBuc } buckets = new( - mapped?.ToArray() ?? Array.Empty(), - supported?.ToArray() ?? Array.Empty(), - deprecated?.ToArray() ?? Array.Empty(), - advertised?.ToArray() ?? Array.Empty(), - deprecatedAdvertised?.ToArray() ?? Array.Empty() ); + mapped?.ToArray() ?? [], + supported?.ToArray() ?? [], + deprecated?.ToArray() ?? [], + advertised?.ToArray() ?? [], + deprecatedAdvertised?.ToArray() ?? [] ); return true; } @@ -212,14 +212,8 @@ private static ApiVersionMetadata Build( IList metadata, ApiVersionSet v 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 ) + + if ( buckets.AreEmpty ) { var noInheritedApiVersions = inheritedSupported.Count == 0 && inheritedDeprecated.Count == 0; @@ -230,7 +224,7 @@ private static ApiVersionMetadata Build( IList metadata, ApiVersionSet v } else { - emptyVersions = Array.Empty(); + emptyVersions = []; endpointModel = new( declaredVersions: emptyVersions, inheritedSupported, @@ -239,24 +233,29 @@ private static ApiVersionMetadata Build( IList metadata, ApiVersionSet v 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 ); + 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 ); @@ -268,7 +267,7 @@ private static RequestDelegate EnsureRequestDelegate( RequestDelegate? current, throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - SR.UnsetRequestDelegate, + Format.UnsetRequestDelegate, nameof( RequestDelegate ), nameof( RouteEndpoint ) ) ); @@ -277,5 +276,12 @@ private record struct ApiVersionBuckets( IReadOnlyList Supported, IReadOnlyList Deprecated, IReadOnlyList Advertised, - IReadOnlyList AdvertisedDeprecated ); + 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/IEndpointConventionBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointConventionBuilderExtensions.cs index 97e53f42..15525d66 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointConventionBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointConventionBuilderExtensions.cs @@ -7,6 +7,7 @@ namespace Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using System.Collections; using System.Globalization; +using System.Runtime.Serialization; using static Asp.Versioning.ApiVersionProviderOptions; /// @@ -27,10 +28,7 @@ public static TBuilder WithApiVersionSet( ApiVersionSet apiVersionSet ) where TBuilder : notnull, IEndpointConventionBuilder { - if ( apiVersionSet == null ) - { - throw new ArgumentNullException( nameof( apiVersionSet ) ); - } + ArgumentNullException.ThrowIfNull( apiVersionSet ); builder.Add( endpoint => AddMetadata( endpoint, apiVersionSet ) ); builder.Finally( EndpointBuilderFinalizer.FinalizeEndpoints ); @@ -442,7 +440,7 @@ private static void AddMetadata( EndpointBuilder builder, object item ) throw new InvalidOperationException( string.Format( CultureInfo.CurrentCulture, - SR.NoVersionSet, + Format.NoVersionSet, builder.DisplayName, nameof( IEndpointRouteBuilderExtensions.NewVersionedApi ), nameof( IEndpointRouteBuilderExtensions.WithApiVersionSet ) ) ); @@ -478,7 +476,11 @@ private sealed class SingleItemReadOnlyList : IReadOnlyList internal SingleItemReadOnlyList( ApiVersion item ) => this.item = item; +#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; 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 2aad4196..e6054e34 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointRouteBuilderExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/IEndpointRouteBuilderExtensions.cs @@ -23,14 +23,9 @@ public static class IEndpointRouteBuilderExtensions /// A new API version set builder. public static ApiVersionSetBuilder NewApiVersionSet( this IEndpointRouteBuilder endpoints, string? name = default ) { - if ( endpoints == null ) - { - throw new ArgumentNullException( nameof( endpoints ) ); - } - + ArgumentNullException.ThrowIfNull( endpoints ); var create = endpoints.ServiceProvider.GetService(); - - return create is null ? new ApiVersionSetBuilder( name ) : create( name ); + return create is null ? new( name ) : create( name ); } /// @@ -43,10 +38,7 @@ public static class IEndpointRouteBuilderExtensions public static IVersionedEndpointRouteBuilder WithApiVersionSet( this TBuilder builder, string? name = default ) where TBuilder : notnull, IEndpointRouteBuilder, IEndpointConventionBuilder { - if ( builder is null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } + ArgumentNullException.ThrowIfNull( builder ); if ( builder.HasMetadata() ) { @@ -71,10 +63,7 @@ public static class IEndpointRouteBuilderExtensions /// A new instance. public static IVersionedEndpointRouteBuilder NewVersionedApi( this IEndpointRouteBuilder builder, string? name = default ) { - if ( builder is null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } + ArgumentNullException.ThrowIfNull( builder ); if ( builder.IsNestedGroup() ) { 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 c1773a6c..36af4a92 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Builder/VersionedEndpointRouteBuilder.cs @@ -59,20 +59,12 @@ public virtual IApplicationBuilder CreateApplicationBuilder() => 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 ) ) @@ -89,19 +81,10 @@ internal ServiceProviderDecorator( } } - private sealed class EndpointDataSourceDecorator : EndpointDataSource - { - private readonly EndpointDataSource decorated; - private readonly ApiVersionSetBuilder versionSetBuilder; - - internal EndpointDataSourceDecorator( + private sealed class EndpointDataSourceDecorator( EndpointDataSource decorated, - ApiVersionSetBuilder versionSetBuilder ) - { - this.decorated = decorated; - this.versionSetBuilder = versionSetBuilder; - } - + ApiVersionSetBuilder versionSetBuilder ) : EndpointDataSource + { public override IReadOnlyList Endpoints => decorated.Endpoints; public override IChangeToken GetChangeToken() => decorated.GetChangeToken(); @@ -175,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; 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 3419a436..89d6d51c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs @@ -6,11 +6,12 @@ namespace Microsoft.Extensions.DependencyInjection; 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.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,8 +214,8 @@ LinkGenerator NewFactory( IServiceProvider serviceProvider ) } } - // TODO: Remove in .NET 8.0 - // REF: https://github.com/dotnet/aspnetcore/issues/45051 + // 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 ); @@ -182,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 22eeef58..92bcbd12 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReaderBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/MediaTypeApiVersionReaderBuilder.cs @@ -22,18 +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 ) ); } @@ -45,7 +46,7 @@ public partial class MediaTypeApiVersionReaderBuilder return this; } - private static IReadOnlyList ReadMediaTypePattern( + private static string[] ReadMediaTypePattern( IReadOnlyList mediaTypes, TemplateMatcher matcher, string? parameterName ) @@ -102,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 1ba4358e..e43223aa 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs @@ -10,6 +10,7 @@ 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; @@ -40,10 +41,15 @@ public ApiVersionMatcherPolicy( IOptions options, ILogger logger ) { - this.apiVersionParser = apiVersionParser ?? throw new ArgumentNullException( nameof( apiVersionParser ) ); - collator = new( providers ?? throw new ArgumentNullException( nameof( providers ) ), options ); - 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; } /// @@ -58,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++ ) { @@ -75,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; } @@ -103,17 +99,12 @@ public Task ApplyAsync( HttpContext httpContext, CandidateSet candidates ) 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 ); var rejection = new RouteDestination( exitDestination ); var capacity = edges.Count - EdgeBuilder.NumberOfRejectionEndpoints; @@ -159,7 +150,7 @@ public PolicyJumpTable BuildJumpTable( int exitDestination, IReadOnlyList(), + routePatterns ?? [], apiVersionParser, source, Options ); @@ -178,10 +169,7 @@ 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 ); @@ -203,7 +191,7 @@ public IReadOnlyList GetEdges( IReadOnlyList endpoints if ( model.IsApiVersionNeutral ) { builder.Add( endpoint, ApiVersion.Neutral, metadata ); - neutralEndpoints ??= new(); + neutralEndpoints ??= []; neutralEndpoints.Add( (endpoint, metadata) ); } else @@ -320,7 +308,7 @@ private static void Collate( if ( versions.Count > 0 ) { - supported ??= new(); + supported ??= []; for ( var j = 0; j < versions.Count; j++ ) { @@ -335,7 +323,7 @@ private static void Collate( return; } - deprecated ??= new(); + deprecated ??= []; for ( var j = 0; j < versions.Count; j++ ) { @@ -464,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 ); @@ -484,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 ) { @@ -510,22 +494,15 @@ internal int CompareTo( in Match other ) } } - private sealed class ApiVersionCollator + private sealed class ApiVersionCollator( + IEnumerable providers, + IOptions options ) { - private readonly IApiVersionMetadataCollationProvider[] providers; - private readonly IOptions options; + private readonly IApiVersionMetadataCollationProvider[] providers = providers.ToArray(); private readonly object syncRoot = new(); private IReadOnlyList? items; private int version; - internal ApiVersionCollator( - IEnumerable providers, - IOptions options ) - { - this.providers = providers.ToArray(); - this.options = options; - } - public IReadOnlyList Items { get 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 a7eb03da..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,6 +6,7 @@ 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 @@ -14,17 +15,17 @@ internal sealed class ApiVersionPolicyJumpTable : PolicyJumpTable private readonly bool versionsByUrlOnly; private readonly bool versionsByMediaTypeOnly; private readonly RouteDestination rejection; - private readonly IReadOnlyDictionary destinations; + private readonly FrozenDictionary destinations; private readonly ApiVersionPolicyFeature? policyFeature; - private readonly IReadOnlyList routePatterns; + private readonly RoutePattern[] routePatterns; private readonly IApiVersionParser parser; private readonly ApiVersioningOptions options; internal ApiVersionPolicyJumpTable( RouteDestination rejection, - IReadOnlyDictionary destinations, + FrozenDictionary destinations, ApiVersionPolicyFeature? policyFeature, - IReadOnlyList routePatterns, + RoutePattern[] routePatterns, IApiVersionParser parser, IApiVersionParameterSource source, ApiVersioningOptions options ) @@ -35,7 +36,7 @@ internal ApiVersionPolicyJumpTable( 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 ); } 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 8077e8f9..cad20584 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs @@ -57,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 010210f7..8fb60798 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs @@ -86,7 +86,7 @@ private void Add( ref EdgeKey key, RouteEndpoint endpoint ) 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/EndpointProblem.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs index bb64582f..9a8b539b 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs @@ -52,11 +52,11 @@ internal static Task UnsupportedApiVersion( { 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; 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/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/SR.Designer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs index afa465b1..4a56d6b9 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.Designer.cs @@ -96,6 +96,15 @@ 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.. /// diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx index b3d9185d..dde23ba1 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/SR.resx @@ -129,6 +129,14 @@ 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. 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 b2d866c9..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 @@ -55,14 +55,25 @@ public ApiExplorerOptionsFactory( /// protected override T CreateInstance( string name ) { - var apiVersioningOptions = Options; var options = base.CreateInstance( name ); + CopyOptions( Options, options ); + return options; + } - options.AssumeDefaultVersionWhenUnspecified = apiVersioningOptions.AssumeDefaultVersionWhenUnspecified; - options.ApiVersionParameterSource = apiVersioningOptions.ApiVersionReader; - options.DefaultApiVersion = apiVersioningOptions.DefaultApiVersion; - options.RouteConstraintName = apiVersioningOptions.RouteConstraintName; + /// + /// Copies the following source options to the target options. + /// + /// The source options. + /// The target options. + protected static void CopyOptions( ApiVersioningOptions sourceOptions, T targetOptions ) + { + ArgumentNullException.ThrowIfNull( targetOptions, nameof( targetOptions ) ); + ArgumentNullException.ThrowIfNull( sourceOptions, nameof( sourceOptions ) ); - 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 4643fe39..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,5 +1,7 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#pragma warning disable CA1812 // Avoid uninstantiated internal classes + namespace Microsoft.AspNetCore.Builder; using Asp.Versioning; @@ -11,18 +13,18 @@ internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescript { private readonly ISunsetPolicyManager sunsetPolicyManager; private readonly IApiVersionMetadataCollationProvider[] providers; + private readonly IEndpointInspector endpointInspector; private readonly IOptions options; - private readonly Func, ISunsetPolicyManager, IOptions, IApiVersionDescriptionProvider> activator; public ApiVersionDescriptionProviderFactory( - Func, ISunsetPolicyManager, IOptions, IApiVersionDescriptionProvider> activator, ISunsetPolicyManager sunsetPolicyManager, IEnumerable providers, + IEndpointInspector endpointInspector, IOptions options ) { - this.activator = activator; this.sunsetPolicyManager = sunsetPolicyManager; this.providers = providers.ToArray(); + this.endpointInspector = endpointInspector; this.options = options; } @@ -30,11 +32,11 @@ public IApiVersionDescriptionProvider Create( EndpointDataSource endpointDataSou { var collators = new List( capacity: providers.Length + 1 ) { - new EndpointApiVersionMetadataCollationProvider( endpointDataSource ), + new EndpointApiVersionMetadataCollationProvider( endpointDataSource, endpointInspector ), }; collators.AddRange( providers ); - return activator( collators, sunsetPolicyManager, options ); + return new DefaultApiVersionDescriptionProvider( collators, sunsetPolicyManager, options ); } } \ No newline at end of file diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/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 8730244c..22151e11 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs @@ -2,9 +2,8 @@ namespace Asp.Versioning.ApiExplorer; +using Asp.Versioning.ApiExplorer.Internal; using Microsoft.Extensions.Options; -using static Asp.Versioning.ApiVersionMapping; -using static System.Globalization.CultureInfo; /// /// Represents the default implementation of an object that discovers and describes the API version information within an application. @@ -12,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer; [CLSCompliant( false )] public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider { - private readonly ApiVersionDescriptionCollection collection; + private readonly ApiVersionDescriptionCollection collection; private readonly IOptions options; /// @@ -28,7 +27,7 @@ public DefaultApiVersionDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); + collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -56,144 +55,55 @@ 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(); - } - - private void BucketizeApiVersions( IReadOnlyList metadata, ISet supported, ISet deprecated ) - { - 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 ); - } - - versions = model.DeprecatedApiVersions; - - 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 ) ); - } + return Array.Empty(); } - private sealed class ApiVersionDescriptionCollection + private sealed class GroupedApiVersionMetadata : + ApiVersionMetadata, + IEquatable, + IGroupedApiVersionMetadata, + IGroupedApiVersionMetadataFactory { - private readonly object syncRoot = new(); - private readonly DefaultApiVersionDescriptionProvider provider; - private readonly IApiVersionMetadataCollationProvider[] collators; - private IReadOnlyList? items; - private int version; - - public ApiVersionDescriptionCollection( - DefaultApiVersionDescriptionProvider provider, - IEnumerable collators ) - { - this.provider = provider; - this.collators = collators.ToArray(); - } + private GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata ) + : base( metadata ) => GroupName = groupName; - 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(); + public string? GroupName { get; } - for ( var i = 0; i < collators.Length; i++ ) - { - collators[i].Execute( context ); - } + static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New( + string? groupName, + ApiVersionMetadata metadata ) => new( groupName, metadata ); - items = provider.Describe( context.Results ); - version = currentVersion; - } + public bool Equals( GroupedApiVersionMetadata? other ) => + other is not null && other.GetHashCode() == GetHashCode(); - return items; - } - } + public override bool Equals( object? obj ) => + obj is not null && + GetType().Equals( obj.GetType() ) && + GetHashCode() == obj.GetHashCode(); - private int ComputeVersion() => - collators.Length switch - { - 0 => 0, - 1 => collators[0].Version, - _ => ComputeVersion( collators ), - }; - - private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers ) + public override int GetHashCode() { var hash = default( HashCode ); - for ( var i = 0; i < providers.Length; i++ ) + if ( !string.IsNullOrEmpty( GroupName ) ) { - hash.Add( providers[i].Version ); + hash.Add( GroupName, StringComparer.Ordinal ); } + hash.Add( base.GetHashCode() ); + return hash.ToHashCode(); } } diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs index f806121d..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 @@ -25,11 +25,7 @@ public static class IApiVersioningBuilderExtensions /// The original . public static IApiVersioningBuilder AddApiExplorer( this IApiVersioningBuilder builder ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); AddApiExplorerServices( builder ); return builder; } @@ -42,14 +38,9 @@ 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 ) ); - } - + ArgumentNullException.ThrowIfNull( builder ); AddApiExplorerServices( builder ); builder.Services.Configure( setupAction ); - return builder; } @@ -61,58 +52,17 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) services.AddMvcCore().AddApiExplorer(); services.TryAddSingleton, ApiExplorerOptionsFactory>(); - services.TryAddTransient( ResolveApiVersionDescriptionProviderFactory ); - services.TryAddSingleton( ResolveApiVersionDescriptionProvider ); + services.TryAddTransient(); + services.TryAddSingleton( static sp => sp.GetRequiredService().Create() ); // use internal constructor until ASP.NET Core fixes their bug // BUG: https://github.com/dotnet/aspnetcore/issues/41773 services.TryAddEnumerable( Transient( - sp => new( + static sp => new( sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService(), sp.GetRequiredService>() ) ) ); } - - private static IApiVersionDescriptionProviderFactory ResolveApiVersionDescriptionProviderFactory( IServiceProvider serviceProvider ) - { - var sunsetPolicyManager = serviceProvider.GetRequiredService(); - var providers = serviceProvider.GetServices(); - var options = serviceProvider.GetRequiredService>(); - var mightUseCustomGroups = options.Value.FormatGroupName is not null; - - return new ApiVersionDescriptionProviderFactory( - mightUseCustomGroups ? NewGroupedProvider : NewDefaultProvider, - sunsetPolicyManager, - providers, - options ); - - static IApiVersionDescriptionProvider NewDefaultProvider( - IEnumerable providers, - ISunsetPolicyManager sunsetPolicyManager, - IOptions apiExplorerOptions ) => - new DefaultApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions ); - - static IApiVersionDescriptionProvider NewGroupedProvider( - IEnumerable providers, - ISunsetPolicyManager sunsetPolicyManager, - IOptions apiExplorerOptions ) => - new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, apiExplorerOptions ); - } - - private static IApiVersionDescriptionProvider ResolveApiVersionDescriptionProvider( IServiceProvider serviceProvider ) - { - var providers = serviceProvider.GetServices(); - var sunsetPolicyManager = serviceProvider.GetRequiredService(); - var options = serviceProvider.GetRequiredService>(); - var mightUseCustomGroups = options.Value.FormatGroupName is not null; - - if ( mightUseCustomGroups ) - { - return new GroupedApiVersionDescriptionProvider( providers, sunsetPolicyManager, options ); - } - - return new DefaultApiVersionDescriptionProvider( providers, 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 716c321c..294db52c 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs @@ -2,10 +2,8 @@ namespace Asp.Versioning.ApiExplorer; +using Asp.Versioning.ApiExplorer.Internal; using Microsoft.Extensions.Options; -using System.Buffers; -using static Asp.Versioning.ApiVersionMapping; -using static System.Globalization.CultureInfo; /// /// Represents the default implementation of an object that discovers and describes the API version information within an application. @@ -13,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer; [CLSCompliant( false )] public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvider { - private readonly ApiVersionDescriptionCollection collection; + private readonly ApiVersionDescriptionCollection collection; private readonly IOptions options; /// @@ -29,7 +27,7 @@ public GroupedApiVersionDescriptionProvider( ISunsetPolicyManager sunsetPolicyManager, IOptions apiExplorerOptions ) { - collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); + collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) ); SunsetPolicyManager = sunsetPolicyManager; options = apiExplorerOptions; } @@ -58,201 +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 provider; - private readonly IApiVersionMetadataCollationProvider[] collators; - private IReadOnlyList? items; - private int version; - - public ApiVersionDescriptionCollection( - GroupedApiVersionDescriptionProvider provider, - IEnumerable collators ) - { - this.provider = provider; - this.collators = collators.ToArray(); - } - - public IReadOnlyList Items - { - get - { - if ( items is not null && version == ComputeVersion() ) - { - return items; - } - - lock ( syncRoot ) - { - var currentVersion = ComputeVersion(); - - if ( items is not null && version == currentVersion ) - { - return items; - } - - var context = new ApiVersionMetadataCollationContext(); - - for ( var i = 0; i < collators.Length; i++ ) - { - collators[i].Execute( context ); - } - - var results = context.Results; - var metadata = new GroupedApiVersionMetadata[results.Count]; - - for ( var i = 0; i < metadata.Length; i++ ) - { - metadata[i] = new( context.Results.GroupName( i ), results[i] ); - } - - items = provider.Describe( metadata ); - version = currentVersion; - } - - return items; - } - } - - private int ComputeVersion() => - collators.Length switch - { - 0 => 0, - 1 => collators[0].Version, - _ => ComputeVersion( collators ), - }; - - private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers ) - { - var hash = default( HashCode ); - - for ( var i = 0; i < providers.Length; i++ ) - { - hash.Add( providers[i].Version ); - } - - return hash.ToHashCode(); - } - } - - private sealed class ApiVersionDescriptionComparer : IComparer - { - public int Compare( ApiVersionDescription? x, ApiVersionDescription? y ) - { - if ( x is null ) - { - return y is null ? 0 : -1; - } - - if ( y is null ) - { - return 1; - } - - var result = x.ApiVersion.CompareTo( y.ApiVersion ); - - if ( result == 0 ) - { - result = StringComparer.Ordinal.Compare( x.GroupName, y.GroupName ); - } - - return result; - } + 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. @@ -268,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(); @@ -293,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 c84f9d06..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; @@ -125,27 +128,29 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context ) var groupResults = new List( capacity: results.Count ); 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++ ) { - if ( unversioned != null && unversioned.ContainsKey( i ) ) + if ( unversioned != null && unversioned.ContainsKey( j ) ) { continue; } - var result = results[i]; + var result = results[j]; var action = result.ActionDescriptor; if ( !ShouldExploreAction( action, version ) ) { if ( IsUnversioned( action ) ) { - unversioned ??= new(); - unversioned.Add( i, result ); + unversioned ??= []; + unversioned.Add( j, result ); } continue; @@ -172,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 ) @@ -245,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 ); @@ -257,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++ ) @@ -268,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 index a4527467..6614986a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiExplorer/ActionApiVersionMetadataCollationProvider.cs @@ -29,10 +29,7 @@ public ActionApiVersionMetadataCollationProvider( IActionDescriptorCollectionPro /// public void Execute( ApiVersionMetadataCollationContext context ) { - if ( context == null ) - { - throw new ArgumentNullException( nameof( context ) ); - } + ArgumentNullException.ThrowIfNull( context ); var actions = provider.ActionDescriptors.Items; 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 bbbbcd71..e621198a 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApiVersionCollator.cs @@ -32,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 ) ) { @@ -82,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 ) { @@ -120,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 438a76f0..69206a57 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/ApplicationModels/ApiBehaviorSpecification.cs @@ -15,10 +15,7 @@ public sealed class ApiBehaviorSpecification : IApiControllerSpecification /// public bool IsSatisfiedBy( ControllerModel controller ) { - if ( controller == null ) - { - throw new ArgumentNullException( nameof( controller ) ); - } + ArgumentNullException.ThrowIfNull( controller ); // REF: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/ApplicationModels/ApiBehaviorApplicationModelProvider.cs if ( controller.Attributes.OfType().Any() ) 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 ee74e9ff..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,5 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +// Ignore Spelling: Mvc namespace Microsoft.Extensions.DependencyInjection; using Asp.Versioning; @@ -13,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 . @@ -27,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; } @@ -45,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; @@ -63,9 +57,9 @@ private static void AddServices( IServiceCollection services ) services.AddMvcCore(); services.TryAddSingleton, MvcApiVersioningOptionsFactory>(); services.TryAddSingleton(); - services.TryAddSingleton( sp => new ApiVersionConventionBuilder( sp.GetRequiredService() ) ); + services.TryAddSingleton( static sp => new ApiVersionConventionBuilder( sp.GetRequiredService() ) ); services.TryAddSingleton(); - services.TryAddSingleton( sp => new ReportApiVersionsAttribute( sp.GetRequiredService() ) ); + services.TryAddSingleton( static sp => new ReportApiVersionsAttribute( sp.GetRequiredService() ) ); services.AddSingleton(); services.TryAddEnumerable( Transient, ApiVersioningMvcOptionsSetup>() ); services.TryAddEnumerable( Transient() ); @@ -73,6 +67,7 @@ private static void AddServices( IServiceCollection services ) services.TryAddEnumerable( Transient() ); services.TryAddEnumerable( Singleton() ); services.Replace( WithUrlHelperFactoryDecorator( services ) ); + services.TryReplace(); } private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor ) @@ -90,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 ); @@ -116,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 420692b4..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,16 +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 // 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 // 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/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 17a20ce7..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 @@ -97,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 ) ); @@ -125,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 @@ -192,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 @@ -210,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 ); 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/GroupedApiVersionDescriptionProviderTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/GroupedApiVersionDescriptionProviderTest.cs index f29b9139..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 @@ -41,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 ) 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 0ccce57b..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,9 +7,9 @@ namespace Asp.Versioning.ApiExplorer; internal sealed class TestActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider { - private readonly Lazy collection = new( CreateActionDescriptors ); + private readonly Lazy collection; - public TestActionDescriptorCollectionProvider() { } + public TestActionDescriptorCollectionProvider() => collection = new( CreateActionDescriptors ); public TestActionDescriptorCollectionProvider( ActionDescriptor action, params ActionDescriptor[] otherActions ) { @@ -17,11 +17,11 @@ public TestActionDescriptorCollectionProvider( ActionDescriptor action, params A if ( otherActions.Length == 0 ) { - actions = new ActionDescriptor[] { action }; + actions = [action]; } else { - actions = new ActionDescriptor[otherActions.Length]; + actions = new ActionDescriptor[otherActions.Length + 1]; actions[0] = action; Array.Copy( otherActions, 0, actions, 1, otherActions.Length ); } @@ -41,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( @@ -106,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 45a50e05..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 @@ -149,6 +149,97 @@ public void versioned_api_explorer_should_use_custom_group_name() 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 a79476ef..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; @@ -66,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(); 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 1fdd81e7..6f940aee 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs @@ -1,15 +1,24 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. +#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; /// @@ -23,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( ',' ); + + for ( var j = 0; j < items.Length; j++ ) + { + var item = items[j].Trim(); - if ( !iterator.MoveNext() ) + 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 + } + + 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 ); - if ( parser.TryParse( iterator.Current, out var value ) ) + 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 f06248aa..32dc0c69 100644 --- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs +++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionHeaderEnumerable.cs @@ -23,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 100% 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 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 eebe73d9..d9d2794a 100644 --- a/src/Common/src/Common.Backport/HashCode.cs +++ b/src/Common/src/Common.Backport/HashCode.cs @@ -1,7 +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 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 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 f7994129..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,7 +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 765ca431..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,7 +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 2093b4c5..142ad4e4 100644 --- a/src/Common/src/Common.Mvc/ReportApiVersionsAttribute.cs +++ b/src/Common/src/Common.Mvc/ReportApiVersionsAttribute.cs @@ -23,6 +23,11 @@ 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> diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/DefaultODataQueryOptionDescriptionProvider.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/DefaultODataQueryOptionDescriptionProvider.cs index d8ec26bd..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,11 +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, + 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 // Normalize strings to uppercase +#pragma warning restore IDE0079 nameof( queryOption ) ), }; } @@ -64,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(); @@ -76,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 ); @@ -86,7 +92,7 @@ protected virtual string DescribeFilter( ODataQueryOptionDescriptionContext cont description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedFilterProperties ) ); } @@ -100,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; @@ -118,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 ) @@ -126,7 +129,7 @@ protected virtual string DescribeExpand( ODataQueryOptionDescriptionContext cont description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedExpandProperties ) ); } @@ -140,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 ) { @@ -155,7 +155,7 @@ protected virtual string DescribeSelect( ODataQueryOptionDescriptionContext cont .Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedSelectProperties ) ) .ToString(); } @@ -167,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; @@ -185,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 ) @@ -193,7 +190,7 @@ protected virtual string DescribeOrderBy( ODataQueryOptionDescriptionContext con description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedPropertiesDesc, + Fmt.AllowedPropertiesDesc, string.Join( ", ", context.AllowedOrderByProperties ) ); } @@ -207,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() ) { @@ -220,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(); } @@ -231,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() ) { @@ -244,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(); } @@ -263,7 +254,7 @@ private static void AppendAllowedOptions( StringBuilder description, ODataQueryO description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedLogicalOperatorsDesc, + Fmt.AllowedLogicalOperatorsDesc, string.Join( ", ", EnumerateLogicalOperators( context.AllowedLogicalOperators ) ) ); @@ -275,7 +266,7 @@ private static void AppendAllowedOptions( StringBuilder description, ODataQueryO description.Append( Space ) .AppendFormat( CurrentCulture, - ODataExpSR.AllowedArithmeticOperatorsDesc, + Fmt.AllowedArithmeticOperatorsDesc, string.Join( ", ", EnumerateArithmeticOperators( context.AllowedArithmeticOperators ) ) ); @@ -284,11 +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, + Fmt.AllowedFunctionsDesc, context.AllowedFunctions.ToString().ToLowerInvariant() ); +#pragma warning restore CA1308 // Normalize strings to uppercase +#pragma warning restore IDE0079 } } diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs index d9c6822f..a9c96f59 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ImplicitModelBoundSettingsConvention.cs @@ -8,6 +8,7 @@ namespace Asp.Versioning.Conventions; using Microsoft.AspNet.OData.Builder; #else using Microsoft.OData.ModelBuilder; +using System.Buffers; #endif /// <summary> @@ -16,15 +17,12 @@ namespace Asp.Versioning.Conventions; /// </summary> public sealed partial class ImplicitModelBoundSettingsConvention : IModelConfiguration, IODataQueryOptionsConvention { - private readonly HashSet<Type> types = new(); + private readonly HashSet<Type> types = []; /// <inheritdoc /> public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix ) { - if ( builder == null ) - { - throw new ArgumentNullException( nameof( builder ) ); - } + ArgumentNullException.ThrowIfNull( builder ); if ( types.Count == 0 ) { @@ -56,18 +54,53 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? rou private static HashSet<Type>? GetExistingTypes( ODataModelBuilder builder ) { - var types = default( HashSet<Type> ); + 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 - foreach ( var entitySet in builder.EntitySets ) + return types; + } + + using var structuralTypes = builder.StructuralTypes.GetEnumerator(); + + if ( !structuralTypes.MoveNext() ) { - types ??= new(); - types.Add( entitySet.ClrType ); + return default; } - foreach ( var singleton in builder.Singletons ) + types = [structuralTypes.Current.ClrType]; + + while ( structuralTypes.MoveNext() ) { - types ??= new(); - types.Add( singleton.ClrType ); + types.Add( structuralTypes.Current.ClrType ); } return types; @@ -75,9 +108,9 @@ public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? rou private void OnModelCreating( ODataModelBuilder builder ) { - foreach ( var entityType in types.Select( builder.AddEntityType ) ) + foreach ( var type in types ) { - builder.AddEntitySet( entityType.Name, entityType ); + 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 78073220..bcd941a0 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs @@ -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 054f5641..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> ); @@ -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 52759fb6..686a1953 100644 --- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs +++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataValidationSettingsConvention.cs @@ -139,7 +139,11 @@ private static bool IsSupported( string? httpMethod ) private string GetName( AllowedQueryOptions option ) { +#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 // Normalize strings to uppercase +#pragma warning restore IDE0079 return Settings.NoDollarPrefix ? name : name.Insert( 0, "$" ); } 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 4b969183..ce5a1010 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs @@ -41,9 +41,9 @@ public sealed class DefaultModelTypeBuilder : IModelTypeBuilder private readonly bool adHoc; private readonly bool excludeAdHocModels; private DefaultModelTypeBuilder? adHocBuilder; - private ConcurrentDictionary<ApiVersion, ModuleBuilder>? modules; - private ConcurrentDictionary<ApiVersion, IDictionary<EdmTypeKey, Type>>? generatedEdmTypesPerVersion; - private ConcurrentDictionary<ApiVersion, ConcurrentDictionary<EdmTypeKey, Type>>? generatedActionParamsPerVersion; + 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 ) { @@ -61,10 +61,7 @@ private DefaultModelTypeBuilder( bool excludeAdHocModels, bool adHoc ) /// <inheritdoc /> public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion ) { - if ( model == null ) - { - throw new ArgumentNullException( nameof( model ) ); - } + ArgumentNullException.ThrowIfNull( model ); if ( model.IsAdHoc() ) { @@ -79,24 +76,13 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp } } - if ( structuredType == null ) - { - throw new ArgumentNullException( nameof( structuredType ) ); - } - - 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 edmTypes = generatedEdmTypesPerVersion.GetOrAdd( apiVersion, key => GenerateTypesForEdmModel( model, key ) ); + var edmTypes = generatedEdmTypesPerVersion.GetOrAdd( new( model, apiVersion ), key => GenerateTypesForEdmModel( model, key.ApiVersion ) ); return edmTypes[new( structuredType, apiVersion )]; } @@ -104,10 +90,7 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp /// <inheritdoc /> public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ) { - if ( model == null ) - { - throw new ArgumentNullException( nameof( model ) ); - } + ArgumentNullException.ThrowIfNull( model ); if ( !adHoc && model.IsAdHoc() ) { @@ -115,24 +98,13 @@ public Type NewActionParameters( IEdmModel model, IEdmAction action, string cont return adHocBuilder.NewActionParameters( model, action, controllerName, apiVersion ); } - if ( action == null ) - { - throw new ArgumentNullException( nameof( action ) ); - } - - 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() ); + 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, _ => @@ -140,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 ); } ); @@ -148,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 ); @@ -223,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 ) @@ -336,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 ) { @@ -398,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; @@ -518,9 +490,9 @@ private static AssemblyName NewAssemblyName( ApiVersion apiVersion, bool adHoc ) } [MethodImpl( MethodImplOptions.AggressiveInlining )] - private ModuleBuilder CreateModuleForApiVersion( ApiVersion apiVersion ) + private ModuleBuilder CreateModuleForApiVersion( EdmModelKey key ) { - var assemblyName = NewAssemblyName( apiVersion, adHoc ); + var assemblyName = NewAssemblyName( key.ApiVersion, adHoc ); #if NETFRAMEWORK var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly( assemblyName, Run ); #else @@ -548,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 bca4a961..a32cd16a 100644 --- a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs +++ b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs @@ -43,15 +43,8 @@ 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; @@ -121,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] ); } } @@ -248,12 +241,7 @@ private static bool CanBeSubstituted( Type type ) => #endif !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>(); 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 6fb980dd..82c1cfce 100644 --- a/src/Common/src/Common/ApiVersioningOptions.cs +++ b/src/Common/src/Common/ApiVersioningOptions.cs @@ -6,7 +6,6 @@ namespace Asp.Versioning; #if NETFRAMEWORK using System.Net; #endif -using static Asp.Versioning.ApiVersionReader; /// <summary> /// Represents the possible options for API versioning. @@ -48,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> @@ -73,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; } 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 02237016..72ed1408 100644 --- a/src/Common/src/Common/DefaultApiVersionReporter.cs +++ b/src/Common/src/Common/DefaultApiVersionReporter.cs @@ -42,18 +42,13 @@ public DefaultApiVersionReporter( string deprecatedHeaderName = ApiDeprecatedVersions, ApiVersionMapping mapping = Explicit | Implicit ) { - this.sunsetPolicyManager = sunsetPolicyManager ?? throw new ArgumentNullException( nameof( sunsetPolicyManager ) ); - - if ( string.IsNullOrEmpty( apiSupportedVersionsName = supportedHeaderName ) ) - { - throw new ArgumentNullException( nameof( supportedHeaderName ) ); - } - - if ( string.IsNullOrEmpty( apiDeprecatedVersionsName = deprecatedHeaderName ) ) - { - throw new ArgumentNullException( nameof( deprecatedHeaderName ) ); - } + ArgumentNullException.ThrowIfNull( sunsetPolicyManager ); + ArgumentException.ThrowIfNullOrEmpty( supportedHeaderName ); + ArgumentException.ThrowIfNullOrEmpty( deprecatedHeaderName ); + this.sunsetPolicyManager = sunsetPolicyManager; + apiSupportedVersionsName = supportedHeaderName; + apiDeprecatedVersionsName = deprecatedHeaderName; Mapping = mapping; } @@ -63,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 ) { @@ -103,9 +91,7 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel ) #endif var name = metadata.Name; - if ( sunsetPolicyManager.TryGetPolicy( name, version, out var policy ) || - ( !string.IsNullOrEmpty( name ) && sunsetPolicyManager.TryGetPolicy( name, out policy ) ) || - ( version != null && sunsetPolicyManager.TryGetPolicy( version, out policy ) ) ) + if ( sunsetPolicyManager.TryResolvePolicy( name, version, out var policy ) ) { response.WriteSunsetPolicy( policy ); } 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 70a67ab8..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. @@ -113,7 +128,9 @@ public virtual MediaTypeApiVersionReaderBuilder Match( [StringSyntax( StringSynt #if !NETFRAMEWORK [CLSCompliant( false )] #endif - public virtual MediaTypeApiVersionReaderBuilder Select( Func<HttpRequest, IReadOnlyList<string>, IReadOnlyList<string>> selector ) +#pragma warning disable CA1716 // Identifiers should not match keywords + public virtual MediaTypeApiVersionReaderBuilder Select( SelectorCallback selector ) +#pragma warning restore CA1716 // Identifiers should not match keywords { select = selector; return this; @@ -128,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. @@ -142,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; @@ -199,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, @@ -214,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]; @@ -252,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, @@ -265,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; @@ -297,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 ); } @@ -317,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>(); } @@ -325,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 } @@ -372,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 ) { @@ -408,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 eb4fc564..baef9fc0 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTTest.cs @@ -30,6 +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 1ab4dca2..ed3fcbbf 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ActionApiVersionConventionBuilderTest.cs @@ -31,6 +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 5d8f409d..00096fce 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ActionConventionBuilderExtensionsTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ActionConventionBuilderExtensionsTest.cs @@ -104,9 +104,10 @@ public void action_should_throw_exception_when_method_does_not_exist() actionConvention.Should().Throw<MissingMethodException>().And.Message.Should().Be( message ); } -#pragma warning disable CA1822 #pragma warning disable IDE0060 #pragma warning disable IDE0079 +#pragma warning disable CA1034 // Nested types should not be visible +#pragma warning disable CA1822 #if !NETFRAMEWORK [ApiController] diff --git a/src/Common/test/Common.Mvc.Tests/Conventions/ApiVersionConventionBuilderTest.cs b/src/Common/test/Common.Mvc.Tests/Conventions/ApiVersionConventionBuilderTest.cs index abe982a9..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,9 +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(); } } @@ -136,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 bed8a174..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,13 +74,15 @@ private sealed class TestControllerApiVersionConventionBuilder : ControllerApiVe internal ActionApiVersionConventionBuilderCollection<ControllerBase> ProtectedActionBuilders => ActionBuilders; } +#pragma warning disable IDE0079 +#pragma warning disable CA1812 #if !NETFRAMEWORK [ApiController] #endif private sealed class UndecoratedController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } #if !NETFRAMEWORK @@ -92,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 9b5bf47f..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,13 +78,15 @@ internal TestControllerApiVersionConventionBuilder( Type controllerType ) : base internal ActionApiVersionConventionBuilderCollection ProtectedActionBuilders => ActionBuilders; } +#pragma warning disable IDE0079 +#pragma warning disable CA1812 #if !NETFRAMEWORK [ApiController] #endif private sealed class UndecoratedController : ControllerBase { - public IActionResult Get() => Ok(); + public OkResult Get() => Ok(); } #if !NETFRAMEWORK @@ -96,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 5463b05a..0d55d027 100644 --- a/src/Common/test/Common.Mvc.Tests/Conventions/ControllerConventionBuilderExtensionsTest.cs +++ b/src/Common/test/Common.Mvc.Tests/Conventions/ControllerConventionBuilderExtensionsTest.cs @@ -104,6 +104,7 @@ 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 #if !NETFRAMEWORK [ApiController] 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 f2161bbf..6080579f 100644 --- a/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderExtensionsTest.cs +++ b/src/Common/test/Common.OData.ApiExplorer.Tests/Conventions/ODataActionQueryOptionsConventionBuilderExtensionsTest.cs @@ -230,6 +230,7 @@ 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 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 03555e12..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,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 + public sealed class TestController : ControllerBase { public IActionResult Get() => Ok(); 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 8d27dd30..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,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 + public sealed class TestController : ControllerBase { public IActionResult Get() => Ok(); 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 8bdbeb07..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,9 @@ 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 { public IActionResult Get() => Ok(); 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 e09502ba..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,9 @@ // 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 + namespace Asp.Versioning.OData; public class Company 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 8f2ea637..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,9 @@ // 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 + namespace Asp.Versioning.OData; public class Contact 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 496be21c..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,8 @@ // 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; public class Employer 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 468f1f01..53ff4535 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -21,6 +21,10 @@ <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>