Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .docfx/Dockerfile.docfx
Original file line number Diff line number Diff line change
@@ -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/*
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
36 changes: 22 additions & 14 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Asp.Versioning.Abstractions" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Http" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
<PackageVersion Include="Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json" Version="10.1.2" />
<PackageVersion Include="Codebelt.Extensions.AspNetCore.Mvc.Formatters.Text.Yaml" Version="10.1.2" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.0.9" />
<PackageVersion Include="Cuemon.AspNetCore" Version="10.5.1" />
<PackageVersion Include="Cuemon.Extensions.AspNetCore.Mvc" Version="10.5.1" />
<PackageVersion Include="Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json" Version="10.5.1" />
<PackageVersion Include="Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml" Version="10.5.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.4.0" />
<PackageVersion Include="Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json" Version="10.1.3" />
<PackageVersion Include="Codebelt.Extensions.AspNetCore.Mvc.Formatters.Text.Yaml" Version="10.1.3" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.0.10" />
<PackageVersion Include="Cuemon.AspNetCore" Version="10.5.2" />
<PackageVersion Include="Cuemon.Extensions.AspNetCore.Mvc" Version="10.5.2" />
<PackageVersion Include="Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json" Version="10.5.2" />
<PackageVersion Include="Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml" Version="10.5.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageVersion Include="MinVer" Version="7.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.0" />
<PackageVersion Include="coverlet.msbuild" Version="10.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="coverlet.msbuild" Version="10.0.1" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="xunit.v3.runner.console" Version="3.2.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net9'))">
<PackageVersion Include="Asp.Versioning.Abstractions" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Http" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.1" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.1" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net10'))">
<PackageVersion Include="Asp.Versioning.Abstractions" Version="10.0.0" />
<PackageVersion Include="Asp.Versioning.Http" Version="10.0.0" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="10.0.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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<HttpContext, HttpStatusCodeException> 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<NotAcceptableException>(() => 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<NotAcceptableException>(() => 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<InternalServerErrorException>(() => client.GetAsync("/test/teapot"));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<IActionResult> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ public void RestfulApiVersioningOptions_ValidAcceptHeadersIsNull_ShouldThrowInva
Assert.IsType<InvalidOperationException>(sut3.InnerException);
}

[Fact]
public void RestfulApiVersioningOptions_UseApiVersionSelector_ShouldUpdateApiVersionSelectorType()
{
var sut = new RestfulApiVersioningOptions();
var result = sut.UseApiVersionSelector<LowestImplementedApiVersionSelector>();

Assert.Same(sut, result);
Assert.Equal(typeof(LowestImplementedApiVersionSelector), sut.ApiVersionSelectorType);
}

[Fact]
public void RestfulApiVersioningOptions_ShouldHaveDefaultValues()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IOptions<ApiExplorerOptions>>().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<InternalServerErrorException>(() => client.GetAsync("/fake/problem418"));
}
}
}
}
Loading