diff --git a/.editorconfig b/.editorconfig
index 68505e1c..6e6a94e7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -6,6 +6,7 @@ root = true
# don't use tabs for indentation
[*]
indent_style = space
+guidelines = 120 1px solid yellow
vsspell_section_id = 41b65011239a40959ccaae2a4ec7044a
vsspell_ignored_words_41b65011239a40959ccaae2a4ec7044a = Accessor|app|clr|Edm|inline|middleware|Mvc|odata|Validator|Deconstruct
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index d3130c5b..e3c10e68 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -4,7 +4,7 @@
-- [ ] You've read the [Contributor Guide](https://github.com/dotnet/aspnet-api-versioning/blob/main/CONTRIBUTING.md) and [Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
+- [ ] You've read the [Contributor Guide](https://github.com/dotnet/aspnet-api-versioning/blob/main/docs/CONTRIBUTING.md) and [Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
- [ ] You've included unit or integration tests for your change, where applicable.
- [ ] You've included inline docs for your change, where applicable.
- [ ] There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.
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
-
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi)
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi)
- [](../../wiki/New-Services-Quick-Start#aspnet-web-api)
- [](../../tree/main/examples/AspNet/WebApi)
-
-* **ASP.NET Web API and OData**
- Adds API versioning to your Web API applications using OData v4.0
-
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData)
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData)
- [](../../wiki/New-Services-Quick-Start#aspnet-web-api-with-odata-v40)
- [](../../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:
[](../../wiki/New-Services-Quick-Start#aspnet-core-with-odata-v40)
[](../../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
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi)
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi)
+ [](../../wiki/New-Services-Quick-Start#aspnet-web-api)
+ [](../../tree/main/examples/AspNet/WebApi)
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer)
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer)
- [](../../wiki/API-Documentation#aspnet-web-api)
- [](../../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
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData)
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData)
+ [](../../wiki/New-Services-Quick-Start#aspnet-web-api-with-odata-v40)
+ [](../../tree/main/examples/AspNet/OData)
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer)
- [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer)
- [](../../wiki/API-Documentation#aspnet-web-api-with-odata)
- [](../../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
[](../../wiki/API-Documentation#aspnet-core-with-odata)
[](../../tree/main/examples/AspNetCore/OData/OpenApiODataSample)
+* **ASP.NET Web API Versioned API Explorer**
+ Replaces the default API explorer in your Web API applications
+
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer)
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi.ApiExplorer)
+ [](../../wiki/API-Documentation#aspnet-web-api)
+ [](../../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
+
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer)
+ [](https://www.nuget.org/packages/Asp.Versioning.WebApi.OData.ApiExplorer)
+ [](../../wiki/API-Documentation#aspnet-web-api-with-odata)
+ [](../../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/build/assets.msbuildproj b/build/assets.msbuildproj
index 8d3dba2a..0ae99dc5 100644
--- a/build/assets.msbuildproj
+++ b/build/assets.msbuildproj
@@ -2,6 +2,7 @@
netstandard1.0
false
+ $(NoWarn);NETSDK1215
diff --git a/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/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/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/OData/Directory.Build.props b/examples/AspNetCore/OData/Directory.Build.props
index 302bad37..0c6cf639 100644
--- a/examples/AspNetCore/OData/Directory.Build.props
+++ b/examples/AspNetCore/OData/Directory.Build.props
@@ -4,7 +4,7 @@
-
+
\ No newline at end of file
diff --git a/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/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/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/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/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/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/Asp.Versioning.Abstractions.csproj b/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj
index 00dd4809..861b642c 100644
--- a/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj
+++ b/src/Abstractions/src/Asp.Versioning.Abstractions/Asp.Versioning.Abstractions.csproj
@@ -1,8 +1,8 @@
- 8.0.0
- 8.0.0.0
+ 8.1.0
+ 8.1.0.0
$(DefaultTargetFramework);netstandard1.0;netstandard2.0
API Versioning Abstractions
The abstractions library for API versioning.
@@ -14,6 +14,10 @@
true
+
+ false
+
+
diff --git a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs
index 37be6965..f28f9cca 100644
--- a/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs
+++ b/src/Abstractions/src/Asp.Versioning.Abstractions/ISunsetPolicyManagerExtensions.cs
@@ -46,7 +46,7 @@ public static bool TryGetPolicy(
/// The name of the API.
/// The API version to get the policy for.
/// The applicable sunset policy , if any.
- /// The resolution or is as follows:
+ /// The resolution order is as follows:
///
/// and
/// only
@@ -76,7 +76,7 @@ public static bool TryGetPolicy(
/// The API version to get the policy for.
/// /// The applicable sunset policy , if any.
/// True if the sunset policy was retrieved; otherwise, false.
- /// The resolution or is as follows:
+ /// The resolution order is as follows:
///
/// and
/// only
@@ -102,7 +102,8 @@ public static bool TryResolvePolicy(
return true;
}
}
- else if ( apiVersion != null && policyManager.TryGetPolicy( apiVersion, out sunsetPolicy ) )
+
+ if ( apiVersion != null && policyManager.TryGetPolicy( apiVersion, out sunsetPolicy ) )
{
return true;
}
diff --git a/src/Abstractions/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/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs
index 1c1a2f49..410f48ff 100644
--- a/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs
+++ b/src/AspNet/OData/src/Asp.Versioning.WebApi.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs
@@ -13,6 +13,6 @@ private void VisitAction( HttpActionDescriptor action )
var attributes = new List( controller.GetCustomAttributes( inherit: true ) );
attributes.AddRange( action.GetCustomAttributes( inherit: true ) );
- VisitEnableQuery( attributes );
+ VisitEnableQuery( attributes.ToArray() );
}
}
\ No newline at end of file
diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs
index 20789427..e971646f 100644
--- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs
+++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/MinimalApiFixture.cs
@@ -57,6 +57,7 @@ protected override void OnConfigureEndpoints( IEndpointRouteBuilder endpoints )
protected override void OnAddApiVersioning( ApiVersioningOptions options )
{
+ options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader(),
new UrlSegmentApiVersionReader(),
diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs
index 2823e74b..85016516 100644
--- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs
+++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Http/given a versioned minimal API/when using a query string.cs
@@ -4,6 +4,7 @@ namespace given_a_versioned_minimal_API;
using Asp.Versioning;
using Asp.Versioning.Http;
+using static System.Net.HttpStatusCode;
[Collection( nameof( MinimalApiTestCollection ) )]
public class when_using_a_query_string : AcceptanceTest
@@ -34,9 +35,58 @@ public async Task then_get_should_report_api_versions()
var response = await GetAsync( "api/values?api-version=1.0" );
// assert
+ response.StatusCode.Should().Be( OK );
response.Headers.GetValues( "api-supported-versions" ).Should().Equal( "1.0, 2.0" );
}
+ [Fact]
+ public async Task then_get_should_return_400_for_an_unsupported_version()
+ {
+ // arrange
+
+
+ // act
+ var response = await GetAsync( "api/values?api-version=3.0" );
+ var problem = await response.Content.ReadAsProblemDetailsAsync();
+
+ // assert
+ response.StatusCode.Should().Be( BadRequest );
+ response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
+ problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );
+ }
+
+ [Fact]
+ public async Task then_get_should_return_400_for_an_unspecified_version()
+ {
+ // arrange
+
+
+ // act
+ var response = await GetAsync( "api/values" );
+ var problem = await response.Content.ReadAsProblemDetailsAsync();
+
+ // assert
+ response.StatusCode.Should().Be( BadRequest );
+ response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
+ problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type );
+ }
+
+ [Fact]
+ public async Task then_get_should_return_400_for_a_malformed_version()
+ {
+ // arrange
+
+
+ // act
+ var response = await GetAsync( "api/values?api-version=abc" );
+ var problem = await response.Content.ReadAsProblemDetailsAsync();
+
+ // assert
+ response.StatusCode.Should().Be( BadRequest );
+ response.Headers.GetValues( "api-supported-versions" ).Should().Equal( "1.0, 2.0" );
+ problem.Type.Should().Be( ProblemDetailsDefaults.Invalid.Type );
+ }
+
public when_using_a_query_string( MinimalApiFixture fixture, ITestOutputHelper console )
: base( fixture ) => console.WriteLine( fixture.DirectedGraphVisualizationUrl );
}
\ No newline at end of file
diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs
index 7c78efd4..7b1a868c 100644
--- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs
+++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/Mvc/UsingAttributes/given a versioned Controller/when using a query string and split into two types.cs
@@ -94,9 +94,12 @@ public async Task then_get_should_return_400_for_an_unsupported_version()
// act
var response = await GetAsync( "api/values?api-version=3.0" );
+ var problem = await response.Content.ReadAsProblemDetailsAsync();
// assert
response.StatusCode.Should().Be( BadRequest );
+ response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
+ problem.Type.Should().Be( ProblemDetailsDefaults.Unsupported.Type );
}
[Fact]
@@ -111,6 +114,7 @@ public async Task then_get_should_return_400_for_an_unspecified_version()
// assert
response.StatusCode.Should().Be( BadRequest );
+ response.Headers.GetValues( "api-supported-versions" ).Single().Should().Be( "1.0, 2.0" );
problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type );
}
diff --git a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs
index 7cea36c0..3e52096b 100644
--- a/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs
+++ b/src/AspNetCore/Acceptance/Asp.Versioning.Mvc.Acceptance.Tests/OData/Basic/given a versioned ODataController/when using a query string.cs
@@ -49,6 +49,7 @@ public async Task then_get_should_return_400_for_an_unspecified_version()
// assert
response.StatusCode.Should().Be( BadRequest );
problem.Type.Should().Be( ProblemDetailsDefaults.Unspecified.Type );
+ problem.Extensions["code"].ToString().Should().Be( "ApiVersionUnspecified" );
}
public when_using_a_query_string( BasicFixture fixture, ITestOutputHelper console )
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs
index 5736e8f4..fdb4ff8d 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/ODataApiDescriptionProvider.cs
@@ -142,6 +142,7 @@ public virtual void OnProvidersExecuted( ApiDescriptionProviderContext context )
else
{
UpdateModelTypes( result, matched );
+ UpdateFunctionCollectionParameters( result, matched );
}
}
@@ -456,6 +457,75 @@ private void UpdateModelTypes( ApiDescription description, IODataRoutingMetadata
}
}
+ private static void UpdateFunctionCollectionParameters( ApiDescription description, IODataRoutingMetadata metadata )
+ {
+ var parameters = description.ParameterDescriptions;
+
+ if ( parameters.Count == 0 )
+ {
+ return;
+ }
+
+ var function = default( IEdmFunction );
+ var mapping = default( IDictionary );
+
+ for ( var i = 0; i < metadata.Template.Count; i++ )
+ {
+ var segment = metadata.Template[i];
+
+ if ( segment is FunctionSegmentTemplate func )
+ {
+ function = func.Function;
+ mapping = func.ParameterMappings;
+ break;
+ }
+ else if ( segment is FunctionImportSegmentTemplate import )
+ {
+ function = import.FunctionImport.Function;
+ mapping = import.ParameterMappings;
+ break;
+ }
+ }
+
+ if ( function is null || mapping is null )
+ {
+ return;
+ }
+
+ var name = default( string );
+
+ foreach ( var parameter in function.Parameters )
+ {
+ if ( parameter.Type.IsCollection() &&
+ mapping.TryGetValue( parameter.Name, out name ) &&
+ parameters.SingleOrDefault( p => p.Name == name ) is { } param )
+ {
+ param.Source = BindingSource.Path;
+ break;
+ }
+ }
+
+ var path = description.RelativePath;
+
+ if ( string.IsNullOrEmpty( name ) || string.IsNullOrEmpty( path ) )
+ {
+ return;
+ }
+
+ var span = name.AsSpan();
+ Span oldValue = stackalloc char[name.Length + 2];
+ Span newValue = stackalloc char[name.Length + 4];
+
+ newValue[1] = oldValue[0] = '{';
+ newValue[^2] = oldValue[^1] = '}';
+ newValue[0] = '[';
+ newValue[^1] = ']';
+ span.CopyTo( oldValue.Slice( 1, name.Length ) );
+ span.CopyTo( newValue.Slice( 2, name.Length ) );
+
+ description.RelativePath = path.Replace( oldValue.ToString(), newValue.ToString(), Ordinal );
+ }
+
private sealed class ApiDescriptionComparer : IEqualityComparer
{
private readonly IEqualityComparer comparer = StringComparer.OrdinalIgnoreCase;
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs
index fc1ff1b6..4099958b 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ApiExplorer/PartialODataDescriptionProvider.cs
@@ -145,14 +145,10 @@ private static int ODataOrder() =>
new ODataApiDescriptionProvider(
new StubModelMetadataProvider(),
new StubModelTypeBuilder(),
- new OptionsFactory(
- Enumerable.Empty>(),
- Enumerable.Empty>() ),
+ new OptionsFactory( [], [] ),
Opts.Create(
new ODataApiExplorerOptions(
- new(
- new StubODataApiVersionCollectionProvider(),
- Enumerable.Empty() ) ) ) ).Order;
+ new( new StubODataApiVersionCollectionProvider(), [] ) ) ) ).Order;
[MethodImpl( MethodImplOptions.AggressiveInlining )]
private static void MarkAsAdHoc( ODataModelBuilder builder, IEdmModel model ) =>
@@ -205,6 +201,8 @@ public ModelMetadata GetMetadataForType( Type modelType ) =>
throw new NotImplementedException();
}
+ [UnconditionalSuppressMessage( "ILLink", "IL2092" )]
+ [UnconditionalSuppressMessage( "ILLink", "IL2093" )]
private sealed class StubModelTypeBuilder : IModelTypeBuilder
{
public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion ) =>
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj
index d132353a..bf0661c6 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/Asp.Versioning.OData.ApiExplorer.csproj
@@ -1,13 +1,14 @@
- 8.0.0
- 8.0.0.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.
Asp;AspNet;AspNetCore;Versioning;ApiExplorer;OData
+ true
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt
index 5f282702..b5318606 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData.ApiExplorer/ReleaseNotes.txt
@@ -1 +1 @@
-
\ No newline at end of file
+Support OData 9.0 ([#1103](https://github.com/dotnet/aspnet-api-versioning/issues/1103))
\ No newline at end of file
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj
index ee194d18..64df816f 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/Asp.Versioning.OData.csproj
@@ -1,13 +1,14 @@
- 8.0.0
- 8.0.0.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.
Asp;AspNet;AspNetCore;Versioning;OData
+ true
@@ -15,7 +16,7 @@
-
+
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs
index 16432665..0f17e2eb 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/DependencyInjection/IServiceCollectionExtensions.cs
@@ -31,6 +31,7 @@ internal static ApplicationPartManager GetOrCreateApplicationPartManager( this I
return partManager;
}
+ [UnconditionalSuppressMessage( "ILLink", "IL2072", Justification = "Model configuration types are never trimmed" )]
internal static void AddModelConfigurationsAsServices( this IServiceCollection services, ApplicationPartManager partManager )
{
var feature = new ModelConfigurationFeature();
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs
index 575a0a3a..567b0c2e 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApiVersioningOptions.cs
@@ -52,7 +52,7 @@ public ODataApiVersioningOptions( VersionedODataModelBuilder modelBuilder ) =>
///
/// The associated OData prefix.
/// The original options .
- public virtual ODataApiVersioningOptions AddRouteComponents( string prefix ) =>
+ public virtual ODataApiVersioningOptions AddRouteComponents( [StringSyntax( "Route" )] string prefix ) =>
AddRouteComponents( prefix, static _ => { } );
///
@@ -70,7 +70,7 @@ public virtual ODataApiVersioningOptions AddRouteComponents( ActionThe $batch handler .
/// The original options .
[CLSCompliant( false )]
- public ODataApiVersioningOptions AddRouteComponents( string prefix, ODataBatchHandler batchHandler ) =>
+ public ODataApiVersioningOptions AddRouteComponents( [StringSyntax( "Route" )] string prefix, ODataBatchHandler batchHandler ) =>
AddRouteComponents( prefix, builder => builder.AddSingleton( batchHandler ) );
///
@@ -89,7 +89,7 @@ public ODataApiVersioningOptions AddRouteComponents( ODataBatchHandler batchHand
/// The configuration action .
/// The original options .
public virtual ODataApiVersioningOptions AddRouteComponents(
- string prefix,
+ [StringSyntax( "Route" )] string prefix,
Action configureAction )
{
configurations ??= new( StringComparer.OrdinalIgnoreCase );
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs
index 402da34d..9ef71b7b 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataApplicationModelProvider.cs
@@ -141,7 +141,7 @@ private static
return (metadataControllers, supported, deprecated);
}
- private static ControllerModel? SelectBestMetadataController( IReadOnlyList controllers )
+ private static ControllerModel? SelectBestMetadataController( List controllers )
{
// note: there should be at least 2 metadata controllers, but there could be 3+
// if a developer defines their own custom controller. ultimately, there can be
diff --git a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs
index cc798280..5676e28f 100644
--- a/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs
+++ b/src/AspNetCore/OData/src/Asp.Versioning.OData/OData/ODataMultiModelApplicationModelProvider.cs
@@ -135,7 +135,8 @@ private static Type GetDefaultApplicationModelProviderType()
return Type.GetType( $"{TypeName}, {assemblyName}", throwOnError: true, ignoreCase: false )!;
}
- private static Func, IApplicationModelProvider> CreateActivator( Type type )
+ private static Func, IApplicationModelProvider> CreateActivator(
+ [DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.PublicConstructors )] Type type )
{
var options = Parameter( typeof( IOptions ), "options" );
var @new = New( type.GetConstructors()[0], options );
diff --git a/src/AspNetCore/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/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs
index 511513b4..532abb00 100644
--- a/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs
+++ b/src/AspNetCore/OData/test/Asp.Versioning.OData.ApiExplorer.Tests/ApiExplorer/ODataApiDescriptionProviderTest.cs
@@ -256,9 +256,9 @@ private static void AssertQueryOptionWithoutOData( ApiDescription description, s
parameter.ModelMetadata.Description.Should().EndWith( suffix + '.' );
}
- private void PrintGroup( IReadOnlyList items )
+ private void PrintGroup( ApiDescription[] items )
{
- for ( var i = 0; i < items.Count; i++ )
+ for ( var i = 0; i < items.Length; i++ )
{
var item = items[i];
console.WriteLine( $"[{item.GroupName}] {item.HttpMethod} {item.RelativePath}" );
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs
new file mode 100644
index 00000000..7109d87f
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/DefaultEndpointInspector.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer;
+
+using Microsoft.AspNetCore.Http;
+
+///
+/// Represents the default endpoint inspector .
+///
+[CLSCompliant(false)]
+public sealed class DefaultEndpointInspector : IEndpointInspector
+{
+ ///
+ public bool IsControllerAction( Endpoint endpoint ) => false;
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs
index 45c14725..e5ce20fb 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/EndpointApiVersionMetadataCollationProvider.cs
@@ -12,15 +12,29 @@ namespace Asp.Versioning.ApiExplorer;
public sealed class EndpointApiVersionMetadataCollationProvider : IApiVersionMetadataCollationProvider
{
private readonly EndpointDataSource endpointDataSource;
+ private readonly IEndpointInspector endpointInspector;
private int version;
///
/// Initializes a new instance of the class.
///
/// The underlying endpoint data source .
+ [Obsolete( "Use the constructor that accepts IEndpointInspector. This constructor will be removed in a future version." )]
public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource )
+ : this( endpointDataSource, new DefaultEndpointInspector() ) { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The underlying endpoint data source .
+ /// The endpoint inspector used to inspect endpoints.
+ public EndpointApiVersionMetadataCollationProvider( EndpointDataSource endpointDataSource, IEndpointInspector endpointInspector )
{
- this.endpointDataSource = endpointDataSource ?? throw new ArgumentNullException( nameof( endpointDataSource ) );
+ ArgumentNullException.ThrowIfNull( endpointDataSource );
+ ArgumentNullException.ThrowIfNull( endpointInspector );
+
+ this.endpointDataSource = endpointDataSource;
+ this.endpointInspector = endpointInspector;
ChangeToken.OnChange( endpointDataSource.GetChangeToken, () => ++version );
}
@@ -38,7 +52,8 @@ public void Execute( ApiVersionMetadataCollationContext context )
{
var endpoint = endpoints[i];
- if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item )
+ if ( endpoint.Metadata.GetMetadata() is not ApiVersionMetadata item ||
+ endpointInspector.IsControllerAction( endpoint ) )
{
continue;
}
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ApiExplorer/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/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj
index 32e89b8d..3abd2a2c 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj
@@ -1,8 +1,8 @@
- 8.0.0
- 8.0.0.0
+ 8.1.0
+ 8.1.0.0
$(DefaultTargetFramework)
Asp.Versioning
ASP.NET Core API Versioning
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs
index 07911329..f8948ab4 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/DependencyInjection/IServiceCollectionExtensions.cs
@@ -10,8 +10,8 @@ namespace Microsoft.Extensions.DependencyInjection;
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.
@@ -75,20 +75,80 @@ public static IApiVersioningBuilder EnableApiVersionBinding( this IApiVersioning
return builder;
}
+ ///
+ /// Adds error object support in problem details.
+ ///
+ /// The services available in the application.
+ /// The JSON options setup to perform, if any.
+ /// The original .
+ ///
+ ///
+ /// This method is only intended to provide backward compatibility with previous library versions by converting
+ /// into Error Objects that conform to the
+ /// Error Responses
+ /// in the Microsoft REST API Guidelines and
+ /// OData Error Responses .
+ ///
+ ///
+ /// This method should be called before .
+ ///
+ ///
+ public static IServiceCollection AddErrorObjects( this IServiceCollection services, Action? setup = default ) =>
+ AddErrorObjects( services, setup );
+
+ ///
+ /// Adds error object support in problem details.
+ ///
+ /// The type of .
+ /// The services available in the application.
+ /// The JSON options setup to perform, if any.
+ /// The original .
+ ///
+ ///
+ /// This method is only intended to provide backward compatibility with previous library versions by converting
+ /// into Error Objects that conform to the
+ /// Error Responses
+ /// in the Microsoft REST API Guidelines and
+ /// OData Error Responses .
+ ///
+ ///
+ /// This method should be called before .
+ ///
+ ///
+ public static IServiceCollection AddErrorObjects<[DynamicallyAccessedMembers( PublicConstructors )] TWriter>(
+ this IServiceCollection services,
+ Action? setup = default )
+ where TWriter : ErrorObjectWriter
+ {
+ ArgumentNullException.ThrowIfNull( services );
+
+ services.TryAddEnumerable( Singleton() );
+ services.Configure( setup ?? DefaultErrorObjectJsonConfig );
+
+ // TODO: remove with TryAddErrorObjectJsonOptions in 9.0+
+ services.AddTransient();
+
+ return services;
+ }
+
+ private static void DefaultErrorObjectJsonConfig( JsonOptions options ) =>
+ options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default );
+
private static void AddApiVersioningServices( IServiceCollection services )
{
ArgumentNullException.ThrowIfNull( services );
services.TryAddSingleton();
- services.AddSingleton( sp => sp.GetRequiredService>().Value.ApiVersionReader );
- services.AddSingleton( sp => (IApiVersionParameterSource) sp.GetRequiredService>().Value.ApiVersionReader );
- services.AddSingleton( sp => sp.GetRequiredService>().Value.ApiVersionSelector );
+ services.AddSingleton( static sp => sp.GetRequiredService>().Value.ApiVersionReader );
+ services.AddSingleton( static sp => (IApiVersionParameterSource) sp.GetRequiredService>().Value.ApiVersionReader );
+ services.AddSingleton( static sp => sp.GetRequiredService>().Value.ApiVersionSelector );
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddEnumerable( Transient, ValidateApiVersioningOptions>() );
services.TryAddEnumerable( Transient, ApiVersioningRouteOptionsSetup>() );
services.TryAddEnumerable( Singleton() );
services.TryAddEnumerable( Singleton() );
+ services.TryAddTransient();
services.Replace( WithLinkGeneratorDecorator( services ) );
TryAddProblemDetailsRfc7231Compliance( services );
TryAddErrorObjectJsonOptions( services );
@@ -154,7 +214,7 @@ LinkGenerator NewFactory( IServiceProvider serviceProvider )
}
}
- // TODO: Remove in .NET 9.0 or .NET 8.0 patch
+ // TODO: Fixed and released; remove in .NET 10.0
// BUG: https://github.com/dotnet/aspnetcore/issues/52577
private static void TryAddProblemDetailsRfc7231Compliance( IServiceCollection services )
{
@@ -165,11 +225,12 @@ private static void TryAddProblemDetailsRfc7231Compliance( IServiceCollection se
return;
}
+ var index = services.IndexOf( descriptor );
var decoratedType = descriptor.ImplementationType!;
var lifetime = descriptor.Lifetime;
+ services[index] = Describe( typeof( IProblemDetailsWriter ), sp => NewProblemDetailsWriter( sp, decoratedType ), lifetime );
services.Add( Describe( decoratedType, decoratedType, lifetime ) );
- services.Replace( Describe( typeof( IProblemDetailsWriter ), sp => NewProblemDetailsWriter( sp, decoratedType ), lifetime ) );
static bool IsDefaultProblemDetailsWriter( ServiceDescriptor serviceDescriptor ) =>
serviceDescriptor.ServiceType == typeof( IProblemDetailsWriter ) &&
@@ -179,23 +240,48 @@ static Rfc7231ProblemDetailsWriter NewProblemDetailsWriter( IServiceProvider ser
new( (IProblemDetailsWriter) serviceProvider.GetRequiredService( decoratedType ) );
}
+ // TODO: retain for 8.1.x back-compat, but remove in 9.0+ in favor of AddErrorObjects for perf
private static void TryAddErrorObjectJsonOptions( IServiceCollection services )
{
var serviceType = typeof( IProblemDetailsWriter );
var implementationType = typeof( ErrorObjectWriter );
+ var markerType = typeof( ErrorObjectsAdded );
+ var hasErrorObjects = false;
+ var hasErrorObjectsJsonConfig = false;
for ( var i = 0; i < services.Count; i++ )
{
var service = services[i];
- // inheritance is intentionally not considered here because it will require a user-defined
- // JsonSerlizerContext and IConfigureOptions
- if ( service.ServiceType == serviceType &&
- service.ImplementationType == implementationType )
+ if ( !hasErrorObjects &&
+ service.ServiceType == serviceType &&
+ implementationType.IsAssignableFrom( service.ImplementationType ) )
{
- services.TryAddEnumerable( Singleton, ErrorObjectJsonOptionsSetup>() );
- return;
+ hasErrorObjects = true;
+
+ if ( hasErrorObjectsJsonConfig )
+ {
+ break;
+ }
}
+ else if ( service.ServiceType == markerType )
+ {
+ hasErrorObjectsJsonConfig = true;
+
+ if ( hasErrorObjects )
+ {
+ break;
+ }
+ }
+ }
+
+ if ( hasErrorObjects && !hasErrorObjectsJsonConfig )
+ {
+ services.Configure( DefaultErrorObjectJsonConfig );
}
}
+
+// 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/ErrorObjectJsonOptionsSetup.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs
deleted file mode 100644
index 664c4840..00000000
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectJsonOptionsSetup.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright (c) .NET Foundation and contributors. All rights reserved.
-
-#pragma warning disable CA1812 // Avoid uninstantiated internal classes
-
-namespace Asp.Versioning;
-
-using Microsoft.AspNetCore.Http.Json;
-using Microsoft.Extensions.Options;
-
-///
-/// Adds the ErrorObjectJsonContext to the current JsonSerializerOptions.
-///
-/// This allows for consistent serialization behavior for ErrorObject regardless if the
-/// default reflection-based serializer is used and makes it trim/NativeAOT compatible.
-///
-internal sealed class ErrorObjectJsonOptionsSetup : IConfigureOptions
-{
- // Always insert the ErrorObjectJsonContext to the beginning of the chain at the time this Configure
- // is invoked. This JsonTypeInfoResolver will be before the default reflection-based resolver, and
- // before any other resolvers currently added. If apps need to customize serialization, they can
- // prepend a custom ErrorObject resolver to the chain in an IConfigureOptions registered.
- public void Configure( JsonOptions options ) =>
- options.SerializerOptions.TypeInfoResolverChain.Insert( 0, ErrorObjectWriter.ErrorObjectJsonContext.Default );
-}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs
index 82460917..b02d1260 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/ErrorObjectWriter.cs
@@ -1,5 +1,6 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Ignore Spelling: Serializer
namespace Asp.Versioning;
using Microsoft.AspNetCore.Http;
@@ -30,6 +31,19 @@ public partial class ErrorObjectWriter : IProblemDetailsWriter
public ErrorObjectWriter( IOptions options ) =>
this.options = ( options ?? throw new ArgumentNullException( nameof( options ) ) ).Value.SerializerOptions;
+ ///
+ /// Gets the associated, default .
+ ///
+ /// The associated, default .
+ public static JsonSerializerContext DefaultJsonSerializerContext => ErrorObjectJsonContext.Default;
+
+ ///
+ /// Creates and returns a new associated with the writer.
+ ///
+ /// The JSON serializer options to use.
+ /// A new .
+ public static JsonSerializerContext NewJsonSerializerContext( JsonSerializerOptions options ) => new ErrorObjectJsonContext( options );
+
///
public virtual bool CanWrite( ProblemDetailsContext context )
{
@@ -89,6 +103,7 @@ internal ErrorObject( ProblemDetails problemDetails ) =>
///
protected internal readonly partial struct ErrorDetail
{
+ private const string CodeProperty = "code";
private readonly ProblemDetails problemDetails;
private readonly InnerError? innerError;
private readonly Dictionary extensions = [];
@@ -103,23 +118,21 @@ internal ErrorDetail( ProblemDetails problemDetails )
/// Gets or sets one of a server-defined set of error codes.
///
/// A server-defined error code.
- [JsonPropertyName( "code" )]
+ [JsonPropertyName( CodeProperty )]
[JsonIgnore( Condition = WhenWritingNull )]
public string? Code
{
- get => problemDetails.Extensions.TryGetValue( "code", out var value ) &&
- value is string code ?
- code :
- default;
+ get => problemDetails.Extensions.TryGetValue( CodeProperty, out var value ) &&
+ value is string code ? code : default;
set
{
if ( value is null )
{
- problemDetails.Extensions.Remove( "code" );
+ problemDetails.Extensions.Remove( CodeProperty );
}
else
{
- problemDetails.Extensions["code"] = value;
+ problemDetails.Extensions[CodeProperty] = value;
}
}
}
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs
index e43223aa..8530e5a4 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionMatcherPolicy.cs
@@ -337,14 +337,14 @@ private static void Collate(
{
// this is a best guess effort at collating all supported and deprecated
// versions for an api when unmatched and it needs to be reported. it's
- // impossible to sure as there is no way to correlate an arbitrary
+ // impossible to be sure as there is no way to correlate an arbitrary
// request url by endpoint or name. the routing system will build a tree
// based on the route template before the jump table policy is created,
// which provides a natural method of grouping. manual, contrived tests
// demonstrated that were the results were correctly collated together.
// it is possible there is an edge case that isn't covered, but it's
// unclear what that would look like. one or more test cases should be
- // added to document that if discovered
+ // added to document that is discovered
ApiVersionModel model;
if ( supported == null )
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs
index 24bb4a23..e2d32a26 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ApiVersionPolicyJumpTable.cs
@@ -76,6 +76,8 @@ public override int GetDestination( HttpContext httpContext )
return rejection.AssumeDefault;
}
+ httpContext.Features.Set( policyFeature );
+
// 3. unspecified
return versionsByUrlOnly
/* 404 */ ? rejection.Exit
@@ -86,6 +88,8 @@ public override int GetDestination( HttpContext httpContext )
if ( !parser.TryParse( rawApiVersion, out var apiVersion ) )
{
+ httpContext.Features.Set( policyFeature );
+
if ( versionsByUrl )
{
feature.RawRequestedApiVersion = rawApiVersion;
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs
index cad20584..9b594f04 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/ClientErrorEndpointBuilder.cs
@@ -30,7 +30,7 @@ public Endpoint Build()
{
if ( feature.RawRequestedApiVersions.Count == 0 )
{
- return new UnspecifiedApiVersionEndpoint( logger, GetDisplayNames() );
+ return new UnspecifiedApiVersionEndpoint( logger, options, GetDisplayNames() );
}
return new UnsupportedApiVersionEndpoint( options );
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs
index 8fb60798..621cf185 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EdgeBuilder.cs
@@ -32,12 +32,12 @@ public EdgeBuilder(
keys = new( capacity + 1 );
edges = new( capacity + RejectionEndpointCapacity )
{
- [EdgeKey.Malformed] = new( capacity: 1 ) { new MalformedApiVersionEndpoint( logger ) },
- [EdgeKey.Ambiguous] = new( capacity: 1 ) { new AmbiguousApiVersionEndpoint( logger ) },
- [EdgeKey.Unspecified] = new( capacity: 1 ) { new UnspecifiedApiVersionEndpoint( logger ) },
- [EdgeKey.Unsupported] = new( capacity: 1 ) { new UnsupportedApiVersionEndpoint( options ) },
- [EdgeKey.UnsupportedMediaType] = new( capacity: 1 ) { new UnsupportedMediaTypeEndpoint( options ) },
- [EdgeKey.NotAcceptable] = new( capacity: 1 ) { new NotAcceptableEndpoint( options ) },
+ [EdgeKey.Malformed] = [new MalformedApiVersionEndpoint( logger, options )],
+ [EdgeKey.Ambiguous] = [new AmbiguousApiVersionEndpoint( logger )],
+ [EdgeKey.Unspecified] = [new UnspecifiedApiVersionEndpoint( logger, options )],
+ [EdgeKey.Unsupported] = [new UnsupportedApiVersionEndpoint( options )],
+ [EdgeKey.UnsupportedMediaType] = [new UnsupportedMediaTypeEndpoint( options )],
+ [EdgeKey.NotAcceptable] = [new NotAcceptableEndpoint( options )],
};
}
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs
index 9a8b539b..61fccfd3 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/EndpointProblem.cs
@@ -25,7 +25,7 @@ internal static ProblemDetailsContext New( HttpContext context, ProblemDetailsIn
},
};
- if ( string.IsNullOrEmpty( code ) )
+ if ( !string.IsNullOrEmpty( code ) )
{
newContext.ProblemDetails.Extensions[nameof( code )] = code;
}
@@ -33,20 +33,30 @@ internal static ProblemDetailsContext New( HttpContext context, ProblemDetailsIn
return newContext;
}
- internal static Task UnsupportedApiVersion(
- HttpContext context,
- ApiVersioningOptions options,
- int statusCode )
+ internal static bool TryReportApiVersions( HttpContext context, ApiVersioningOptions options )
{
- context.Response.StatusCode = statusCode;
-
if ( options.ReportApiVersions &&
context.Features.Get() is ApiVersionPolicyFeature feature )
{
var reporter = context.RequestServices.GetRequiredService();
var model = feature.Metadata.Map( reporter.Mapping );
context.Response.OnStarting( ReportApiVersions, (reporter, context.Response, model) );
+ return true;
+ }
+ else
+ {
+ return false;
}
+ }
+
+ internal static Task UnsupportedApiVersion(
+ HttpContext context,
+ ApiVersioningOptions options,
+ int statusCode )
+ {
+ context.Response.StatusCode = statusCode;
+
+ TryReportApiVersions( context, options );
if ( context.TryGetProblemDetailsService( out var problemDetails ) )
{
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs
index c129794a..5c777eb6 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/MalformedApiVersionEndpoint.cs
@@ -12,16 +12,18 @@ internal sealed class MalformedApiVersionEndpoint : Endpoint
{
private const string Name = "400 Invalid API Version";
- internal MalformedApiVersionEndpoint( ILogger logger )
- : base( c => OnExecute( c, logger ), Empty, Name ) { }
+ internal MalformedApiVersionEndpoint( ILogger logger, ApiVersioningOptions options )
+ : base( context => OnExecute( context, options, logger ), Empty, Name ) { }
- private static Task OnExecute( HttpContext context, ILogger logger )
+ private static Task OnExecute( HttpContext context, ApiVersioningOptions options, ILogger logger )
{
var requestedVersion = context.ApiVersioningFeature().RawRequestedApiVersion;
logger.ApiVersionInvalid( requestedVersion );
context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ EndpointProblem.TryReportApiVersions( context, options );
+
if ( !context.TryGetProblemDetailsService( out var problemDetails ) )
{
return Task.CompletedTask;
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs
index 6597aec9..7c861b88 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Routing/UnspecifiedApiVersionEndpoint.cs
@@ -10,10 +10,17 @@ internal sealed class UnspecifiedApiVersionEndpoint : Endpoint
{
private const string Name = "400 Unspecified API Version";
- internal UnspecifiedApiVersionEndpoint( ILogger logger, string[]? displayNames = default )
- : base( c => OnExecute( c, displayNames, logger ), Empty, Name ) { }
-
- private static Task OnExecute( HttpContext context, string[]? candidateEndpoints, ILogger logger )
+ internal UnspecifiedApiVersionEndpoint(
+ ILogger logger,
+ ApiVersioningOptions options,
+ string[]? displayNames = default )
+ : base( context => OnExecute( context, options, displayNames, logger ), Empty, Name ) { }
+
+ private static Task OnExecute(
+ HttpContext context,
+ ApiVersioningOptions options,
+ string[]? candidateEndpoints,
+ ILogger logger )
{
if ( candidateEndpoints == null || candidateEndpoints.Length == 0 )
{
@@ -26,6 +33,8 @@ private static Task OnExecute( HttpContext context, string[]? candidateEndpoints
context.Response.StatusCode = StatusCodes.Status400BadRequest;
+ EndpointProblem.TryReportApiVersions( context, options );
+
if ( context.TryGetProblemDetailsService( out var problemDetails ) )
{
return problemDetails.TryWriteAsync(
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs
index a0cf3835..30aaf0cd 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiDescriptionExtensions.cs
@@ -106,9 +106,14 @@ public static bool TryUpdateRelativePathAndRemoveApiVersionParameter( this ApiDe
return false;
}
- var token = '{' + parameter.Name + '}';
+ Span token = stackalloc char[parameter.Name.Length + 2];
+
+ token[0] = '{';
+ token[^1] = '}';
+ parameter.Name.AsSpan().CopyTo( token.Slice( 1, parameter.Name.Length ) );
+
var value = apiVersion.ToString( options.SubstitutionFormat, CultureInfo.InvariantCulture );
- var newRelativePath = relativePath.Replace( token, value, StringComparison.Ordinal );
+ var newRelativePath = relativePath.Replace( token.ToString(), value, StringComparison.Ordinal );
if ( relativePath == newRelativePath )
{
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs
index 1a79e0a3..6c94a430 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiExplorerOptionsFactory{T}.cs
@@ -3,13 +3,15 @@
namespace Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.Options;
+using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes;
///
/// Represents a factory to create API explorer options.
///
/// The type of options to create.
[CLSCompliant( false )]
-public class ApiExplorerOptionsFactory : OptionsFactory where T : ApiExplorerOptions
+public class ApiExplorerOptionsFactory<[DynamicallyAccessedMembers( PublicParameterlessConstructor )] T>
+ : OptionsFactory where T : ApiExplorerOptions
{
private readonly IOptions optionsHolder;
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionDescriptionProviderFactory.cs
index 05b1cba1..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,33 +1,30 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
-#pragma warning disable SA1135 // Using directives should be qualified
-#pragma warning disable SA1200 // Using directives should be placed correctly
-
-using Asp.Versioning;
-using Asp.Versioning.ApiExplorer;
-using Microsoft.Extensions.Options;
+#pragma warning disable CA1812 // Avoid uninstantiated internal classes
namespace Microsoft.AspNetCore.Builder;
+using Asp.Versioning;
+using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Routing;
-using Activator = Func, ISunsetPolicyManager, IOptions, IApiVersionDescriptionProvider>;
+using Microsoft.Extensions.Options;
internal sealed class ApiVersionDescriptionProviderFactory : IApiVersionDescriptionProviderFactory
{
private readonly ISunsetPolicyManager sunsetPolicyManager;
private readonly IApiVersionMetadataCollationProvider[] providers;
+ private readonly IEndpointInspector endpointInspector;
private readonly IOptions options;
- private readonly Activator activator;
public ApiVersionDescriptionProviderFactory(
- Activator activator,
ISunsetPolicyManager sunsetPolicyManager,
IEnumerable providers,
+ IEndpointInspector endpointInspector,
IOptions options )
{
- this.activator = activator;
this.sunsetPolicyManager = sunsetPolicyManager;
this.providers = providers.ToArray();
+ this.endpointInspector = endpointInspector;
this.options = options;
}
@@ -35,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/ApiVersionParameterDescriptionContext.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/ApiVersionParameterDescriptionContext.cs
index 136ad268..8ebe5895 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;
@@ -116,6 +117,11 @@ public virtual void AddParameter( string name, ApiVersionParameterLocation locat
{
if ( IsApiVersionNeutral && !Options.AddApiVersionParametersWhenVersionNeutral )
{
+ if ( location == Path )
+ {
+ UpdateUrlSegment();
+ }
+
return;
}
@@ -304,7 +310,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 +381,7 @@ routeInfo.Constraints is IEnumerable constraints &&
},
Source = BindingSource.Path,
};
- var token = $"{parameter.Name}:{constraintName}";
+ var token = FormatToken( parameter.Name!, constraintName! );
description.RelativePath = relativePath.Replace( token, parameter.Name, Ordinal );
description.ParameterDescriptions.Insert( 0, result );
@@ -457,4 +463,18 @@ private static bool FirstParameterIsOptional(
return apiVersion == defaultApiVersion;
}
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ private static string FormatToken( ReadOnlySpan parameterName, ReadOnlySpan constraintName )
+ {
+ var left = parameterName.Length;
+ var right = constraintName.Length;
+ Span token = stackalloc char[left + right + 1];
+
+ parameterName.CopyTo( token[..left] );
+ token[left] = ':';
+ constraintName.CopyTo( token.Slice( left + 1, right ) );
+
+ return token.ToString();
+ }
}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj
index 35aa3746..5845752b 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Asp.Versioning.Mvc.ApiExplorer.csproj
@@ -1,13 +1,14 @@
- 8.0.0
- 8.0.0.0
+ 8.1.0
+ 8.1.0.0
$(DefaultTargetFramework)
Asp.Versioning.ApiExplorer
ASP.NET Core API Versioning API Explorer
The API Explorer extensions for ASP.NET Core API Versioning.
Asp;AspNet;AspNetCore;Versioning;ApiExplorer
+ true
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs
index 63408cad..22151e11 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DefaultApiVersionDescriptionProvider.cs
@@ -2,9 +2,8 @@
namespace Asp.Versioning.ApiExplorer;
+using Asp.Versioning.ApiExplorer.Internal;
using Microsoft.Extensions.Options;
-using static Asp.Versioning.ApiVersionMapping;
-using static System.Globalization.CultureInfo;
///
/// Represents the default implementation of an object that discovers and describes the API version information within an application.
@@ -12,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer;
[CLSCompliant( false )]
public class DefaultApiVersionDescriptionProvider : IApiVersionDescriptionProvider
{
- private readonly ApiVersionDescriptionCollection collection;
+ private readonly ApiVersionDescriptionCollection collection;
private readonly IOptions options;
///
@@ -28,7 +27,7 @@ public DefaultApiVersionDescriptionProvider(
ISunsetPolicyManager sunsetPolicyManager,
IOptions apiExplorerOptions )
{
- collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
+ collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
SunsetPolicyManager = sunsetPolicyManager;
options = apiExplorerOptions;
}
@@ -58,133 +57,53 @@ protected virtual IReadOnlyList Describe( IReadOnlyList( capacity: metadata.Count );
- var supported = new HashSet();
- var deprecated = new HashSet();
-
- BucketizeApiVersions( metadata, supported, deprecated );
- AppendDescriptions( descriptions, supported, deprecated: false );
- AppendDescriptions( descriptions, deprecated, deprecated: true );
-
- return descriptions.OrderBy( d => d.ApiVersion ).ToArray();
- }
-
- private void BucketizeApiVersions( IReadOnlyList metadata, HashSet supported, HashSet deprecated )
- {
- var declared = new HashSet();
- var advertisedSupported = new HashSet();
- var advertisedDeprecated = new HashSet();
-
- for ( var i = 0; i < metadata.Count; i++ )
+ // TODO: consider refactoring and removing GroupedApiVersionDescriptionProvider as both implementations are now
+ // effectively the same. this cast is safe as an internal implementation detail. if this method is
+ // overridden, then this code doesn't even run
+ //
+ // REF: https://github.com/dotnet/aspnet-api-versioning/issues/1066
+ if ( metadata is GroupedApiVersionMetadata[] groupedMetadata )
{
- var model = metadata[i].Map( Explicit | Implicit );
- var versions = model.DeclaredApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- declared.Add( versions[j] );
- }
-
- versions = model.SupportedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- supported.Add( version );
- advertisedSupported.Add( version );
- }
-
- versions = model.DeprecatedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- deprecated.Add( version );
- advertisedDeprecated.Add( version );
- }
+ return DescriptionProvider.Describe( groupedMetadata, SunsetPolicyManager, Options );
}
- advertisedSupported.ExceptWith( declared );
- advertisedDeprecated.ExceptWith( declared );
- supported.ExceptWith( advertisedSupported );
- deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) );
-
- if ( supported.Count == 0 && deprecated.Count == 0 )
- {
- supported.Add( Options.DefaultApiVersion );
- }
+ return Array.Empty();
}
- private void AppendDescriptions( List descriptions, IEnumerable versions, bool deprecated )
+ private sealed class GroupedApiVersionMetadata :
+ ApiVersionMetadata,
+ IEquatable,
+ IGroupedApiVersionMetadata,
+ IGroupedApiVersionMetadataFactory
{
- foreach ( var version in versions )
- {
- var groupName = version.ToString( Options.GroupNameFormat, CurrentCulture );
- var sunsetPolicy = SunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default;
- descriptions.Add( new( version, groupName, deprecated, sunsetPolicy ) );
- }
- }
+ private GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata )
+ : base( metadata ) => GroupName = groupName;
- private sealed class ApiVersionDescriptionCollection(
- DefaultApiVersionDescriptionProvider provider,
- IEnumerable collators )
- {
- private readonly object syncRoot = new();
- private readonly DefaultApiVersionDescriptionProvider provider = provider;
- private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
- private IReadOnlyList? items;
- private int version;
-
- public IReadOnlyList Items
- {
- get
- {
- if ( items is not null && version == ComputeVersion() )
- {
- return items;
- }
+ public string? GroupName { get; }
- lock ( syncRoot )
- {
- var currentVersion = ComputeVersion();
+ static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New(
+ string? groupName,
+ ApiVersionMetadata metadata ) => new( groupName, metadata );
- if ( items is not null && version == currentVersion )
- {
- return items;
- }
+ public bool Equals( GroupedApiVersionMetadata? other ) =>
+ other is not null && other.GetHashCode() == GetHashCode();
- var context = new ApiVersionMetadataCollationContext();
+ public override bool Equals( object? obj ) =>
+ obj is not null &&
+ GetType().Equals( obj.GetType() ) &&
+ GetHashCode() == obj.GetHashCode();
- for ( var i = 0; i < collators.Length; i++ )
- {
- collators[i].Execute( context );
- }
-
- items = provider.Describe( context.Results );
- version = currentVersion;
- }
-
- return items;
- }
- }
-
- private int ComputeVersion() =>
- collators.Length switch
- {
- 0 => 0,
- 1 => collators[0].Version,
- _ => ComputeVersion( collators ),
- };
-
- private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers )
+ public override int GetHashCode()
{
var hash = default( HashCode );
- for ( var i = 0; i < providers.Length; i++ )
+ if ( !string.IsNullOrEmpty( GroupName ) )
{
- hash.Add( providers[i].Version );
+ hash.Add( GroupName, StringComparer.Ordinal );
}
+ hash.Add( base.GetHashCode() );
+
return hash.ToHashCode();
}
}
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/DependencyInjection/IApiVersioningBuilderExtensions.cs
index 1c1d2e63..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
@@ -52,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 ca31264b..294db52c 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/GroupedApiVersionDescriptionProvider.cs
@@ -2,10 +2,8 @@
namespace Asp.Versioning.ApiExplorer;
+using Asp.Versioning.ApiExplorer.Internal;
using Microsoft.Extensions.Options;
-using System.Buffers;
-using static Asp.Versioning.ApiVersionMapping;
-using static System.Globalization.CultureInfo;
///
/// Represents the default implementation of an object that discovers and describes the API version information within an application.
@@ -13,7 +11,7 @@ namespace Asp.Versioning.ApiExplorer;
[CLSCompliant( false )]
public class GroupedApiVersionDescriptionProvider : IApiVersionDescriptionProvider
{
- private readonly ApiVersionDescriptionCollection collection;
+ private readonly ApiVersionDescriptionCollection collection;
private readonly IOptions options;
///
@@ -29,7 +27,7 @@ public GroupedApiVersionDescriptionProvider(
ISunsetPolicyManager sunsetPolicyManager,
IOptions apiExplorerOptions )
{
- collection = new( this, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
+ collection = new( Describe, providers ?? throw new ArgumentNullException( nameof( providers ) ) );
SunsetPolicyManager = sunsetPolicyManager;
options = apiExplorerOptions;
}
@@ -59,191 +57,17 @@ public GroupedApiVersionDescriptionProvider(
protected virtual IReadOnlyList Describe( IReadOnlyList metadata )
{
ArgumentNullException.ThrowIfNull( metadata );
-
- var descriptions = new SortedSet( new ApiVersionDescriptionComparer() );
- var supported = new HashSet();
- var deprecated = new HashSet();
-
- BucketizeApiVersions( metadata, supported, deprecated );
- AppendDescriptions( descriptions, supported, deprecated: false );
- AppendDescriptions( descriptions, deprecated, deprecated: true );
-
- return descriptions.ToArray();
- }
-
- private void BucketizeApiVersions(
- IReadOnlyList list,
- ISet supported,
- ISet deprecated )
- {
- var declared = new HashSet();
- var advertisedSupported = new HashSet();
- var advertisedDeprecated = new HashSet();
-
- for ( var i = 0; i < list.Count; i++ )
- {
- var metadata = list[i];
- var groupName = metadata.GroupName;
- var model = metadata.Map( Explicit | Implicit );
- var versions = model.DeclaredApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- declared.Add( new( groupName, versions[j] ) );
- }
-
- versions = model.SupportedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- supported.Add( new( groupName, version ) );
- advertisedSupported.Add( new( groupName, version ) );
- }
-
- versions = model.DeprecatedApiVersions;
-
- for ( var j = 0; j < versions.Count; j++ )
- {
- var version = versions[j];
- deprecated.Add( new( groupName, version ) );
- advertisedDeprecated.Add( new( groupName, version ) );
- }
- }
-
- advertisedSupported.ExceptWith( declared );
- advertisedDeprecated.ExceptWith( declared );
- supported.ExceptWith( advertisedSupported );
- deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) );
-
- if ( supported.Count == 0 && deprecated.Count == 0 )
- {
- supported.Add( new( default, Options.DefaultApiVersion ) );
- }
- }
-
- private void AppendDescriptions(
- ICollection descriptions,
- IEnumerable versions,
- bool deprecated )
- {
- var format = Options.GroupNameFormat;
- var formatGroupName = Options.FormatGroupName;
-
- foreach ( var (groupName, version) in versions )
- {
- var formattedVersion = version.ToString( format, CurrentCulture );
- var formattedGroupName =
- string.IsNullOrEmpty( groupName ) || formatGroupName is null
- ? formattedVersion
- : formatGroupName( groupName, formattedVersion );
-
- var sunsetPolicy = SunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default;
- descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy ) );
- }
- }
-
- private sealed class ApiVersionDescriptionCollection(
- GroupedApiVersionDescriptionProvider provider,
- IEnumerable collators )
- {
- private readonly object syncRoot = new();
- private readonly GroupedApiVersionDescriptionProvider provider = provider;
- private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
- private IReadOnlyList? items;
- private int version;
-
- public IReadOnlyList Items
- {
- get
- {
- if ( items is not null && version == ComputeVersion() )
- {
- return items;
- }
-
- lock ( syncRoot )
- {
- var currentVersion = ComputeVersion();
-
- if ( items is not null && version == currentVersion )
- {
- return items;
- }
-
- var context = new ApiVersionMetadataCollationContext();
-
- for ( var i = 0; i < collators.Length; i++ )
- {
- collators[i].Execute( context );
- }
-
- var results = context.Results;
- var metadata = new GroupedApiVersionMetadata[results.Count];
-
- for ( var i = 0; i < metadata.Length; i++ )
- {
- metadata[i] = new( context.Results.GroupName( i ), results[i] );
- }
-
- items = provider.Describe( metadata );
- version = currentVersion;
- }
-
- return items;
- }
- }
-
- private int ComputeVersion() =>
- collators.Length switch
- {
- 0 => 0,
- 1 => collators[0].Version,
- _ => ComputeVersion( collators ),
- };
-
- private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers )
- {
- var hash = default( HashCode );
-
- for ( var i = 0; i < providers.Length; i++ )
- {
- hash.Add( providers[i].Version );
- }
-
- return hash.ToHashCode();
- }
- }
-
- private sealed class ApiVersionDescriptionComparer : IComparer
- {
- public int Compare( ApiVersionDescription? x, ApiVersionDescription? y )
- {
- if ( x is null )
- {
- return y is null ? 0 : -1;
- }
-
- if ( y is null )
- {
- return 1;
- }
-
- var result = x.ApiVersion.CompareTo( y.ApiVersion );
-
- if ( result == 0 )
- {
- result = StringComparer.Ordinal.Compare( x.GroupName, y.GroupName );
- }
-
- return result;
- }
+ return DescriptionProvider.Describe( metadata, SunsetPolicyManager, Options );
}
///
/// Represents the API version metadata applied to an endpoint with an optional group name.
///
- protected class GroupedApiVersionMetadata : ApiVersionMetadata, IEquatable
+ protected class GroupedApiVersionMetadata :
+ ApiVersionMetadata,
+ IEquatable,
+ IGroupedApiVersionMetadata,
+ IGroupedApiVersionMetadataFactory
{
///
/// Initializes a new instance of the class.
@@ -259,6 +83,10 @@ public GroupedApiVersionMetadata( string? groupName, ApiVersionMetadata metadata
/// The associated group name, if any.
public string? GroupName { get; }
+ static GroupedApiVersionMetadata IGroupedApiVersionMetadataFactory.New(
+ string? groupName,
+ ApiVersionMetadata metadata ) => new( groupName, metadata );
+
///
public bool Equals( GroupedApiVersionMetadata? other ) =>
other is not null && other.GetHashCode() == GetHashCode();
@@ -284,6 +112,4 @@ public override int GetHashCode()
return hash.ToHashCode();
}
}
-
- private record struct GroupedApiVersion( string? GroupName, ApiVersion ApiVersion );
}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs
new file mode 100644
index 00000000..f5847dd0
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionCollection.cs
@@ -0,0 +1,76 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal sealed class ApiVersionDescriptionCollection(
+ Func, IReadOnlyList> describe,
+ IEnumerable collators )
+ where T : IGroupedApiVersionMetadata, IGroupedApiVersionMetadataFactory
+{
+ private readonly object syncRoot = new();
+ private readonly Func, IReadOnlyList> describe = describe;
+ private readonly IApiVersionMetadataCollationProvider[] collators = collators.ToArray();
+ private IReadOnlyList? items;
+ private int version;
+
+ public IReadOnlyList Items
+ {
+ get
+ {
+ if ( items is not null && version == ComputeVersion() )
+ {
+ return items;
+ }
+
+ lock ( syncRoot )
+ {
+ var currentVersion = ComputeVersion();
+
+ if ( items is not null && version == currentVersion )
+ {
+ return items;
+ }
+
+ var context = new ApiVersionMetadataCollationContext();
+
+ for ( var i = 0; i < collators.Length; i++ )
+ {
+ collators[i].Execute( context );
+ }
+
+ var results = context.Results;
+ var metadata = new T[results.Count];
+
+ for ( var i = 0; i < metadata.Length; i++ )
+ {
+ metadata[i] = T.New( context.Results.GroupName( i ), results[i] );
+ }
+
+ items = describe( metadata );
+ version = currentVersion;
+ }
+
+ return items;
+ }
+ }
+
+ private int ComputeVersion() =>
+ collators.Length switch
+ {
+ 0 => 0,
+ 1 => collators[0].Version,
+ _ => ComputeVersion( collators ),
+ };
+
+ private static int ComputeVersion( IApiVersionMetadataCollationProvider[] providers )
+ {
+ var hash = default( HashCode );
+
+ for ( var i = 0; i < providers.Length; i++ )
+ {
+ hash.Add( providers[i].Version );
+ }
+
+ return hash.ToHashCode();
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs
new file mode 100644
index 00000000..3fb73385
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/ApiVersionDescriptionComparer.cs
@@ -0,0 +1,28 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal sealed class ApiVersionDescriptionComparer : IComparer
+{
+ public int Compare( ApiVersionDescription? x, ApiVersionDescription? y )
+ {
+ if ( x is null )
+ {
+ return y is null ? 0 : -1;
+ }
+
+ if ( y is null )
+ {
+ return 1;
+ }
+
+ var result = x.ApiVersion.CompareTo( y.ApiVersion );
+
+ if ( result == 0 )
+ {
+ result = StringComparer.Ordinal.Compare( x.GroupName, y.GroupName );
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs
new file mode 100644
index 00000000..ce3a0dbd
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/DescriptionProvider.cs
@@ -0,0 +1,107 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+using static Asp.Versioning.ApiVersionMapping;
+using static System.Globalization.CultureInfo;
+
+internal static class DescriptionProvider
+{
+ internal static ApiVersionDescription[] Describe(
+ IReadOnlyList metadata,
+ ISunsetPolicyManager sunsetPolicyManager,
+ ApiExplorerOptions options )
+ where T : IGroupedApiVersionMetadata, IEquatable
+ {
+ var descriptions = new SortedSet( new ApiVersionDescriptionComparer() );
+ var supported = new HashSet();
+ var deprecated = new HashSet();
+
+ BucketizeApiVersions( metadata, supported, deprecated, options );
+ AppendDescriptions( descriptions, supported, sunsetPolicyManager, options, deprecated: false );
+ AppendDescriptions( descriptions, deprecated, sunsetPolicyManager, options, deprecated: true );
+
+ return [.. descriptions];
+ }
+
+ private static void BucketizeApiVersions(
+ IReadOnlyList list,
+ HashSet supported,
+ HashSet deprecated,
+ ApiExplorerOptions options )
+ where T : IGroupedApiVersionMetadata
+ {
+ var declared = new HashSet();
+ var advertisedSupported = new HashSet();
+ var advertisedDeprecated = new HashSet();
+
+ for ( var i = 0; i < list.Count; i++ )
+ {
+ var metadata = list[i];
+ var groupName = metadata.GroupName;
+ var model = metadata.Map( Explicit | Implicit );
+ var versions = model.DeclaredApiVersions;
+
+ for ( var j = 0; j < versions.Count; j++ )
+ {
+ declared.Add( new( groupName, versions[j] ) );
+ }
+
+ versions = model.SupportedApiVersions;
+
+ for ( var j = 0; j < versions.Count; j++ )
+ {
+ var version = versions[j];
+ supported.Add( new( groupName, version ) );
+ advertisedSupported.Add( new( groupName, version ) );
+ }
+
+ versions = model.DeprecatedApiVersions;
+
+ for ( var j = 0; j < versions.Count; j++ )
+ {
+ var version = versions[j];
+ deprecated.Add( new( groupName, version ) );
+ advertisedDeprecated.Add( new( groupName, version ) );
+ }
+ }
+
+ advertisedSupported.ExceptWith( declared );
+ advertisedDeprecated.ExceptWith( declared );
+ supported.ExceptWith( advertisedSupported );
+ deprecated.ExceptWith( supported.Concat( advertisedDeprecated ) );
+
+ if ( supported.Count == 0 && deprecated.Count == 0 )
+ {
+ supported.Add( new( default, options.DefaultApiVersion ) );
+ }
+ }
+
+ private static void AppendDescriptions(
+ SortedSet descriptions,
+ HashSet versions,
+ ISunsetPolicyManager sunsetPolicyManager,
+ ApiExplorerOptions options,
+ bool deprecated )
+ {
+ var format = options.GroupNameFormat;
+ var formatGroupName = options.FormatGroupName;
+
+ foreach ( var (groupName, version) in versions )
+ {
+ var formattedGroupName = groupName;
+
+ if ( string.IsNullOrEmpty( formattedGroupName ) )
+ {
+ formattedGroupName = version.ToString( format, CurrentCulture );
+ }
+ else if ( formatGroupName is not null )
+ {
+ formattedGroupName = formatGroupName( formattedGroupName, version.ToString( format, CurrentCulture ) );
+ }
+
+ var sunsetPolicy = sunsetPolicyManager.TryGetPolicy( version, out var policy ) ? policy : default;
+ descriptions.Add( new( version, formattedGroupName, deprecated, sunsetPolicy ) );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs
new file mode 100644
index 00000000..8d276e60
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/GroupedApiVersion.cs
@@ -0,0 +1,5 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal record struct GroupedApiVersion( string? GroupName, ApiVersion ApiVersion );
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs
new file mode 100644
index 00000000..ec0c13e3
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadata.cs
@@ -0,0 +1,20 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal interface IGroupedApiVersionMetadata
+{
+ string? GroupName { get; }
+
+ string Name { get; }
+
+ bool IsApiVersionNeutral { get; }
+
+ ApiVersionModel Map( ApiVersionMapping mapping );
+
+ ApiVersionMapping MappingTo( ApiVersion? apiVersion );
+
+ bool IsMappedTo( ApiVersion? apiVersion );
+
+ void Deconstruct( out ApiVersionModel apiModel, out ApiVersionModel endpointModel );
+}
\ No newline at end of file
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs
new file mode 100644
index 00000000..ac9d885f
--- /dev/null
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc.ApiExplorer/Internal/IGroupedApiVersionMetadataFactory.cs
@@ -0,0 +1,9 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+
+namespace Asp.Versioning.ApiExplorer.Internal;
+
+internal interface IGroupedApiVersionMetadataFactory
+ where T : IGroupedApiVersionMetadata
+{
+ static abstract T New( string? groupName, ApiVersionMetadata metadata );
+}
\ No newline at end of file
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/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/Asp.Versioning.Mvc.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj
index 0871cc58..21ca9fe8 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Asp.Versioning.Mvc.csproj
@@ -1,14 +1,15 @@
- 8.0.0
- 8.0.0.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
+ true
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs
index 7796ca96..21e66163 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/DependencyInjection/IApiVersioningBuilderExtensions.cs
@@ -15,7 +15,8 @@ namespace Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System.Runtime.CompilerServices;
-using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
+using static ServiceDescriptor;
+using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes;
///
/// Provides ASP.NET Core MVC specific extension methods for .
@@ -57,9 +58,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() );
@@ -67,6 +68,7 @@ private static void AddServices( IServiceCollection services )
services.TryAddEnumerable( Transient() );
services.TryAddEnumerable( Singleton() );
services.Replace( WithUrlHelperFactoryDecorator( services ) );
+ services.TryReplace();
}
private static object CreateInstance( this IServiceProvider services, ServiceDescriptor descriptor )
@@ -84,6 +86,27 @@ private static object CreateInstance( this IServiceProvider services, ServiceDes
return ActivatorUtilities.GetServiceOrCreateInstance( services, descriptor.ImplementationType! );
}
+ private static void TryReplace<
+ TService,
+ TImplementation,
+ [DynamicallyAccessedMembers( NonPublicConstructors | PublicConstructors )]
+ TReplacement>( 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 )
{
diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs
index bbf03793..bd3e398e 100644
--- a/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs
+++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Mvc/Routing/ApiVersionUrlHelper.cs
@@ -98,9 +98,30 @@ public ApiVersionUrlHelper( ActionContext actionContext, IUrlHelper url )
return current;
}
- if ( current is not RouteValueDictionary values )
+ RouteValueDictionary values;
+
+ if ( current is null )
+ {
+ values = new() { { key, value } };
+ return values;
+ }
+
+ if ( current is RouteValueDictionary dictionary )
+ {
+ values = dictionary;
+ }
+ else if ( current is IEnumerable> kvps )
{
- values = current == null ? new() : new( current );
+ values = [];
+
+ foreach ( var kvp in kvps )
+ {
+ values.Add( kvp.Key, kvp.Value );
+ }
+ }
+ else
+ {
+ return current;
}
if ( !values.ContainsKey( key ) )
diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs
index ec26b099..8d2e90c7 100644
--- a/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs
+++ b/src/AspNetCore/WebApi/test/Asp.Versioning.Mvc.ApiExplorer.Tests/ApiVersionParameterDescriptionContextTest.cs
@@ -82,7 +82,7 @@ public void add_parameter_should_add_descriptor_for_path()
Name = "api-version",
RouteInfo = new()
{
- Constraints = new[] { new ApiVersionRouteConstraint() },
+ Constraints = [new ApiVersionRouteConstraint()],
},
Source = BindingSource.Path,
};
@@ -119,6 +119,55 @@ public void add_parameter_should_add_descriptor_for_path()
o => o.ExcludingMissingMembers() );
}
+ [Fact]
+ public void add_parameter_should_add_descriptor_for_path_when_version_neutral()
+ {
+ // arrange
+ var version = new ApiVersion( 1, 0 );
+ var description = new ApiDescription()
+ {
+ ActionDescriptor = new ActionDescriptor() { EndpointMetadata = [ApiVersionMetadata.Neutral] },
+ ParameterDescriptions =
+ {
+ new()
+ {
+ Name = "api-version",
+ RouteInfo = new() { Constraints = [new ApiVersionRouteConstraint()] },
+ Source = BindingSource.Path,
+ },
+ },
+ };
+ var modelMetadata = new Mock( ModelMetadataIdentity.ForType( typeof( string ) ) ).Object;
+ var options = new ApiExplorerOptions()
+ {
+ DefaultApiVersion = version,
+ ApiVersionParameterSource = new UrlSegmentApiVersionReader(),
+ };
+ var context = new ApiVersionParameterDescriptionContext( description, version, modelMetadata, options );
+
+ // act
+ context.AddParameter( "api-version", Path );
+
+ // assert
+ description.ParameterDescriptions.Single().Should().BeEquivalentTo(
+ new
+ {
+ Name = "api-version",
+ ModelMetadata = modelMetadata,
+ Source = BindingSource.Path,
+ DefaultValue = (object) "1.0",
+ IsRequired = true,
+ RouteInfo = new ApiParameterRouteInfo()
+ {
+ DefaultValue = "1.0",
+ IsOptional = false,
+ Constraints = description.ParameterDescriptions[0].RouteInfo.Constraints,
+ },
+ Type = typeof( string ),
+ },
+ o => o.ExcludingMissingMembers() );
+ }
+
[Fact]
public void add_parameter_should_remove_other_descriptors_after_path_parameter_is_added()
{
@@ -128,7 +177,7 @@ public void add_parameter_should_remove_other_descriptors_after_path_parameter_i
Name = "api-version",
RouteInfo = new()
{
- Constraints = new[] { new ApiVersionRouteConstraint() },
+ Constraints = [new ApiVersionRouteConstraint()],
},
Source = BindingSource.Path,
};
@@ -179,7 +228,7 @@ public void add_parameter_should_not_add_query_parameter_after_path_parameter_ha
Name = "api-version",
RouteInfo = new()
{
- Constraints = new[] { new ApiVersionRouteConstraint() },
+ Constraints = [new ApiVersionRouteConstraint()],
},
Source = BindingSource.Path,
};
@@ -214,7 +263,7 @@ public void add_parameter_should_add_descriptor_for_media_type_parameter()
var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( version ) );
var description = new ApiDescription()
{
- ActionDescriptor = new() { EndpointMetadata = new[] { metadata } },
+ ActionDescriptor = new() { EndpointMetadata = [metadata] },
SupportedRequestFormats = { new() { MediaType = Json } },
SupportedResponseTypes = { new() { ApiResponseFormats = { new() { MediaType = Json } } } },
};
@@ -304,7 +353,7 @@ public void add_parameter_should_make_parameters_optional_after_first_parameter(
private static ApiDescription NewApiDescription( ApiVersion apiVersion, params ApiParameterDescription[] parameters )
{
var metadata = new ApiVersionMetadata( ApiVersionModel.Empty, new ApiVersionModel( apiVersion ) );
- var action = new ActionDescriptor() { EndpointMetadata = new[] { metadata } };
+ var action = new ActionDescriptor() { EndpointMetadata = [metadata] };
var description = new ApiDescription() { ActionDescriptor = action };
for ( var i = 0; i < parameters.Length; i++ )
diff --git a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs
index 49139052..6f940aee 100644
--- a/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs
+++ b/src/Client/src/Asp.Versioning.Http.Client/ApiVersionEnumerator.cs
@@ -5,14 +5,20 @@
namespace Asp.Versioning.Http;
+#if NET
+using System.Buffers;
+#endif
using System.Collections;
+#if NET
+using static System.StringSplitOptions;
+#endif
///
/// Represents an enumerator of API versions from a HTTP header.
///
public readonly struct ApiVersionEnumerator : IEnumerable
{
- private readonly IEnumerable values;
+ private readonly string[] values;
private readonly IApiVersionParser parser;
///
@@ -29,37 +35,72 @@ public ApiVersionEnumerator(
ArgumentNullException.ThrowIfNull( response );
ArgumentException.ThrowIfNullOrEmpty( headerName );
- this.values =
- response.Headers.TryGetValues( headerName, out var values )
- ? values
- : Enumerable.Empty();
-
+ this.values = response.Headers.TryGetValues( headerName, out var values ) ? values.ToArray() : [];
this.parser = parser ?? ApiVersionParser.Default;
}
///
public IEnumerator GetEnumerator()
{
- using var iterator = values.GetEnumerator();
+#if NETSTANDARD
+ for ( var i = 0; i < values.Length; i++ )
+ {
+ var items = values[i].Split( ',' );
- if ( !iterator.MoveNext() )
+ for ( var j = 0; j < items.Length; j++ )
+ {
+ var item = items[j].Trim();
+
+ if ( item.Length > 0 && parser.TryParse( item, out var result ) )
+ {
+ yield return result!;
+ }
+ }
+ }
+#else
+ for ( var i = 0; i < values.Length; i++ )
{
- yield break;
+ var (count, versions) = ParseVersions( values[i] );
+
+ for ( var j = 0; j < count; j++ )
+ {
+ yield return versions[j];
+ }
}
+#endif
+ }
- if ( parser.TryParse( iterator.Current, out var value ) )
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+#if NET
+ private (int Count, ApiVersion[] Results) ParseVersions( ReadOnlySpan value )
+ {
+ var pool = ArrayPool.Shared;
+ var ranges = pool.Rent( 5 );
+ var length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries );
+
+ while ( length >= ranges.Length )
{
- yield return value!;
+ pool.Return( ranges );
+ length <<= 1;
+ ranges = pool.Rent( length );
+ length = value.Split( ranges, ',', RemoveEmptyEntries | TrimEntries );
}
- while ( iterator.MoveNext() )
+ var results = new ApiVersion[length];
+ var count = 0;
+
+ for ( var i = 0; i < length; i++ )
{
- if ( parser.TryParse( iterator.Current, out value ) )
+ var text = value[ranges[i]];
+
+ if ( text.Length > 0 && parser.TryParse( text, out var result ) )
{
- yield return value!;
+ results[count++] = result;
}
}
- }
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ pool.Return( ranges );
+ return (count, results);
+ }
+#endif
}
\ No newline at end of file
diff --git a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj
index 75416005..08fc140f 100644
--- a/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj
+++ b/src/Client/src/Asp.Versioning.Http.Client/Asp.Versioning.Http.Client.csproj
@@ -1,8 +1,8 @@
- 8.0.0
- 8.0.0.0
+ 8.1.0
+ 8.1.0.0
$(DefaultTargetFramework);netstandard1.1;netstandard2.0
Asp.Versioning.Http
API Versioning Client Extensions
@@ -14,6 +14,10 @@
true
+
+ false
+
+
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/Common/src/Common.Backport/Common.Backport.msbuildproj b/src/Common/src/Common.Backport/Common.Backport.msbuildproj
index 9dd66e3a..0da069a9 100644
--- a/src/Common/src/Common.Backport/Common.Backport.msbuildproj
+++ b/src/Common/src/Common.Backport/Common.Backport.msbuildproj
@@ -1,6 +1,7 @@
netstandard1.0
+ $(NoWarn);NETSDK1215
diff --git a/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs b/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs
index 98cb8aa3..63464548 100644
--- a/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs
+++ b/src/Common/src/Common.Mvc/Conventions/ActionConventionBuilderExtensions.cs
@@ -73,6 +73,9 @@ public static IActionConventionBuilder Action
/// If there is only one corresponding match found, then the argument types are ignored;
/// otherwise, the argument types are used for method overload resolution. Action
/// methods that have the applied will also be ignored.
+#if !NETFRAMEWORK
+ [UnconditionalSuppressMessage( "ILLink", "IL2072", Justification = "Controller types are never trimmed" )]
+#endif
public static IActionConventionBuilder Action( this IActionConventionBuilder builder, string methodName, params Type[] argumentTypes )
{
ArgumentNullException.ThrowIfNull( builder );
diff --git a/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs b/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs
index 3c0fa420..8feb6bfd 100644
--- a/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs
+++ b/src/Common/src/Common.Mvc/Conventions/ActionMethodResolver.cs
@@ -10,12 +10,19 @@ namespace Asp.Versioning.Conventions;
#if NETFRAMEWORK
using System.Web.Http;
#endif
+using static System.Reflection.BindingFlags;
internal static class ActionMethodResolver
{
- internal static MethodInfo Resolve( Type controllerType, string methodName, Type[] argumentTypes )
+ internal static MethodInfo Resolve(
+#if !NETFRAMEWORK
+ [DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.PublicMethods )]
+#endif
+ Type controllerType,
+ string methodName,
+ Type[] argumentTypes )
{
- var methods = controllerType.GetRuntimeMethods().Where( m => m.Name == methodName && IsAction( m ) ).ToArray();
+ var methods = controllerType.GetMethods( Instance | Public ).Where( m => m.Name == methodName && IsAction( m ) ).ToArray();
switch ( methods.Length )
{
diff --git a/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs b/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs
index 328b8b70..47e7a491 100644
--- a/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs
+++ b/src/Common/src/Common.Mvc/Conventions/ControllerConventionBuilderExtensions.cs
@@ -73,6 +73,9 @@ public static IActionConventionBuilder Action
/// If there is only one corresponding match found, then the argument types are ignored;
/// otherwise, the argument types are used for method overload resolution. Action
/// methods that have the applied will also be ignored.
+#if !NETFRAMEWORK
+ [UnconditionalSuppressMessage( "ILLink", "IL2072", Justification = "Controller types are never trimmed" )]
+#endif
public static IActionConventionBuilder Action( this IControllerConventionBuilder builder, string methodName, params Type[] argumentTypes )
{
ArgumentNullException.ThrowIfNull( builder );
diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs
index bcd941a0..33ab2471 100644
--- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs
+++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataActionQueryOptionsConventionBuilderExtensions.cs
@@ -16,6 +16,7 @@ namespace Asp.Versioning.Conventions;
using System.Web.Http;
using System.Web.Http.Controllers;
#endif
+using static System.Reflection.BindingFlags;
///
/// Provides extension methods for the
@@ -187,6 +188,9 @@ public static ODataActionQueryOptionsConventionBuilder Actionargument types are ignored;
/// otherwise, the argument types are used for method overload resolution. Action
/// methods that have the applied will also be ignored.
+#if !NETFRAMEWORK
+ [UnconditionalSuppressMessage( "ILLink", "IL2075", Justification = "Controller types and actions are never trimmed" )]
+#endif
public static ODataActionQueryOptionsConventionBuilder Action(
this IODataActionQueryOptionsConventionBuilder builder,
string methodName,
@@ -197,7 +201,7 @@ public static ODataActionQueryOptionsConventionBuilder Action(
string message;
var methods = builder.ControllerType
- .GetRuntimeMethods()
+ .GetMethods( Instance | Public )
.Where( m => m.Name == methodName && IsAction( m ) )
.ToArray();
diff --git a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs
index 7cb50224..fd973fe6 100644
--- a/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs
+++ b/src/Common/src/Common.OData.ApiExplorer/Conventions/ODataAttributeVisitor.cs
@@ -76,11 +76,11 @@ private void VisitModel( IEdmStructuredType modelType )
VisitMaxTop( querySettings );
}
- private void VisitEnableQuery( IReadOnlyList attributes )
+ private void VisitEnableQuery( EnableQueryAttribute[] attributes )
{
var @default = new EnableQueryAttribute();
- for ( var i = 0; i < attributes.Count; i++ )
+ for ( var i = 0; i < attributes.Length; i++ )
{
var attribute = attributes[i];
diff --git a/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs
index 16ef9c9c..f9c60670 100644
--- a/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs
+++ b/src/Common/src/Common.OData.ApiExplorer/Microsoft.OData.Edm/EdmExtensions.cs
@@ -37,6 +37,9 @@ internal static class EdmExtensions
return null;
}
+#if !NETFRAMEWORK
+ [UnconditionalSuppressMessage( "ILLink", "IL2057", Justification = "The types being referenced are well-known and will not be trimmed." )]
+#endif
private static Type? DeriveFromWellKnowPrimitive( string edmFullName ) => edmFullName switch
{
"Edm.String" or "Edm.Byte" or "Edm.SByte" or "Edm.Int16" or "Edm.Int32" or "Edm.Int64" or
diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs b/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs
index 98784a2f..f5959f92 100644
--- a/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs
+++ b/src/Common/src/Common.OData.ApiExplorer/OData/ClassProperty.cs
@@ -21,6 +21,9 @@ internal ClassProperty( PropertyInfo clrProperty, Type propertyType )
Attributes = clrProperty.DeclaredAttributes().ToArray();
}
+#if !NETFRAMEWORK
+ [UnconditionalSuppressMessage( "ILLink", "IL2072" )]
+#endif
internal ClassProperty( IEdmOperationParameter parameter, TypeSubstitutionContext context )
{
Name = parameter.Name;
diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs
index ce5a1010..76fec0ff 100644
--- a/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs
+++ b/src/Common/src/Common.OData.ApiExplorer/OData/DefaultModelTypeBuilder.cs
@@ -14,6 +14,9 @@ namespace Asp.Versioning.OData;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;
+#if !NETFRAMEWORK
+using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes;
+#endif
using static System.Globalization.CultureInfo;
using static System.Guid;
using static System.Reflection.BindingFlags;
@@ -22,6 +25,11 @@ namespace Asp.Versioning.OData;
///
/// Represents the default model type builder.
///
+#if !NETFRAMEWORK
+[UnconditionalSuppressMessage( "ILLink", "IL2055")]
+[UnconditionalSuppressMessage( "ILLink", "IL2070")]
+[UnconditionalSuppressMessage( "ILLink", "IL2073")]
+#endif
public sealed class DefaultModelTypeBuilder : IModelTypeBuilder
{
/* design: there is typically a 1:1 relationship between an edm and api version. odata model bound settings
@@ -59,7 +67,17 @@ private DefaultModelTypeBuilder( bool excludeAdHocModels, bool adHoc )
public DefaultModelTypeBuilder( bool includeAdHocModels = false ) => excludeAdHocModels = !includeAdHocModels;
///
- public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion )
+#if !NETFRAMEWORK
+ [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
+ public Type NewStructuredType(
+ IEdmModel model,
+ IEdmStructuredType structuredType,
+#if !NETFRAMEWORK
+ [DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
+ Type clrType,
+ ApiVersion apiVersion )
{
ArgumentNullException.ThrowIfNull( model );
@@ -88,6 +106,9 @@ public Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredTyp
}
///
+#if !NETFRAMEWORK
+ [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
public Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion )
{
ArgumentNullException.ThrowIfNull( model );
@@ -417,7 +438,7 @@ private static PropertyBuilder AddProperty(
var field = addTo.DefineField( "_" + name, shouldBeAdded, FieldAttributes.Private );
var propertyBuilder = addTo.DefineProperty( name, PropertyAttributes.HasDefault, shouldBeAdded, null );
var getter = addTo.DefineMethod( "get_" + name, propertyMethodAttributes, shouldBeAdded, Type.EmptyTypes );
- var setter = addTo.DefineMethod( "set_" + name, propertyMethodAttributes, shouldBeAdded, Type.EmptyTypes );
+ var setter = addTo.DefineMethod( "set_" + name, propertyMethodAttributes, null, [shouldBeAdded] );
var il = getter.GetILGenerator();
il.Emit( OpCodes.Ldarg_0 );
diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs b/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs
index 96b80e9f..cfc73a43 100644
--- a/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs
+++ b/src/Common/src/Common.OData.ApiExplorer/OData/IModelTypeBuilder.cs
@@ -8,6 +8,9 @@ namespace Asp.Versioning.OData;
using Microsoft.AspNetCore.OData.Formatter;
#endif
using Microsoft.OData.Edm;
+#if !NETFRAMEWORK
+using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes;
+#endif
///
/// Defines the behavior of a model type builder.
@@ -26,7 +29,17 @@ public interface IModelTypeBuilder
/// structured type .
/// If a substitution is not required, the original CLR type is returned. When a substitution
/// type is generated, it is performed only once per API version .
- Type NewStructuredType( IEdmModel model, IEdmStructuredType structuredType, Type clrType, ApiVersion apiVersion );
+#if !NETFRAMEWORK
+ [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
+ Type NewStructuredType(
+ IEdmModel model,
+ IEdmStructuredType structuredType,
+#if !NETFRAMEWORK
+ [DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
+ Type clrType,
+ ApiVersion apiVersion );
///
/// Creates an returns a strongly-typed definition for OData action parameters.
@@ -39,5 +52,8 @@ public interface IModelTypeBuilder
/// OData action parameters are modeled as a dictionary ,
/// which is difficult to use effectively by documentation tools such as the API Explorer. The corresponding type is generated only once per
/// API version .
+#if !NETFRAMEWORK
+ [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
Type NewActionParameters( IEdmModel model, IEdmAction action, string controllerName, ApiVersion apiVersion );
}
\ No newline at end of file
diff --git a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs
index a32cd16a..72c76457 100644
--- a/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs
+++ b/src/Common/src/Common.OData.ApiExplorer/OData/TypeExtensions.cs
@@ -16,6 +16,8 @@ namespace Asp.Versioning.OData;
using System.Runtime.CompilerServices;
#if NETFRAMEWORK
using IActionResult = System.Web.Http.IHttpActionResult;
+#else
+using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes;
#endif
///
@@ -41,7 +43,17 @@ public static partial class TypeExtensions
/// The current type substitution context .
/// The original or a substitution type based on the
/// provided .
- public static Type SubstituteIfNecessary( this Type type, TypeSubstitutionContext context )
+#if !NETFRAMEWORK
+ [UnconditionalSuppressMessage( "ILLink", "IL2026" )]
+ [UnconditionalSuppressMessage( "ILLink", "IL2073" )]
+ [return: DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
+ public static Type SubstituteIfNecessary(
+#if !NETFRAMEWORK
+ [DynamicallyAccessedMembers( Interfaces | PublicProperties )]
+#endif
+ this Type type,
+ TypeSubstitutionContext context )
{
ArgumentNullException.ThrowIfNull( type );
ArgumentNullException.ThrowIfNull( context );
@@ -151,7 +163,13 @@ internal static Type ExtractInnerType( this Type type )
return type;
}
- private static bool IsSubstitutableGeneric( Type type, Stack openTypes, out Type? innerType )
+ private static bool IsSubstitutableGeneric(
+#if !NETFRAMEWORK
+ [DynamicallyAccessedMembers( Interfaces )]
+#endif
+ Type type,
+ Stack openTypes,
+ out Type? innerType )
{
innerType = default;
@@ -212,6 +230,10 @@ private static bool IsSubstitutableGeneric( Type type, Stack openTypes, ou
return true;
}
+#if !NETFRAMEWORK
+ [RequiresDynamicCode( "Might not be available at runtime" )]
+ [RequiresUnreferencedCode( "Cannot be validated by trim analysis" )]
+#endif
private static Type CloseGeneric( Stack openTypes, Type innerType )
{
var type = openTypes.Pop();
@@ -241,7 +263,10 @@ private static bool CanBeSubstituted( Type type ) =>
#endif
!type.IsODataActionParameters();
- internal static bool IsEnumerable( this Type type, [NotNullWhen( true )] out Type? itemType )
+#if !NETFRAMEWORK
+ [UnconditionalSuppressMessage( "ILLink", "IL2070" )]
+#endif
+ internal static bool IsEnumerable(this Type type, [NotNullWhen( true )] out Type? itemType )
{
var types = new Queue();
diff --git a/src/Common/src/Common/ApiVersioningOptions.cs b/src/Common/src/Common/ApiVersioningOptions.cs
index 82c1cfce..75f22711 100644
--- a/src/Common/src/Common/ApiVersioningOptions.cs
+++ b/src/Common/src/Common/ApiVersioningOptions.cs
@@ -59,14 +59,16 @@ public partial class ApiVersioningOptions
///
/// Gets or sets the API version reader.
///
- /// An API version reader object. The default value
- /// is an instance of the .
- /// The API version reader is used to read the
- /// API version specified by a client. The default value is the
- /// , which only reads the API version from
- /// the "api-version" query string parameter. Replace the default value with an alternate
- /// implementation, such as the , which
- /// can read the API version from additional information like HTTP headers.
+ /// An API version reader object. The default value is a combined reader
+ /// with both and .
+ ///
+ /// The API version reader is used to read the API version specified by a
+ /// client. The default value consist of both and
+ /// , which reads the API version from the "api-version" query string
+ /// parameter and a path segment in the request URL respectively.
+ /// Replace the default value with an alternate implementation, such as the ,
+ /// which can read the API version from additional information like HTTP headers.
+ ///
#if !NETFRAMEWORK
[CLSCompliant( false )]
#endif
diff --git a/src/Common/src/Common/DefaultApiVersionReporter.cs b/src/Common/src/Common/DefaultApiVersionReporter.cs
index 95e4d017..72ed1408 100644
--- a/src/Common/src/Common/DefaultApiVersionReporter.cs
+++ b/src/Common/src/Common/DefaultApiVersionReporter.cs
@@ -91,9 +91,7 @@ public void Report( HttpResponse response, ApiVersionModel apiVersionModel )
#endif
var name = metadata.Name;
- if ( sunsetPolicyManager.TryGetPolicy( name, version, out var policy ) ||
- ( !string.IsNullOrEmpty( name ) && sunsetPolicyManager.TryGetPolicy( name, out policy ) ) ||
- ( version != null && sunsetPolicyManager.TryGetPolicy( version, out policy ) ) )
+ if ( sunsetPolicyManager.TryResolvePolicy( name, version, out var policy ) )
{
response.WriteSunsetPolicy( policy );
}
diff --git a/src/Common/src/Common/HeaderApiVersionReader.cs b/src/Common/src/Common/HeaderApiVersionReader.cs
index 2bffbf31..e5bea98c 100644
--- a/src/Common/src/Common/HeaderApiVersionReader.cs
+++ b/src/Common/src/Common/HeaderApiVersionReader.cs
@@ -15,6 +15,10 @@ public partial class HeaderApiVersionReader : IApiVersionReader
///
/// Initializes a new instance of the class.
///
+ ///
+ /// There is no default header. Either initialize headers using the collection property,
+ /// or call a constructor that accepts header names. Otherwise, this reader has no effect.
+ ///
public HeaderApiVersionReader() { }
///
diff --git a/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs b/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs
index 43ffe5b9..20418181 100644
--- a/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs
+++ b/src/Common/test/Common.Acceptance.Tests/HttpContentExtensions.cs
@@ -6,20 +6,30 @@ namespace Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
#endif
using System.Net.Http;
+#if NETFRAMEWORK
using System.Net.Http.Formatting;
+#else
+using System.Net.Http.Json;
+#endif
internal static class HttpContentExtensions
{
+#if NETFRAMEWORK
private static readonly JsonMediaTypeFormatter ProblemDetailsMediaTypeFormatter = new()
{
SupportedMediaTypes = { new( ProblemDetailsDefaults.MediaType.Json ) },
};
private static readonly IEnumerable MediaTypeFormatters = new[] { ProblemDetailsMediaTypeFormatter };
+#endif
public static Task ReadAsProblemDetailsAsync(
this HttpContent content,
CancellationToken cancellationToken = default ) =>
+#if NETFRAMEWORK
content.ReadAsAsync( MediaTypeFormatters, cancellationToken );
+#else
+ content.ReadFromJsonAsync( cancellationToken );
+#endif
#pragma warning disable IDE0060 // Remove unused parameter
#pragma warning disable IDE0079 // Remove unnecessary suppression
diff --git a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs
index 100af41f..d5295031 100644
--- a/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs
+++ b/src/Common/test/Common.OData.ApiExplorer.Tests/OData/DefaultModelTypeBuilderTest.cs
@@ -409,6 +409,42 @@ public void substitute_should_resolve_types_that_reference_a_model_that_match_th
substitutionType.Should().NotBeOfType();
}
+ [Fact]
+ public void ignoring_property_should_force_substitution_with_valid_runtime_properties()
+ {
+ // arrange
+ var modelBuilder = new ODataConventionModelBuilder();
+ var address = modelBuilder.EntitySet( nameof( Address ) ).EntityType;
+
+ address.Ignore( x => x.City );
+
+ var context = NewContext( modelBuilder.GetEdmModel() );
+ var addressType = typeof( Address );
+
+ // act
+ var substitutedType = addressType.SubstituteIfNecessary( context );
+
+ // assert
+ substitutedType.Should().NotBe( addressType );
+#if NET452
+ substitutedType.GetRuntimeProperties().Should().HaveCount( 5 );
+
+ foreach ( var substitutedProperty in substitutedType.GetRuntimeProperties() )
+ {
+ substitutedProperty.Should().NotBeNull();
+ substitutedProperty.GetSetMethod( true ).Should().NotBeNull()
+ .And.Match( p => p.ReturnType == typeof( void ) )
+ .And.Match( p => p.GetParameters().Length == 1 );
+ }
+#else
+ substitutedType.GetRuntimeProperties().Should().HaveCount( 5 )
+ .And.AllSatisfy( prop => prop.GetSetMethod( true ).Should()
+ .NotBeNull()
+ .And.ReturnVoid()
+ .And.Match( setter => setter.GetParameters().Length == 1 ) );
+#endif
+ }
+
public static IEnumerable SubstitutionNotRequiredData
{
get