Skip to content

Commit 7734605

Browse files
[OpenAPI] Use invariant culture for TextWriter (#62239)
Ensure OpenAPI documents are written to a culture-invariant `TextWriter` implementation. Resolves #60628.
1 parent 898e997 commit 7734605

11 files changed

+112
-19
lines changed

src/OpenApi/sample/Controllers/TestController.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public IActionResult PostForm([FromForm] MvcTodo todo)
2424
return Ok(todo);
2525
}
2626

27+
[HttpGet]
28+
[Produces("application/json")]
29+
[ProducesResponseType(typeof(CurrentWeather), 200)]
30+
[Route("/getcultureinvariant")]
31+
public IActionResult GetCurrentWeather()
32+
{
33+
return Ok(new CurrentWeather(1.0f));
34+
}
35+
2736
public class RouteParamsContainer
2837
{
2938
[FromRoute]
@@ -36,4 +45,6 @@ public class RouteParamsContainer
3645
}
3746

3847
public record MvcTodo(string Title, string Description, bool IsCompleted);
48+
49+
public record CurrentWeather([Range(-100.5f, 100.5f)] float Temperature = 0.1f);
3950
}

src/OpenApi/sample/Program.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Immutable;
55
using System.ComponentModel;
6+
using System.Globalization;
67
using Microsoft.AspNetCore.Http.HttpResults;
78
using Microsoft.AspNetCore.Mvc;
89
using Microsoft.OpenApi.Models;
@@ -36,6 +37,32 @@
3637

3738
var app = builder.Build();
3839

40+
// Run requests with a culture that uses commas to format decimals to
41+
// verify the invariant culture is used to generate the OpenAPI document.
42+
app.Use((next) =>
43+
{
44+
return async context =>
45+
{
46+
var originalCulture = CultureInfo.CurrentCulture;
47+
var originalUICulture = CultureInfo.CurrentUICulture;
48+
49+
var newCulture = new CultureInfo("fr-FR");
50+
51+
try
52+
{
53+
CultureInfo.CurrentCulture = newCulture;
54+
CultureInfo.CurrentUICulture = newCulture;
55+
56+
await next(context);
57+
}
58+
finally
59+
{
60+
CultureInfo.CurrentCulture = originalCulture;
61+
CultureInfo.CurrentUICulture = originalUICulture;
62+
}
63+
};
64+
});
65+
3966
app.MapOpenApi();
4067
if (app.Environment.IsDevelopment())
4168
{

src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e
4646
var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.Request, context.RequestAborted);
4747
var documentOptions = options.Get(documentName);
4848
using var output = MemoryBufferWriter.Get();
49-
using var writer = Utf8BufferTextWriter.Get(output);
49+
using var writer = new Utf8BufferTextWriter(System.Globalization.CultureInfo.InvariantCulture);
50+
writer.SetWriter(output);
5051
try
5152
{
5253
document.Serialize(new OpenApiJsonWriter(writer), documentOptions.OpenApiVersion);
Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Globalization;
54
using Microsoft.AspNetCore.InternalTesting;
6-
using Microsoft.AspNetCore.OpenApi;
7-
using Microsoft.Extensions.DependencyInjection;
8-
using Microsoft.OpenApi.Models;
9-
using Microsoft.OpenApi.Writers;
105

116
[UsesVerify]
127
public sealed class OpenApiDocumentIntegrationTests(SampleAppFixture fixture) : IClassFixture<SampleAppFixture>
@@ -20,21 +15,12 @@ public sealed class OpenApiDocumentIntegrationTests(SampleAppFixture fixture) :
2015
[InlineData("schemas-by-ref")]
2116
public async Task VerifyOpenApiDocument(string documentName)
2217
{
23-
var documentService = fixture.Services.GetRequiredKeyedService<OpenApiDocumentService>(documentName);
24-
var scopedServiceProvider = fixture.Services.CreateScope();
25-
var document = await documentService.GetOpenApiDocumentAsync(scopedServiceProvider.ServiceProvider);
26-
await Verifier.Verify(GetOpenApiJson(document))
18+
using var client = fixture.CreateClient();
19+
var json = await client.GetStringAsync($"/openapi/{documentName}.json");
20+
await Verify(json)
2721
.UseDirectory(SkipOnHelixAttribute.OnHelix()
2822
? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "Integration", "snapshots")
2923
: "snapshots")
3024
.UseParameters(documentName);
3125
}
32-
33-
private static string GetOpenApiJson(OpenApiDocument document)
34-
{
35-
using var textWriter = new StringWriter(CultureInfo.InvariantCulture);
36-
var jsonWriter = new OpenApiJsonWriter(textWriter);
37-
document.SerializeAsV3(jsonWriter);
38-
return textWriter.ToString();
39-
}
4026
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"title": "Sample | controllers",
55
"version": "1.0.0"
66
},
7+
"servers": [
8+
{
9+
"url": "http://localhost/"
10+
}
11+
],
712
"paths": {
813
"/getbyidandname/{id}/{name}": {
914
"get": {
@@ -88,9 +93,41 @@
8893
}
8994
}
9095
}
96+
},
97+
"/getcultureinvariant": {
98+
"get": {
99+
"tags": [
100+
"Test"
101+
],
102+
"responses": {
103+
"200": {
104+
"description": "OK",
105+
"content": {
106+
"application/json": {
107+
"schema": {
108+
"$ref": "#/components/schemas/CurrentWeather"
109+
}
110+
}
111+
}
112+
}
113+
}
114+
}
115+
}
116+
},
117+
"components": {
118+
"schemas": {
119+
"CurrentWeather": {
120+
"type": "object",
121+
"properties": {
122+
"temperature": {
123+
"type": "number",
124+
"format": "float",
125+
"default": 0.1
126+
}
127+
}
128+
}
91129
}
92130
},
93-
"components": { },
94131
"tags": [
95132
{
96133
"name": "Test"

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=forms.verified.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"title": "Sample | forms",
55
"version": "1.0.0"
66
},
7+
"servers": [
8+
{
9+
"url": "http://localhost/"
10+
}
11+
],
712
"paths": {
813
"/forms/form-file": {
914
"post": {

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=responses.verified.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"title": "Sample | responses",
55
"version": "1.0.0"
66
},
7+
"servers": [
8+
{
9+
"url": "http://localhost/"
10+
}
11+
],
712
"paths": {
813
"/responses/200-add-xml": {
914
"get": {

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"title": "Sample | schemas-by-ref",
55
"version": "1.0.0"
66
},
7+
"servers": [
8+
{
9+
"url": "http://localhost/"
10+
}
11+
],
712
"paths": {
813
"/schemas-by-ref/typed-results": {
914
"get": {

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v1.verified.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"title": "Sample | v1",
55
"version": "1.0.0"
66
},
7+
"servers": [
8+
{
9+
"url": "http://localhost/"
10+
}
11+
],
712
"paths": {
813
"/v1/array-of-guids": {
914
"get": {

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=v2.verified.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
},
1212
"version": "1.0.0"
1313
},
14+
"servers": [
15+
{
16+
"url": "http://localhost/"
17+
}
18+
],
1419
"paths": {
1520
"/v2/users": {
1621
"get": {

0 commit comments

Comments
 (0)