From cef79b3adacd2f0b8b379e813dc51240f30f580c Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Fri, 27 Feb 2026 23:38:06 +0100 Subject: [PATCH 1/6] Add Copilot instructions for ASP.NET API Versioning --- .github/copilot-instructions.md | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..3e2b0b7e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,82 @@ +# Copilot Instructions for ASP.NET API Versioning + +## Project Overview + +This is **ASP.NET API Versioning** (`dotnet/aspnet-api-versioning`), a .NET Foundation library providing API versioning semantics for ASP.NET. It produces multiple NuGet packages supporting ASP.NET Core (Minimal APIs, MVC, OData) and legacy ASP.NET Web API. + +## Build & Test + +- **SDK**: .NET 10.0 (`net10.0` primary; `net472` for legacy Web API; `netstandard1.0`/`2.0` for Abstractions) +- **Solution**: `asp.slnx` at repo root +- **Restore**: `dotnet restore` +- **Build**: `dotnet build` (or `dotnet build --configuration Release`) +- **Test**: `dotnet test` (or `dotnet test --configuration Release`) +- **Pack**: `dotnet pack --configuration Release` +- **Test runner**: Microsoft.Testing.Platform with xUnit v3 (`xunit.v3.mtp-v2`) + +## Architecture + +``` +src/ +├── Abstractions/ # Core types (ApiVersion, etc.) — multi-target netstandard1.0+ +├── AspNetCore/ +│ ├── WebApi/ # Asp.Versioning.Http, .Mvc, .Mvc.ApiExplorer, .OpenApi +│ ├── OData/ # Asp.Versioning.OData, .OData.ApiExplorer +│ └── Acceptance/ # Integration tests using TestHost +├── AspNet/ +│ ├── WebApi/ # Asp.Versioning.WebApi, .WebApi.ApiExplorer (legacy) +│ ├── OData/ # Asp.Versioning.WebApi.OData (legacy) +│ └── Acceptance/ # Integration tests +├── Client/ # Asp.Versioning.Http.Client +└── Common/ + ├── src/ # Shared code via .shproj (Common, Common.Mvc, Common.OData, etc.) + └── test/ # Tests for shared code +examples/ # Working example apps for each flavor +build/ # MSBuild props/targets, CI YAML, signing key +``` + +- **Shared projects** (`.shproj`) under `src/Common/src/` contain code compiled into multiple assemblies. When modifying shared code, consider impact on all consuming projects. +- Production code lives in `src/**/src/`; tests live in `src/**/test/`. + +## Code Conventions + +- **C# Latest** language version with **implicit usings** enabled +- **Nullable reference types**: enabled in production code, disabled in tests +- **`var`**: preferred everywhere +- **`this.`**: avoid unless necessary (enforced as error) +- **Predefined type names**: use `int`, `string` over `Int32`, `String` (enforced as error) +- **Braces**: Allman style (new line before all braces) +- **Indentation**: 4 spaces for C#; 1 space for XML/MSBuild files +- **Line length**: 120 characters guideline +- **Properties**: expression-bodied preferred +- **Methods**: block body (not expression-bodied) +- **Analyzers**: StyleCop v1.2.0-beta with `TreatWarningsAsErrors: true` — fix all warnings before committing +- **Strong naming**: all production assemblies are signed (`build/key.snk`) +- **CLS Compliance**: production assemblies are CLS-compliant + +## Test Conventions + +- **Framework**: xUnit v3 with FluentAssertions and Moq +- **Class naming**: `[ClassName]Test` (e.g., `ApiVersionTest`, `SunsetPolicyManagerTest`) +- **Method structure**: arrange / act / assert (use comments to delineate sections) +- **Async tests**: use `TestContext.Current.CancellationToken` +- **Assertions**: FluentAssertions (`.Should().Be()`, `.Should().BeEquivalentTo()`, etc.) +- **Mocking**: Moq (`Mock.Of<>()`, `Mock.Get().Verify()`) +- **Every bug fix or feature must include tests** +- **Acceptance tests** use `TestHost` for full integration scenarios + +## Build Infrastructure + +- MSBuild property hierarchy flows through `build/*.props` → `src/Directory.Build.props` → individual `.csproj` +- NuGet package metadata is centralized in `build/nuget.props` +- Version is set per-project in `.csproj` files (currently `10.0.0`) +- CI runs on Azure Pipelines (`azure-pipelines.yml` → `build/steps-ci.yml`) +- Deterministic builds are enabled in CI (`ContinuousIntegrationBuild=true`) +- SourceLink is configured for GitHub + +## Common Pitfalls + +- Modifying `.shproj` shared projects affects multiple target assemblies — always build the full solution to verify +- The `.editorconfig` and StyleCop analyzers enforce strict style; the build fails on violations +- Legacy ASP.NET Web API projects (`net472`) have different API surfaces — test both frameworks when changing shared code +- Example projects have their own `Directory.Build.props` and `Directory.Packages.props` separate from `src/` From 5af856355b454a2e5569339749c2e7235028f33b Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Sat, 28 Feb 2026 00:23:49 +0100 Subject: [PATCH 2/6] Register EndpointMetadataApiDescriptionProvider for minimal APIs AddApiExplorerServices() only called AddMvcCore().AddApiExplorer(), which registers DefaultApiDescriptionProvider for controllers. Minimal API endpoints were never discovered because EndpointMetadataApiDescriptionProvider was not registered. Added services.AddEndpointsApiExplorer() which registers the missing provider. This call is safe to invoke multiple times (uses TryAddEnumerable internally). Fixes dotnet/aspnet-api-versioning#1165 --- .../IApiVersioningBuilderExtensions.cs | 5 ++ .../Transformers/AcceptanceTest.cs | 72 ++++++++++++++++--- 2 files changed, 69 insertions(+), 8 deletions(-) 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 f577ebf4..c31945a5 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 @@ -58,7 +58,12 @@ private static void AddApiExplorerServices( IApiVersioningBuilder builder ) var services = builder.Services; + // registers DefaultApiDescriptionProvider, which discovers controller-based endpoints services.AddMvcCore().AddApiExplorer(); + + // registers EndpointMetadataApiDescriptionProvider, which discovers minimal API endpoints. + // both providers are required so that OpenApiDocumentService sees all endpoints. + services.AddEndpointsApiExplorer(); services.TryAddSingleton, ApiExplorerOptionsFactory>(); services.TryAddTransient(); services.TryAddSingleton( static sp => sp.GetRequiredService().Create() ); diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs index 738e5d89..9d780bbf 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs @@ -7,12 +7,18 @@ namespace Asp.Versioning.OpenApi.Transformers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using System.Collections.Generic; using System.Net.Http.Json; using System.Text.Json.Nodes; public class AcceptanceTest { + /// + /// Verifies that minimal API endpoints produce a non-empty OpenAPI document. + /// AddApiExplorer internally calls AddMvcCore().AddApiExplorer(), + /// which auto-discovers controllers from the test assembly. Application parts + /// are cleared to isolate the test to minimal API endpoints only. + /// + /// A representing the asynchronous unit test. [Fact] public async Task minimal_api_should_generate_expected_open_api_document() { @@ -20,12 +26,13 @@ public async Task minimal_api_should_generate_expected_open_api_document() var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); - builder.Services.AddControllers() - .AddApplicationPart( GetType().Assembly ); + builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => AddPolicies( options ) ) - .AddMvc() .AddApiExplorer( options => options.GroupNameFormat = "'v'VVV" ) .AddOpenApi(); + builder.Services.AddMvcCore() + .ConfigureApplicationPartManager( + m => m.ApplicationParts.Clear() ); var app = builder.Build(); var api = app.NewVersionedApi( "Test" ) @@ -36,9 +43,6 @@ public async Task minimal_api_should_generate_expected_open_api_document() app.MapOpenApi().WithDocumentPerVersion(); var cancellationToken = TestContext.Current.CancellationToken; - using var stream = File.OpenRead( Path.Combine( AppContext.BaseDirectory, "Content", "v1.json" ) ); - var expected = await JsonNode.ParseAsync( stream, default, default, cancellationToken ); - await app.StartAsync( cancellationToken ); using var client = app.GetTestClient(); @@ -47,7 +51,18 @@ public async Task minimal_api_should_generate_expected_open_api_document() var actual = await client.GetFromJsonAsync( "/openapi/v1.json", cancellationToken ); // assert - JsonNode.DeepEquals( actual, expected ).Should().BeTrue(); + actual!["info"]!["version"]!.GetValue().Should().Be( "1.0" ); + + var paths = actual["paths"]!.AsObject(); + + paths.Select( p => p.Key ).Should().Contain( "/test/{id}" ); + paths.Select( p => p.Key ).Should().NotContain( "/Test" ); + + var operation = paths["/test/{id}"]!["get"]!; + var parameters = operation["parameters"]!.AsArray(); + + parameters.Should().Contain( p => p!["name"]!.GetValue() == "id" ); + parameters.Should().Contain( p => p!["name"]!.GetValue() == "api-version" ); } [Fact] @@ -84,6 +99,47 @@ public async Task controller_should_generate_expected_open_api_document() JsonNode.DeepEquals( actual, expected ).Should().BeTrue(); } + [Fact] + public async Task mixed_api_should_generate_expected_open_api_document() + { + // arrange + var builder = WebApplication.CreateBuilder(); + + builder.WebHost.UseTestServer(); + builder.Services.AddControllers() + .AddApplicationPart( GetType().Assembly ); + builder.Services.AddApiVersioning( options => AddPolicies( options ) ) + .AddMvc() + .AddApiExplorer( options => options.GroupNameFormat = "'v'VVV" ) + .AddOpenApi(); + + var app = builder.Build(); + + app.MapControllers(); + var api = app.NewVersionedApi( "Test" ) + .MapGroup( "/minimal" ) + .HasApiVersion( 1.0 ); + + api.MapGet( "{id:int}", MinimalApi.Get ).Produces().Produces( 400 ); + app.MapOpenApi().WithDocumentPerVersion(); + + var cancellationToken = TestContext.Current.CancellationToken; + await app.StartAsync( cancellationToken ); + + using var client = app.GetTestClient(); + + // act + var actual = await client.GetFromJsonAsync( "/openapi/v1.json", cancellationToken ); + + // assert + actual!["info"]!["version"]!.GetValue().Should().Be( "1.0" ); + + var paths = actual["paths"]!.AsObject(); + + paths.Select( p => p.Key ).Should().Contain( "/minimal/{id}" ); + paths.Select( p => p.Key ).Should().Contain( "/Test" ); + } + private static ApiVersioningOptions AddPolicies( ApiVersioningOptions options ) { options.Policies.Deprecate( 1.0 ) From 676cf8112dd62d4b6cbf717908c906a5211e27e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:03:29 +0000 Subject: [PATCH 3/6] Initial plan From 3ff777c6b5acbbc4e50cc10f98e9872f9e110d03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:11:14 +0000 Subject: [PATCH 4/6] Rewrite AcceptanceTests to use JSON files and JsonNode.DeepEquals Replace manual parameter checks in minimal_api and mixed_api tests with JsonNode.DeepEquals comparison against expected JSON content files. Add v1-minimal.json and v1-mixed.json expected output files. Co-authored-by: sander1095 <7312681+sander1095@users.noreply.github.com> --- .../Content/v1-minimal.json | 86 ++++++++++ .../Content/v1-mixed.json | 157 ++++++++++++++++++ .../Transformers/AcceptanceTest.cs | 27 +-- 3 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json create mode 100644 src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json new file mode 100644 index 00000000..5e9491ce --- /dev/null +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json @@ -0,0 +1,86 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Test API | v1", + "description": "The API was deprecated on 01/01/2026. The API was sunset on 02/10/2026.\n\n### Links\n\n- [Version Deprecation Policy](http://my.api.com/policies/versions/deprecated.html)\n- [Version Sunset Policy](http://my.api.com/policies/versions/sunset.html)", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost/" + } + ], + "paths": { + "/test/{id}": { + "get": { + "tags": [ + "Test" + ], + "summary": "", + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The requested API version", + "required": true, + "schema": { + "type": "string", + "default": "1.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + } + } + }, + "400": { + "description": "Bad Request" + } + } + } + } + }, + "tags": [ + { + "name": "Test" + } + ], + "x-api-versioning": [ + { + "title": "Version Deprecation Policy", + "type": "text/html", + "rel": "deprecation", + "url": "http://my.api.com/policies/versions/deprecated.html" + }, + { + "title": "Version Sunset Policy", + "type": "text/html", + "rel": "sunset", + "url": "http://my.api.com/policies/versions/sunset.html" + } + ] +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json new file mode 100644 index 00000000..f6ec6c15 --- /dev/null +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json @@ -0,0 +1,157 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Test API | v1", + "description": "The API was deprecated on 01/01/2026. The API was sunset on 02/10/2026.\n\n### Links\n\n- [Version Deprecation Policy](http://my.api.com/policies/versions/deprecated.html)\n- [Version Sunset Policy](http://my.api.com/policies/versions/sunset.html)", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost/" + } + ], + "paths": { + "/minimal/{id}": { + "get": { + "tags": [ + "Test" + ], + "summary": "", + "description": "", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "", + "required": true, + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": "integer", + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The requested API version", + "required": true, + "schema": { + "type": "string", + "default": "1.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + } + } + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/Test": { + "get": { + "tags": [ + "Test" + ], + "summary": "", + "description": "", + "parameters": [ + { + "name": "id", + "in": "query", + "description": "", + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + { + "name": "api-version", + "in": "query", + "description": "The requested API version", + "required": true, + "schema": { + "type": "string", + "default": "1.0" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + "application/json": { + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + }, + "text/json": { + "schema": { + "pattern": "^-?(?:0|[1-9]\\d*)$", + "type": [ + "integer", + "string" + ], + "format": "int32" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "Test" + } + ], + "x-api-versioning": [ + { + "title": "Version Deprecation Policy", + "type": "text/html", + "rel": "deprecation", + "url": "http://my.api.com/policies/versions/deprecated.html" + }, + { + "title": "Version Sunset Policy", + "type": "text/html", + "rel": "sunset", + "url": "http://my.api.com/policies/versions/sunset.html" + } + ] +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs index 9d780bbf..54d9822e 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs @@ -26,7 +26,6 @@ public async Task minimal_api_should_generate_expected_open_api_document() var builder = WebApplication.CreateBuilder(); builder.WebHost.UseTestServer(); - builder.Services.AddProblemDetails(); builder.Services.AddApiVersioning( options => AddPolicies( options ) ) .AddApiExplorer( options => options.GroupNameFormat = "'v'VVV" ) .AddOpenApi(); @@ -43,6 +42,9 @@ public async Task minimal_api_should_generate_expected_open_api_document() app.MapOpenApi().WithDocumentPerVersion(); var cancellationToken = TestContext.Current.CancellationToken; + using var stream = File.OpenRead( Path.Combine( AppContext.BaseDirectory, "Content", "v1-minimal.json" ) ); + var expected = await JsonNode.ParseAsync( stream, default, default, cancellationToken ); + await app.StartAsync( cancellationToken ); using var client = app.GetTestClient(); @@ -51,18 +53,7 @@ public async Task minimal_api_should_generate_expected_open_api_document() var actual = await client.GetFromJsonAsync( "/openapi/v1.json", cancellationToken ); // assert - actual!["info"]!["version"]!.GetValue().Should().Be( "1.0" ); - - var paths = actual["paths"]!.AsObject(); - - paths.Select( p => p.Key ).Should().Contain( "/test/{id}" ); - paths.Select( p => p.Key ).Should().NotContain( "/Test" ); - - var operation = paths["/test/{id}"]!["get"]!; - var parameters = operation["parameters"]!.AsArray(); - - parameters.Should().Contain( p => p!["name"]!.GetValue() == "id" ); - parameters.Should().Contain( p => p!["name"]!.GetValue() == "api-version" ); + JsonNode.DeepEquals( actual, expected ).Should().BeTrue(); } [Fact] @@ -124,6 +115,9 @@ public async Task mixed_api_should_generate_expected_open_api_document() app.MapOpenApi().WithDocumentPerVersion(); var cancellationToken = TestContext.Current.CancellationToken; + using var stream = File.OpenRead( Path.Combine( AppContext.BaseDirectory, "Content", "v1-mixed.json" ) ); + var expected = await JsonNode.ParseAsync( stream, default, default, cancellationToken ); + await app.StartAsync( cancellationToken ); using var client = app.GetTestClient(); @@ -132,12 +126,7 @@ public async Task mixed_api_should_generate_expected_open_api_document() var actual = await client.GetFromJsonAsync( "/openapi/v1.json", cancellationToken ); // assert - actual!["info"]!["version"]!.GetValue().Should().Be( "1.0" ); - - var paths = actual["paths"]!.AsObject(); - - paths.Select( p => p.Key ).Should().Contain( "/minimal/{id}" ); - paths.Select( p => p.Key ).Should().Contain( "/Test" ); + JsonNode.DeepEquals( actual, expected ).Should().BeTrue(); } private static ApiVersioningOptions AddPolicies( ApiVersioningOptions options ) From de2a7569effd70ce9d464cad5567da9deab7a131 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Wed, 4 Mar 2026 17:13:07 +0100 Subject: [PATCH 5/6] Add AssumeCultureAttribute for locale-specific testing and update API descriptions The AssumeCultureAttribute is, for now, copied from another test project. PR feedback can indicate if a shared approach is preferred, and, if so, how that can be achieved --- .../AssumeCultureAttribute.cs | 39 +++++++++++++++++++ .../Content/v1-minimal.json | 2 +- .../Content/v1-mixed.json | 2 +- .../Transformers/AcceptanceTest.cs | 6 ++- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/AssumeCultureAttribute.cs diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/AssumeCultureAttribute.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/AssumeCultureAttribute.cs new file mode 100644 index 00000000..395a9550 --- /dev/null +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/AssumeCultureAttribute.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. + +namespace Asp.Versioning; + +using System.Globalization; +using System.Reflection; +using static System.AttributeTargets; +using static System.Threading.Thread; + +/// +/// Allows a test method to assume that it is running in a specific locale. +/// +[AttributeUsage( Class | Method, AllowMultiple = false, Inherited = true )] +internal sealed class AssumeCultureAttribute : BeforeAfterTestAttribute +{ + private CultureInfo originalCulture; + private CultureInfo originalUICulture; + + public AssumeCultureAttribute( string name ) => Name = name; + + public string Name { get; } + + public override void Before( MethodInfo methodUnderTest, IXunitTest test ) + { + originalCulture = CurrentThread.CurrentCulture; + originalUICulture = CurrentThread.CurrentUICulture; + + var culture = CultureInfo.CreateSpecificCulture( Name ); + + CurrentThread.CurrentCulture = culture; + CurrentThread.CurrentUICulture = culture; + } + + public override void After( MethodInfo methodUnderTest, IXunitTest test ) + { + CurrentThread.CurrentCulture = originalCulture; + CurrentThread.CurrentUICulture = originalUICulture; + } +} \ No newline at end of file diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json index 5e9491ce..3a29e0a2 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json @@ -2,7 +2,7 @@ "openapi": "3.1.1", "info": { "title": "Test API | v1", - "description": "The API was deprecated on 01/01/2026. The API was sunset on 02/10/2026.\n\n### Links\n\n- [Version Deprecation Policy](http://my.api.com/policies/versions/deprecated.html)\n- [Version Sunset Policy](http://my.api.com/policies/versions/sunset.html)", + "description": "The API was deprecated on 1/1/2026. The API was sunset on 2/10/2026.\r\n\r\n### Links\r\n\r\n- [Version Deprecation Policy](http://my.api.com/policies/versions/deprecated.html)\r\n- [Version Sunset Policy](http://my.api.com/policies/versions/sunset.html)", "version": "1.0" }, "servers": [ diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json index f6ec6c15..30b4afc6 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json @@ -2,7 +2,7 @@ "openapi": "3.1.1", "info": { "title": "Test API | v1", - "description": "The API was deprecated on 01/01/2026. The API was sunset on 02/10/2026.\n\n### Links\n\n- [Version Deprecation Policy](http://my.api.com/policies/versions/deprecated.html)\n- [Version Sunset Policy](http://my.api.com/policies/versions/sunset.html)", + "description": "The API was deprecated on 1/1/2026. The API was sunset on 2/10/2026.\r\n\r\n### Links\r\n\r\n- [Version Deprecation Policy](http://my.api.com/policies/versions/deprecated.html)\r\n- [Version Sunset Policy](http://my.api.com/policies/versions/sunset.html)", "version": "1.0" }, "servers": [ diff --git a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs index 54d9822e..2e8652cc 100644 --- a/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs +++ b/src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Transformers/AcceptanceTest.cs @@ -7,6 +7,7 @@ namespace Asp.Versioning.OpenApi.Transformers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using System.Globalization; using System.Net.Http.Json; using System.Text.Json.Nodes; @@ -20,11 +21,12 @@ public class AcceptanceTest /// /// A representing the asynchronous unit test. [Fact] + [AssumeCulture( "en-US" )] public async Task minimal_api_should_generate_expected_open_api_document() { // arrange var builder = WebApplication.CreateBuilder(); - + var culture = CultureInfo.CurrentCulture; builder.WebHost.UseTestServer(); builder.Services.AddApiVersioning( options => AddPolicies( options ) ) .AddApiExplorer( options => options.GroupNameFormat = "'v'VVV" ) @@ -57,6 +59,7 @@ public async Task minimal_api_should_generate_expected_open_api_document() } [Fact] + [AssumeCulture( "en-US" )] public async Task controller_should_generate_expected_open_api_document() { // arrange @@ -91,6 +94,7 @@ public async Task controller_should_generate_expected_open_api_document() } [Fact] + [AssumeCulture( "en-US" )] public async Task mixed_api_should_generate_expected_open_api_document() { // arrange From 44cd5853ac0c9c18fcf327f27e920a73034c3964 Mon Sep 17 00:00:00 2001 From: Sander ten Brinke Date: Wed, 4 Mar 2026 18:11:33 +0100 Subject: [PATCH 6/6] Remove Copilot instructions for ASP.NET API Versioning --- .github/copilot-instructions.md | 82 --------------------------------- 1 file changed, 82 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 3e2b0b7e..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,82 +0,0 @@ -# Copilot Instructions for ASP.NET API Versioning - -## Project Overview - -This is **ASP.NET API Versioning** (`dotnet/aspnet-api-versioning`), a .NET Foundation library providing API versioning semantics for ASP.NET. It produces multiple NuGet packages supporting ASP.NET Core (Minimal APIs, MVC, OData) and legacy ASP.NET Web API. - -## Build & Test - -- **SDK**: .NET 10.0 (`net10.0` primary; `net472` for legacy Web API; `netstandard1.0`/`2.0` for Abstractions) -- **Solution**: `asp.slnx` at repo root -- **Restore**: `dotnet restore` -- **Build**: `dotnet build` (or `dotnet build --configuration Release`) -- **Test**: `dotnet test` (or `dotnet test --configuration Release`) -- **Pack**: `dotnet pack --configuration Release` -- **Test runner**: Microsoft.Testing.Platform with xUnit v3 (`xunit.v3.mtp-v2`) - -## Architecture - -``` -src/ -├── Abstractions/ # Core types (ApiVersion, etc.) — multi-target netstandard1.0+ -├── AspNetCore/ -│ ├── WebApi/ # Asp.Versioning.Http, .Mvc, .Mvc.ApiExplorer, .OpenApi -│ ├── OData/ # Asp.Versioning.OData, .OData.ApiExplorer -│ └── Acceptance/ # Integration tests using TestHost -├── AspNet/ -│ ├── WebApi/ # Asp.Versioning.WebApi, .WebApi.ApiExplorer (legacy) -│ ├── OData/ # Asp.Versioning.WebApi.OData (legacy) -│ └── Acceptance/ # Integration tests -├── Client/ # Asp.Versioning.Http.Client -└── Common/ - ├── src/ # Shared code via .shproj (Common, Common.Mvc, Common.OData, etc.) - └── test/ # Tests for shared code -examples/ # Working example apps for each flavor -build/ # MSBuild props/targets, CI YAML, signing key -``` - -- **Shared projects** (`.shproj`) under `src/Common/src/` contain code compiled into multiple assemblies. When modifying shared code, consider impact on all consuming projects. -- Production code lives in `src/**/src/`; tests live in `src/**/test/`. - -## Code Conventions - -- **C# Latest** language version with **implicit usings** enabled -- **Nullable reference types**: enabled in production code, disabled in tests -- **`var`**: preferred everywhere -- **`this.`**: avoid unless necessary (enforced as error) -- **Predefined type names**: use `int`, `string` over `Int32`, `String` (enforced as error) -- **Braces**: Allman style (new line before all braces) -- **Indentation**: 4 spaces for C#; 1 space for XML/MSBuild files -- **Line length**: 120 characters guideline -- **Properties**: expression-bodied preferred -- **Methods**: block body (not expression-bodied) -- **Analyzers**: StyleCop v1.2.0-beta with `TreatWarningsAsErrors: true` — fix all warnings before committing -- **Strong naming**: all production assemblies are signed (`build/key.snk`) -- **CLS Compliance**: production assemblies are CLS-compliant - -## Test Conventions - -- **Framework**: xUnit v3 with FluentAssertions and Moq -- **Class naming**: `[ClassName]Test` (e.g., `ApiVersionTest`, `SunsetPolicyManagerTest`) -- **Method structure**: arrange / act / assert (use comments to delineate sections) -- **Async tests**: use `TestContext.Current.CancellationToken` -- **Assertions**: FluentAssertions (`.Should().Be()`, `.Should().BeEquivalentTo()`, etc.) -- **Mocking**: Moq (`Mock.Of<>()`, `Mock.Get().Verify()`) -- **Every bug fix or feature must include tests** -- **Acceptance tests** use `TestHost` for full integration scenarios - -## Build Infrastructure - -- MSBuild property hierarchy flows through `build/*.props` → `src/Directory.Build.props` → individual `.csproj` -- NuGet package metadata is centralized in `build/nuget.props` -- Version is set per-project in `.csproj` files (currently `10.0.0`) -- CI runs on Azure Pipelines (`azure-pipelines.yml` → `build/steps-ci.yml`) -- Deterministic builds are enabled in CI (`ContinuousIntegrationBuild=true`) -- SourceLink is configured for GitHub - -## Common Pitfalls - -- Modifying `.shproj` shared projects affects multiple target assemblies — always build the full solution to verify -- The `.editorconfig` and StyleCop analyzers enforce strict style; the build fails on violations -- Legacy ASP.NET Web API projects (`net472`) have different API surfaces — test both frameworks when changing shared code -- Example projects have their own `Directory.Build.props` and `Directory.Packages.props` separate from `src/`