diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx
index 04c49b3..9d42262 100644
--- a/.docfx/Dockerfile.docfx
+++ b/.docfx/Dockerfile.docfx
@@ -1,4 +1,4 @@
-ARG NGINX_VERSION=1.30.0-alpine
+ARG NGINX_VERSION=1.31.0-alpine
FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base
RUN rm -rf /usr/share/nginx/html/*
diff --git a/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt
index 9352e9a..6165b8c 100644
--- a/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.Extensions.Asp.Versioning/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 10.0.7
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 10.0.6
Availability: .NET 10 and .NET 9
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 15c351c..229ae8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,15 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba
> [!NOTE]
> Changelog entries prior to version 8.4.0 was migrated from previous versions of Cuemon.Extensions.Asp.Versioning.
+## [10.0.7] - 2026-05-23
+
+This is a service update that focuses on package dependencies and explicit dual-framework support through conditionally-targeted Asp.Versioning package versions, ensuring compatibility with .NET 9 (Asp.Versioning 8.1.1) and .NET 10 (Asp.Versioning 10.0.0).
+
+### Changed
+
+- `Directory.Packages.props` to conditionally target Asp.Versioning package versions: version 8.1.1 for .NET 9 target framework and version 10.0.0 for .NET 10 target framework, providing version-appropriate behavior for each framework,
+- Dependencies upgraded to the latest compatible versions for all supported target frameworks (.NET 10 and .NET 9).
+
## [10.0.6] - 2026-04-18
This is a service update that focuses on package dependencies.
@@ -120,7 +129,9 @@ This major release is first and foremost focused on ironing out any wrinkles tha
- RestfulApiVersionReader class in the Codebelt.Extensions.Asp.Versioning namespace that represents a RESTful API version reader that reads the value from a filtered list of HTTP Accept headers in the request
- RestfulProblemDetailsFactory class in the Codebelt.Extensions.Asp.Versioning namespace that represents a RESTful implementation of the IProblemDetailsFactory which throws variants of HttpStatusCodeException that needs to be translated accordingly
-[Unreleased]: https://github.com/codebeltnet/asp-versioning/compare/v10.0.5...HEAD
+[Unreleased]: https://github.com/codebeltnet/asp-versioning/compare/v10.0.7...HEAD
+[10.0.7]: https://github.com/codebeltnet/asp-versioning/compare/v10.0.6...v10.0.7
+[10.0.6]: https://github.com/codebeltnet/asp-versioning/compare/v10.0.5...v10.0.6
[10.0.5]: https://github.com/codebeltnet/asp-versioning/compare/v10.0.4...v10.0.5
[10.0.4]: https://github.com/codebeltnet/asp-versioning/compare/v10.0.3...v10.0.4
[10.0.3]: https://github.com/codebeltnet/asp-versioning/compare/v10.0.2...v10.0.3
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 52de1f8..4e49a37 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,23 +3,31 @@
true
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Codebelt.Extensions.Asp.Versioning.Tests/ApplicationBuilderExtensionsTest.cs b/test/Codebelt.Extensions.Asp.Versioning.Tests/ApplicationBuilderExtensionsTest.cs
new file mode 100644
index 0000000..c8572ec
--- /dev/null
+++ b/test/Codebelt.Extensions.Asp.Versioning.Tests/ApplicationBuilderExtensionsTest.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Threading.Tasks;
+using Codebelt.Extensions.Asp.Versioning.Assets;
+using Codebelt.Extensions.Xunit;
+using Codebelt.Extensions.Xunit.Hosting.AspNetCore;
+using Cuemon.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Codebelt.Extensions.Asp.Versioning
+{
+ public class ApplicationBuilderExtensionsTest : Test
+ {
+ public ApplicationBuilderExtensionsTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ private static void ConfigureServices(IServiceCollection services)
+ {
+ // Use minimal controller registration.
+ // [ApiController]'s ClientErrorResultFilter automatically converts StatusCode(4xx) results
+ // to problem details (sets application/problem+json content-type), which prevents
+ // UseStatusCodePages from firing (it requires content-type to be null).
+ // The minimal-API lambda endpoints added in ConfigureApp bypass [ApiController] filters.
+ services.AddControllers().AddApplicationPart(typeof(FakeController).Assembly);
+ }
+
+ private static void ConfigureApp(IApplicationBuilder app, Func factory = null)
+ {
+ if (factory != null)
+ {
+ app.UseRestfulApiVersioning(factory);
+ }
+ else
+ {
+ app.UseRestfulApiVersioning();
+ }
+
+ app.UseRouting();
+ app.UseEndpoints(endpoints =>
+ {
+ // Minimal-API lambda endpoints: just set the status code with no content-type.
+ // These bypass [ApiController]'s ClientErrorResultFilter so UseStatusCodePages can fire.
+ endpoints.MapGet("/test/not-acceptable", ctx =>
+ {
+ ctx.Response.StatusCode = StatusCodes.Status406NotAcceptable;
+ return Task.CompletedTask;
+ });
+ endpoints.MapGet("/test/teapot", ctx =>
+ {
+ ctx.Response.StatusCode = StatusCodes.Status418ImATeapot;
+ return Task.CompletedTask;
+ });
+ });
+ }
+
+ [Fact]
+ public async Task UseRestfulApiVersioning_WithCustomFactory_ShouldInvokeProvidedFactory()
+ {
+ var customFactoryInvoked = false;
+
+ using (var app = WebHostTestFactory.Create(ConfigureServices, appBuilder =>
+ {
+ ConfigureApp(appBuilder, context =>
+ {
+ customFactoryInvoked = true;
+ return new NotAcceptableException("Custom factory: version not acceptable");
+ });
+ }))
+ {
+ var client = app.Host.GetTestClient();
+
+ // /test/not-acceptable is a minimal-API endpoint that sets 406 with no content-type.
+ // UseStatusCodePages fires; the custom factory (non-null ??= branch) is invoked.
+ var ex = await Assert.ThrowsAsync(() => client.GetAsync("/test/not-acceptable"));
+
+ Assert.True(customFactoryInvoked);
+ Assert.StartsWith("Custom factory:", ex.Message);
+ }
+ }
+
+ [Fact]
+ public async Task UseRestfulApiVersioning_DefaultFactory_WithMappedStatusCode_ShouldThrowCorrespondingException()
+ {
+ using (var app = WebHostTestFactory.Create(ConfigureServices, app => ConfigureApp(app)))
+ {
+ var client = app.Host.GetTestClient();
+
+ // /test/not-acceptable returns 406 with no content-type.
+ // UseStatusCodePages fires; default factory invoked with Response.StatusCode=406.
+ // TryParse(406) succeeds -> NotAcceptableException is returned and thrown.
+ await Assert.ThrowsAsync(() => client.GetAsync("/test/not-acceptable"));
+ }
+ }
+
+ [Fact]
+ public async Task UseRestfulApiVersioning_DefaultFactory_WithUnmappedStatusCode_ShouldThrowInternalServerErrorException()
+ {
+ using (var app = WebHostTestFactory.Create(ConfigureServices, app => ConfigureApp(app)))
+ {
+ var client = app.Host.GetTestClient();
+
+ // /test/teapot returns 418 with no content-type.
+ // UseStatusCodePages fires; default factory invoked with Response.StatusCode=418.
+ // TryParse(418) fails (418 is not in Cuemon's mapped codes) -> InternalServerErrorException.
+ await Assert.ThrowsAsync(() => client.GetAsync("/test/teapot"));
+ }
+ }
+ }
+}
diff --git a/test/Codebelt.Extensions.Asp.Versioning.Tests/Assets/FakeController.cs b/test/Codebelt.Extensions.Asp.Versioning.Tests/Assets/FakeController.cs
index 5cf8010..766113c 100644
--- a/test/Codebelt.Extensions.Asp.Versioning.Tests/Assets/FakeController.cs
+++ b/test/Codebelt.Extensions.Asp.Versioning.Tests/Assets/FakeController.cs
@@ -1,6 +1,9 @@
-using Cuemon.AspNetCore.Http;
+using System.Threading.Tasks;
+using Cuemon.AspNetCore.Http;
using Cuemon.AspNetCore.Mvc.Filters.Diagnostics;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Options;
namespace Codebelt.Extensions.Asp.Versioning.Assets
@@ -34,5 +37,35 @@ public IActionResult GetException()
{
throw new GoneException();
}
+
+ [HttpGet]
+ [Route("not-acceptable")]
+ public IActionResult GetNotAcceptable()
+ {
+ return StatusCode(StatusCodes.Status406NotAcceptable);
+ }
+
+ [HttpGet]
+ [Route("teapot")]
+ public IActionResult GetTeapot()
+ {
+ return StatusCode(StatusCodes.Status418ImATeapot);
+ }
+
+ [HttpGet]
+ [Route("problem418")]
+ public async Task GetProblem418([FromServices] IProblemDetailsService problemDetailsService)
+ {
+ await problemDetailsService.WriteAsync(new ProblemDetailsContext
+ {
+ HttpContext = HttpContext,
+ ProblemDetails = new Microsoft.AspNetCore.Mvc.ProblemDetails
+ {
+ Status = StatusCodes.Status418ImATeapot,
+ Detail = "I'm a teapot - an unmapped status code"
+ }
+ });
+ return new EmptyResult();
+ }
}
}
diff --git a/test/Codebelt.Extensions.Asp.Versioning.Tests/RestfulApiVersioningOptionsTest.cs b/test/Codebelt.Extensions.Asp.Versioning.Tests/RestfulApiVersioningOptionsTest.cs
index 4c2a284..83b1be0 100644
--- a/test/Codebelt.Extensions.Asp.Versioning.Tests/RestfulApiVersioningOptionsTest.cs
+++ b/test/Codebelt.Extensions.Asp.Versioning.Tests/RestfulApiVersioningOptionsTest.cs
@@ -72,6 +72,16 @@ public void RestfulApiVersioningOptions_ValidAcceptHeadersIsNull_ShouldThrowInva
Assert.IsType(sut3.InnerException);
}
+ [Fact]
+ public void RestfulApiVersioningOptions_UseApiVersionSelector_ShouldUpdateApiVersionSelectorType()
+ {
+ var sut = new RestfulApiVersioningOptions();
+ var result = sut.UseApiVersionSelector();
+
+ Assert.Same(sut, result);
+ Assert.Equal(typeof(LowestImplementedApiVersionSelector), sut.ApiVersionSelectorType);
+ }
+
[Fact]
public void RestfulApiVersioningOptions_ShouldHaveDefaultValues()
{
diff --git a/test/Codebelt.Extensions.Asp.Versioning.Tests/ServiceCollectionExtensionsTest.cs b/test/Codebelt.Extensions.Asp.Versioning.Tests/ServiceCollectionExtensionsTest.cs
new file mode 100644
index 0000000..2f223df
--- /dev/null
+++ b/test/Codebelt.Extensions.Asp.Versioning.Tests/ServiceCollectionExtensionsTest.cs
@@ -0,0 +1,75 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Asp.Versioning;
+using Asp.Versioning.ApiExplorer;
+using Codebelt.Extensions.Asp.Versioning.Assets;
+using Codebelt.Extensions.Xunit;
+using Codebelt.Extensions.Xunit.Hosting.AspNetCore;
+using Cuemon.AspNetCore.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Codebelt.Extensions.Asp.Versioning
+{
+ public class ServiceCollectionExtensionsTest : Test
+ {
+ public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void AddRestfulApiVersioning_ShouldConfigureApiExplorerOptions_WithExpectedGroupNameFormat()
+ {
+ using (var app = WebHostTestFactory.Create(services =>
+ {
+ services.AddControllers().AddApplicationPart(typeof(FakeController).Assembly);
+ services.AddRestfulApiVersioning(o =>
+ {
+ o.ParameterName = "version";
+ o.DefaultApiVersion = new ApiVersion(2, 0);
+ });
+ }, app =>
+ {
+ app.UseRouting();
+ app.UseEndpoints(routes => routes.MapControllers());
+ }))
+ {
+ var options = app.Host.Services.GetRequiredService>().Value;
+
+ TestOutput.WriteLine($"GroupNameFormat: {options.GroupNameFormat}");
+ TestOutput.WriteLine($"SubstituteApiVersionInUrl: {options.SubstituteApiVersionInUrl}");
+ TestOutput.WriteLine($"DefaultApiVersion: {options.DefaultApiVersion}");
+
+ Assert.Equal("'version'VVV", options.GroupNameFormat);
+ Assert.True(options.SubstituteApiVersionInUrl);
+ Assert.Equal(new ApiVersion(2, 0), options.DefaultApiVersion);
+ }
+ }
+
+ [Fact]
+ public async Task AddRestfulApiVersioning_CustomizeProblemDetails_ShouldThrowInternalServerErrorException_WhenStatusCodeIsUnmapped()
+ {
+ using (var app = WebHostTestFactory.Create(services =>
+ {
+ services.AddControllers().AddApplicationPart(typeof(FakeController).Assembly);
+ services.AddRestfulApiVersioning();
+ }, app =>
+ {
+ app.UseRouting();
+ app.UseEndpoints(routes => routes.MapControllers());
+ }))
+ {
+ var client = app.Host.GetTestClient();
+
+ // /fake/problem418 invokes IProblemDetailsService.WriteAsync with Status=418
+ // CustomizeProblemDetails fires; TryParse(418) fails (418 is not in Cuemon's mapped codes)
+ // -> throws InternalServerErrorException (line 57 in ServiceCollectionExtensions.cs)
+ await Assert.ThrowsAsync(() => client.GetAsync("/fake/problem418"));
+ }
+ }
+ }
+}