[OpenAPI] Use invariant culture for TextWriter#62193
[OpenAPI] Use invariant culture for TextWriter#62193captainsafia merged 22 commits intodotnet:mainfrom
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR updates the OpenAPI implementation to enforce the usage of an invariant culture when writing documents using TextWriter. Key changes include adding a new constructor overload to Utf8BufferTextWriter that accepts an IFormatProvider, updating OpenAPI endpoint routing to explicitly use CultureInfo.InvariantCulture, and adding tests and a sample endpoint ("/getcultureinvariant") to verify the culture invariance behavior.
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/SignalR/common/Shared/Utf8BufferTextWriter.cs | Added a new constructor overload to inject an IFormatProvider. |
| src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/... | Updated snapshots to include a new "/getcultureinvariant" endpoint. |
| src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/... | Updated snapshots to include a new "/getcultureinvariant" endpoint. |
| src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs | Refactored test data setup using MemberData and updated the verification call. |
| src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs | Modified the instantiation of Utf8BufferTextWriter to use CultureInfo.InvariantCulture. |
| src/OpenApi/sample/Program.cs | Added middleware to change the current culture for testing invariant behavior. |
| src/OpenApi/sample/Controllers/TestController.cs | Introduced a new GET endpoint and record type to support culture-invariant document generation. |
Comments suppressed due to low confidence (2)
src/OpenApi/sample/Controllers/TestController.cs:35
- [nitpick] The method name 'PostTypedResult' is potentially misleading for a GET endpoint. Consider renaming it to something like 'GetCultureInvariant' to reflect its purpose more clearly.
[HttpGet]
src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs:49
- The change from 'Verifier.Verify(json)' to 'Verify(json)' should be confirmed as intentional, as it alters the API call pattern. If this change is designed to utilize a new verification mechanism, consider adding a brief comment for clarity.
await Verify(json)
|
I think this change should also be applied for GetDocument, but I am not sure. |
Where do you mean exactly? |
|
It's not in the filtered solution, but the dotnet tool/executable which extracts the document during build. |
|
Yep you're right. The code here creates a Then that gets used here: aspnetcore/src/OpenApi/src/Services/OpenApiDocumentProvider.cs Lines 58 to 60 in faf2696 I'll update that to use the invariant culture too. |
Do you mean that the new tests pass without any of the code changes in this PR? It might be good to track the change on the Microsoft.OpenApi side that caused this. |
|
Yeah, they still pass without the fix being present. My hunch is that they stopped using an overload like I did try to test the theory by making an equivalent change to the |
|
@captainsafia I worked out why the repro wasn't happening and fixed it. TL;DR - because the snapshots directly got the document and self-serialized it, it didn't go through the code path for the endpoint that was causing the issue (the tests didn't have the same bug in them). |
| @@ -1,9 +1,14 @@ | |||
| { | |||
| "openapi": "3.0.4", | |||
There was a problem hiding this comment.
Feels like something spooky happened with the new test data setup. These should still target OpenApi v3.
There was a problem hiding this comment.
Oh yes, I see what I've done.
There's no mechanism to pass the OpenAPI version to use on to the endpoint now that it's doing it over HTTP instead of using the service directly.
There was a problem hiding this comment.
Right, I've fixed this now, but I'm not particularly happy with it.
Unless I've missed something, it's not possible to dynamically determine the OpenAPI version to return without encoding it into the document's name, so I've had to expose the endpoints on multiple URLs each to cover 3.0 and 3.1, which then means I need to customise ShouldInclude so the paths still get returned.
Is it worth adding a capability somehow to allow the user to dynamically compute the OpenAPI version to use at runtime? I tried to do it dynamically with IPostConfigureOptions<T>, but as it uses IOptionsMonitor<T> it only runs once and keeps the first request's preference.
Maybe something like this?
public OpenApiOptions
{
+ public Func<HttpContext, OpenApiSpecVersion?>? OpenApiVersionSelector { get; set; }
}If you think it's worth proposing I'll spin up an API Suggestion issue (and am happy to take a hand at implementing it if approved), assuming there's still time to get it into 10.0.
|
While working on a related Swashbuckle change I realised that if I've fixed that and added unit tests for various scenarios. I've also refactored the code to avoid the round-trip string format/parse where possible. |
Move the fixture for OpenAPI localization to its own fixture to not pollute the sample app.
|
@captainsafia I've moved the localization stuff out into its own text fixture as discussed offline. I've left the changes to extend the sample to have different OpenAPI versions covered in the snapshots. Merging with main has uncovered a failing test added by #61399 so I need to investigate what's going on there. It fails in main too, so maybe it's another localisation-related thing that my en-GB machine has flushed out. |
Worked it out - MVC isn't part of the OpenAPI solution filter, so when I rebased it hadn't been rebuilt with the fix, but the tests were being rebuilt because they are in the filter. Running |
captainsafia
left a comment
There was a problem hiding this comment.
Bleh, had these comments in pending and never hit submit 😭
| var scopedServiceProvider = fixture.Services.CreateScope(); | ||
| var document = await documentService.GetOpenApiDocumentAsync(scopedServiceProvider.ServiceProvider); | ||
| var json = await document.SerializeAsJsonAsync(version); | ||
| var versionString = version.ToString(); |
There was a problem hiding this comment.
Can we keep the logic that queries the OpenApiDocumentService directly and only run through the HTTP request based path for the localization tests?
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| [UsesVerify] | ||
| public sealed class OpenApiDocumentLocalizationTests(LocalizedSampleAppFixture fixture) |
There was a problem hiding this comment.
Instead of running the localization tests on every document, can we create a new document specifically for localization scenarios then update this test to only send a request to that one?
- Switch integration tests back to using service directly. - Use dedicated resource for localization test.
...enApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs
Outdated
Show resolved
Hide resolved
Restore `sealed` as there's no longer a derived class.
Remove version-specific endpoints.
captainsafia
left a comment
There was a problem hiding this comment.
Thanks for reacting to all the feedback here! 😃
Use invariant culture for TextWriter
Use culture-invariant
TextWriterimplementation for OpenAPI.Description
TextWriterimplementation.exclusiveMinimum/exclusiveMaximuminstead ofminimum/maximumwhere relevant based on the values ofRangeAttributeproperties.I couldn't replicate this issue in .NET 10, I'm guessing Microsoft.OpenApi has had some internal refactoring that means that it's no longer using an overload onTextWriterthat was hitting this (unless I messed up my test to repro it somehow).