diff --git a/src/OpenApi/sample/Controllers/TestController.cs b/src/OpenApi/sample/Controllers/TestController.cs index fdc398987a35..79784263fb0f 100644 --- a/src/OpenApi/sample/Controllers/TestController.cs +++ b/src/OpenApi/sample/Controllers/TestController.cs @@ -32,6 +32,13 @@ public IActionResult PostForm([FromForm] MvcTodo todo) return Ok(todo); } + [HttpGet] + [Route("/getcultureinvariant")] + public Ok GetCurrentWeather() + { + return TypedResults.Ok(new CurrentWeather(1.0f)); + } + public class RouteParamsContainer { [FromRoute] @@ -44,4 +51,6 @@ public class RouteParamsContainer } public record MvcTodo(string Title, string Description, bool IsCompleted); + + public record CurrentWeather([property: Range(-100.5f, 100.5f)] float Temperature = 0.1f); } diff --git a/src/OpenApi/sample/Program.cs b/src/OpenApi/sample/Program.cs index 773e89d28093..11fd830b1234 100644 --- a/src/OpenApi/sample/Program.cs +++ b/src/OpenApi/sample/Program.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using System.Text.Json.Serialization; using Sample.Transformers; @@ -23,11 +24,13 @@ options.AddHeader("X-Version", "1.0"); options.AddDocumentTransformer(); }); -builder.Services.AddOpenApi("v2", options => { +builder.Services.AddOpenApi("v2", options => +{ options.AddSchemaTransformer(); options.AddOperationTransformer(); options.AddDocumentTransformer(new AddContactTransformer()); - options.AddDocumentTransformer((document, context, token) => { + options.AddDocumentTransformer((document, context, token) => + { document.Info.License = new OpenApiLicense { Name = "MIT" }; return Task.CompletedTask; }); @@ -37,6 +40,15 @@ builder.Services.AddOpenApi("forms"); builder.Services.AddOpenApi("schemas-by-ref"); builder.Services.AddOpenApi("xml"); +builder.Services.AddOpenApi("localized", options => +{ + options.ShouldInclude = _ => true; + options.AddDocumentTransformer((document, context, token) => + { + document.Info.Description = $"This is a localized OpenAPI document for {CultureInfo.CurrentUICulture.NativeName}."; + return Task.CompletedTask; + }); +}); var app = builder.Build(); diff --git a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs index bde53bfd7f2e..c41a2d3441b0 100644 --- a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs +++ b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs @@ -90,22 +90,42 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable } else if (attribute is RangeAttribute rangeAttribute) { - // Use InvariantCulture if explicitly requested or if the range has been set via the - // RangeAttribute(double, double) or RangeAttribute(int, int) constructors. - var targetCulture = rangeAttribute.ParseLimitsInInvariantCulture || rangeAttribute.Minimum is double || rangeAttribute.Maximum is int - ? CultureInfo.InvariantCulture - : CultureInfo.CurrentCulture; + decimal? minDecimal = null; + decimal? maxDecimal = null; - var minString = rangeAttribute.Minimum.ToString(); - var maxString = rangeAttribute.Maximum.ToString(); + if (rangeAttribute.Minimum is int minimumInteger) + { + // The range was set with the RangeAttribute(int, int) constructor. + minDecimal = minimumInteger; + maxDecimal = (int)rangeAttribute.Maximum; + } + else + { + // Use InvariantCulture if explicitly requested or if the range has been set via the RangeAttribute(double, double) constructor. + var targetCulture = rangeAttribute.ParseLimitsInInvariantCulture || rangeAttribute.Minimum is double + ? CultureInfo.InvariantCulture + : CultureInfo.CurrentCulture; + + var minString = Convert.ToString(rangeAttribute.Minimum, targetCulture); + var maxString = Convert.ToString(rangeAttribute.Maximum, targetCulture); + + if (decimal.TryParse(minString, NumberStyles.Any, targetCulture, out var value)) + { + minDecimal = value; + } + if (decimal.TryParse(maxString, NumberStyles.Any, targetCulture, out value)) + { + maxDecimal = value; + } + } - if (decimal.TryParse(minString, NumberStyles.Any, targetCulture, out var minDecimal)) + if (minDecimal is { } minValue) { - schema[OpenApiSchemaKeywords.MinimumKeyword] = minDecimal; + schema[rangeAttribute.MinimumIsExclusive ? OpenApiSchemaKeywords.ExclusiveMinimum : OpenApiSchemaKeywords.MinimumKeyword] = minValue; } - if (decimal.TryParse(maxString, NumberStyles.Any, targetCulture, out var maxDecimal)) + if (maxDecimal is { } maxValue) { - schema[OpenApiSchemaKeywords.MaximumKeyword] = maxDecimal; + schema[rangeAttribute.MaximumIsExclusive ? OpenApiSchemaKeywords.ExclusiveMaximum : OpenApiSchemaKeywords.MaximumKeyword] = maxValue; } } else if (attribute is RegularExpressionAttribute regularExpressionAttribute) diff --git a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs index 8fdbfa27d723..7028ae71600b 100644 --- a/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs +++ b/src/OpenApi/src/Extensions/OpenApiEndpointRouteBuilderExtensions.cs @@ -52,7 +52,7 @@ public static IEndpointConventionBuilder MapOpenApi(this IEndpointRouteBuilder e var document = await documentService.GetOpenApiDocumentAsync(context.RequestServices, context.Request, context.RequestAborted); var documentOptions = options.Get(lowercasedDocumentName); - using var textWriter = new Utf8BufferTextWriter(); + using var textWriter = new Utf8BufferTextWriter(System.Globalization.CultureInfo.InvariantCulture); textWriter.SetWriter(context.Response.BodyWriter); string contentType; diff --git a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs index 6633ddac3f65..3b0937db091e 100644 --- a/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs +++ b/src/OpenApi/src/Schemas/OpenApiJsonSchema.Helpers.cs @@ -260,11 +260,21 @@ public static void ReadProperty(ref Utf8JsonReader reader, string propertyName, var minimum = reader.GetDecimal(); schema.Minimum = minimum.ToString(CultureInfo.InvariantCulture); break; + case OpenApiSchemaKeywords.ExclusiveMinimum: + reader.Read(); + var exclusiveMinimum = reader.GetDecimal(); + schema.ExclusiveMinimum = exclusiveMinimum.ToString(CultureInfo.InvariantCulture); + break; case OpenApiSchemaKeywords.MaximumKeyword: reader.Read(); var maximum = reader.GetDecimal(); schema.Maximum = maximum.ToString(CultureInfo.InvariantCulture); break; + case OpenApiSchemaKeywords.ExclusiveMaximum: + reader.Read(); + var exclusiveMaximum = reader.GetDecimal(); + schema.ExclusiveMaximum = exclusiveMaximum.ToString(CultureInfo.InvariantCulture); + break; case OpenApiSchemaKeywords.PatternKeyword: reader.Read(); var pattern = reader.GetString(); diff --git a/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs b/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs index 255cfae73c1c..84f27500d135 100644 --- a/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs +++ b/src/OpenApi/src/Schemas/OpenApiSchemaKeywords.cs @@ -19,7 +19,9 @@ internal class OpenApiSchemaKeywords public const string MaxLengthKeyword = "maxLength"; public const string PatternKeyword = "pattern"; public const string MinimumKeyword = "minimum"; + public const string ExclusiveMinimum = "exclusiveMinimum"; public const string MaximumKeyword = "maximum"; + public const string ExclusiveMaximum = "exclusiveMaximum"; public const string MinItemsKeyword = "minItems"; public const string MaxItemsKeyword = "maxItems"; public const string RefKeyword = "$ref"; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs index 6c4febb6beec..4693176aedc3 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests/SnapshotTestHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Globalization; using System.Reflection; using System.Runtime.Loader; using System.Text; @@ -196,8 +197,7 @@ void OnEntryPointExit(Exception exception) var service = services.GetService(serviceType) ?? throw new InvalidOperationException("Could not resolve IDocumentProvider service."); using var stream = new MemoryStream(); - var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - using var writer = new StreamWriter(stream, encoding, bufferSize: 1024, leaveOpen: true); + using var writer = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture) { AutoFlush = true }; var targetMethod = serviceType.GetMethod("GenerateAsync", [typeof(string), typeof(TextWriter)]) ?? throw new InvalidOperationException("Could not resolve GenerateAsync method."); targetMethod.Invoke(service, ["v1", writer]); stream.Position = 0; diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs new file mode 100644 index 000000000000..bf4126735a9d --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.Text.Json.Nodes; + +namespace Microsoft.AspNetCore.OpenApi.Tests; + +public static class JsonNodeSchemaExtensionsTests +{ + public static TheoryData TestCases() + { + bool[] isExclusive = [false, true]; + + string[] invariantOrEnglishCultures = + [ + string.Empty, + "en", + "en-AU", + "en-GB", + "en-US", + ]; + + string[] commaForDecimalCultures = + [ + "de-DE", + "fr-FR", + "sv-SE", + ]; + + Type[] fractionNumberTypes = + [ + typeof(float), + typeof(double), + typeof(decimal), + ]; + + var testCases = new TheoryData(); + + foreach (var culture in invariantOrEnglishCultures) + { + foreach (var exclusive in isExclusive) + { + testCases.Add(culture, exclusive, new(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234"); + testCases.Add(culture, exclusive, new(1d, 1234d) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234"); + testCases.Add(culture, exclusive, new(1.23, 4.56) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56"); + + foreach (var type in fractionNumberTypes) + { + testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56"); + testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "1.23", "4.56"); + } + } + } + + foreach (var culture in commaForDecimalCultures) + { + foreach (var exclusive in isExclusive) + { + testCases.Add(culture, exclusive, new(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234"); + testCases.Add(culture, exclusive, new(1d, 1234d) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234"); + testCases.Add(culture, exclusive, new(1.23, 4.56) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56"); + + foreach (var type in fractionNumberTypes) + { + testCases.Add(culture, exclusive, new(type, "1,23", "4,56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1.23", "4.56"); + testCases.Add(culture, exclusive, new(type, "1.23", "4.56") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "1.23", "4.56"); + } + } + } + + // Numbers using numeric format, such as with thousands separators + testCases.Add("en-GB", false, new(typeof(float), "-12,445.7", "12,445.7"), "-12445.7", "12445.7"); + testCases.Add("fr-FR", false, new(typeof(float), "-12 445,7", "12 445,7"), "-12445.7", "12445.7"); + testCases.Add("sv-SE", false, new(typeof(float), "-12 445,7", "12 445,7"), "-12445.7", "12445.7"); + + // Decimal value that would lose precision if parsed as a float or double + foreach (var exclusive in isExclusive) + { + testCases.Add("en-US", exclusive, new(typeof(decimal), "12345678901234567890.123456789", "12345678901234567890.123456789") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "12345678901234567890.123456789", "12345678901234567890.123456789"); + testCases.Add("en-US", exclusive, new(typeof(decimal), "12345678901234567890.123456789", "12345678901234567890.123456789") { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive, ParseLimitsInInvariantCulture = true }, "12345678901234567890.123456789", "12345678901234567890.123456789"); + } + + return testCases; + } + + [Theory] + [MemberData(nameof(TestCases))] + public static void ApplyValidationAttributes_Handles_RangeAttribute_Correctly( + string cultureName, + bool isExclusive, + RangeAttribute rangeAttribute, + string expectedMinimum, + string expectedMaximum) + { + // Arrange + var minimum = decimal.Parse(expectedMinimum, CultureInfo.InvariantCulture); + var maximum = decimal.Parse(expectedMaximum, CultureInfo.InvariantCulture); + + var schema = new JsonObject(); + + // Act + var previous = CultureInfo.CurrentCulture; + + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(cultureName); + + schema.ApplyValidationAttributes([rangeAttribute]); + } + finally + { + CultureInfo.CurrentCulture = previous; + } + + // Assert + if (isExclusive) + { + Assert.Equal(minimum, schema["exclusiveMinimum"].GetValue()); + Assert.Equal(maximum, schema["exclusiveMaximum"].GetValue()); + Assert.False(schema.TryGetPropertyValue("minimum", out _)); + Assert.False(schema.TryGetPropertyValue("maximum", out _)); + } + else + { + Assert.Equal(minimum, schema["minimum"].GetValue()); + Assert.Equal(maximum, schema["maximum"].GetValue()); + Assert.False(schema.TryGetPropertyValue("exclusiveMinimum", out _)); + Assert.False(schema.TryGetPropertyValue("exclusiveMaximum", out _)); + } + } + + [Fact] + public static void ApplyValidationAttributes_Handles_Invalid_RangeAttribute_Values() + { + // Arrange + var rangeAttribute = new RangeAttribute(typeof(int), "foo", "bar"); + var schema = new JsonObject(); + + // Act + schema.ApplyValidationAttributes([rangeAttribute]); + + // Assert + Assert.False(schema.TryGetPropertyValue("minimum", out _)); + Assert.False(schema.TryGetPropertyValue("maximum", out _)); + Assert.False(schema.TryGetPropertyValue("exclusiveMinimum", out _)); + Assert.False(schema.TryGetPropertyValue("exclusiveMaximum", out _)); + } +} diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/LocalizedSampleAppFixture.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/LocalizedSampleAppFixture.cs new file mode 100644 index 000000000000..1aee02e9f053 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/LocalizedSampleAppFixture.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +// Runs requests with a culture that uses commas to format decimals to +// verify the invariant culture is used to generate the OpenAPI document. + +public sealed class LocalizedSampleAppFixture : SampleAppFixture +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + base.ConfigureWebHost(builder); + + builder.ConfigureServices(services => + { + services.AddTransient(); + services.AddRequestLocalization((options) => + { + options.DefaultRequestCulture = new("fr-FR"); + options.SupportedCultures = [new("fr-FR")]; + options.SupportedUICultures = [new("fr-FR")]; + }); + }); + } + + private sealed class AddLocalizationMiddlewareFilter : IStartupFilter + { + public Action Configure(Action next) + { + return (app) => + { + app.UseRequestLocalization(); + next(app); + }; + } + } +} diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs index 95959eb526d0..1ce73bd3e57c 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentIntegrationTests.cs @@ -4,26 +4,36 @@ using Microsoft.AspNetCore.InternalTesting; using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.DependencyInjection; -using Microsoft.OpenApi; [UsesVerify] public sealed class OpenApiDocumentIntegrationTests(SampleAppFixture fixture) : IClassFixture { + public static TheoryData OpenApiDocuments() + { + OpenApiSpecVersion[] versions = + [ + OpenApiSpecVersion.OpenApi3_0, + OpenApiSpecVersion.OpenApi3_1, + ]; + + var testCases = new TheoryData(); + + foreach (var version in versions) + { + testCases.Add("v1", version); + testCases.Add("v2", version); + testCases.Add("controllers", version); + testCases.Add("responses", version); + testCases.Add("forms", version); + testCases.Add("schemas-by-ref", version); + testCases.Add("xml", version); + } + + return testCases; + } + [Theory] - [InlineData("v1", OpenApiSpecVersion.OpenApi3_0)] - [InlineData("v2", OpenApiSpecVersion.OpenApi3_0)] - [InlineData("controllers", OpenApiSpecVersion.OpenApi3_0)] - [InlineData("responses", OpenApiSpecVersion.OpenApi3_0)] - [InlineData("forms", OpenApiSpecVersion.OpenApi3_0)] - [InlineData("schemas-by-ref", OpenApiSpecVersion.OpenApi3_0)] - [InlineData("xml", OpenApiSpecVersion.OpenApi3_0)] - [InlineData("v1", OpenApiSpecVersion.OpenApi3_1)] - [InlineData("v2", OpenApiSpecVersion.OpenApi3_1)] - [InlineData("controllers", OpenApiSpecVersion.OpenApi3_1)] - [InlineData("responses", OpenApiSpecVersion.OpenApi3_1)] - [InlineData("forms", OpenApiSpecVersion.OpenApi3_1)] - [InlineData("schemas-by-ref", OpenApiSpecVersion.OpenApi3_1)] - [InlineData("xml", OpenApiSpecVersion.OpenApi3_1)] + [MemberData(nameof(OpenApiDocuments))] public async Task VerifyOpenApiDocument(string documentName, OpenApiSpecVersion version) { var documentService = fixture.Services.GetRequiredKeyedService(documentName); @@ -34,7 +44,7 @@ public async Task VerifyOpenApiDocument(string documentName, OpenApiSpecVersion ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "Integration", "snapshots") : "snapshots"; var outputDirectory = Path.Combine(baseSnapshotsDirectory, version.ToString()); - await Verifier.Verify(json) + await Verify(json) .UseDirectory(outputDirectory) .UseParameters(documentName); } diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentLocalizationTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentLocalizationTests.cs new file mode 100644 index 000000000000..e2480db6e048 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/OpenApiDocumentLocalizationTests.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.InternalTesting; + +[UsesVerify] +public sealed class OpenApiDocumentLocalizationTests(LocalizedSampleAppFixture fixture) : IClassFixture +{ + [Fact] + public async Task VerifyOpenApiDocumentIsInvariant() + { + using var client = fixture.CreateClient(); + var json = await client.GetStringAsync("/openapi/localized.json"); + var outputDirectory = SkipOnHelixAttribute.OnHelix() + ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "Integration", "snapshots") + : "snapshots"; + await Verify(json) + .UseDirectory(outputDirectory); + } +} diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt index ce5cd62b4ecf..5728ecec9c3c 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt @@ -105,10 +105,41 @@ } } } + }, + "/getcultureinvariant": { + "get": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + } + } + } + } + } } }, "components": { "schemas": { + "CurrentWeather": { + "type": "object", + "properties": { + "temperature": { + "maximum": 100.5, + "minimum": -100.5, + "type": "number", + "format": "float", + "default": 0.1 + } + } + }, "MvcTodo": { "required": [ "title", diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt index efb88cb71d5e..b6e1a4692d89 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=controllers.verified.txt @@ -105,10 +105,41 @@ } } } + }, + "/getcultureinvariant": { + "get": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + } + } + } + } + } } }, "components": { "schemas": { + "CurrentWeather": { + "type": "object", + "properties": { + "temperature": { + "maximum": 100.5, + "minimum": -100.5, + "type": "number", + "format": "float", + "default": 0.1 + } + } + }, "MvcTodo": { "required": [ "title", diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt new file mode 100644 index 000000000000..2567b12e4e02 --- /dev/null +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt @@ -0,0 +1,1863 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Sample | localized", + "description": "This is a localized OpenAPI document for français (France).", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://localhost/" + } + ], + "paths": { + "/forms/form-file": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "resume" + ], + "type": "object", + "properties": { + "resume": { + "$ref": "#/components/schemas/IFormFile" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/forms/form-files": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "files" + ], + "type": "object", + "properties": { + "files": { + "$ref": "#/components/schemas/IFormFileCollection" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/forms/form-file-multiple": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "resume", + "files" + ], + "type": "object", + "allOf": [ + { + "type": "object", + "properties": { + "resume": { + "$ref": "#/components/schemas/IFormFile" + } + } + }, + { + "type": "object", + "properties": { + "files": { + "$ref": "#/components/schemas/IFormFileCollection" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/forms/form-todo": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/forms/forms-pocos-and-files": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "required": [ + "file" + ], + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/Todo" + }, + { + "type": "object", + "properties": { + "file": { + "$ref": "#/components/schemas/IFormFile" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/array-of-guids": { + "get": { + "tags": [ + "Sample" + ], + "parameters": [ + { + "name": "guids", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } + } + } + } + } + }, + "/v1/todos": { + "post": { + "tags": [ + "Sample" + ], + "summary": "Creates a new todo item.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/todos/{id}": { + "get": { + "tags": [ + "Sample" + ], + "description": "Returns a specific todo item.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoWithDueDate" + } + } + } + } + } + } + }, + "/v2/users": { + "get": { + "tags": [ + "users" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Sample" + ], + "operationId": "CreateUser", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/type-with-examples": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithExamples" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypeWithExamples" + } + } + } + } + } + } + }, + "/xml/todo": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoFomInterface" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/project": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/board": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoardItem" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/project-record": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectRecord" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/xml/todo-with-description": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TodoWithDescription" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/Xml": { + "get": { + "tags": [ + "Xml" + ], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "The name of the person.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Returns the greeting.", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "tags": [ + "Xml" + ], + "requestBody": { + "description": "The todo to insert into the database.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/schemas-by-ref/typed-results": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + } + } + } + }, + "/schemas-by-ref/multiple-results": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/schemas-by-ref/iresult-no-produces": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/iresult-with-produces": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/xml": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + } + } + } + }, + "/schemas-by-ref/primitives": { + "get": { + "tags": [ + "Sample" + ], + "parameters": [ + { + "name": "id", + "in": "query", + "description": "The ID associated with the Todo item.", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "size", + "in": "query", + "description": "The number of Todos to fetch", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/product": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + }, + "/schemas-by-ref/account": { + "get": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Account" + } + } + } + } + } + } + }, + "/schemas-by-ref/array-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/list-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/ienumerable-of-ints": { + "post": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + }, + "/schemas-by-ref/dictionary-of-ints": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + } + }, + "/schemas-by-ref/frozen-dictionary-of-ints": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + } + } + }, + "/schemas-by-ref/shape": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shape" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/weatherforecastbase": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WeatherForecastBase" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/person": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Person" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/category": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Category" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/container": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContainerType" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/root": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Root" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/location": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocationContainer" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/parent": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/schemas-by-ref/child": { + "post": { + "tags": [ + "Sample" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChildObject" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/responses/200-add-xml": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + }, + "text/xml": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + } + } + } + } + }, + "/responses/200-only-xml": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/xml": { + "schema": { + "$ref": "#/components/schemas/Todo" + } + } + } + } + } + } + }, + "/responses/triangle": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Triangle" + } + } + } + } + } + } + }, + "/responses/shape": { + "get": { + "tags": [ + "Sample" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shape" + } + } + } + } + } + } + }, + "/getbyidandname/{id}/{name}": { + "get": { + "tags": [ + "Test" + ], + "parameters": [ + { + "name": "Id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "Name", + "in": "path", + "required": true, + "schema": { + "minLength": 5, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/gettypedresult": { + "get": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MvcTodo" + } + } + } + } + } + } + }, + "/forms": { + "post": { + "tags": [ + "Test" + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "Title": { + "type": "string" + }, + "Description": { + "type": "string" + }, + "IsCompleted": { + "type": "boolean" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/getcultureinvariant": { + "get": { + "tags": [ + "Test" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrentWeather" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Account": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "AddressDto": { + "required": [ + "relatedLocation" + ], + "type": "object", + "properties": { + "relatedLocation": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "BoardItem": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "description": "An item on the board." + }, + "Category": { + "required": [ + "name", + "parent" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/components/schemas/Category" + }, + "tags": { + "$ref": "#/components/schemas/Category/properties/parent/properties/tags" + } + } + }, + "ChildObject": { + "required": [ + "parent" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "parent": { + "$ref": "#/components/schemas/ParentObject" + } + } + }, + "ContainerType": { + "type": "object", + "properties": { + "seq1": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "seq2": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContainerType/properties/seq1/items" + } + } + } + }, + "CurrentWeather": { + "type": "object", + "properties": { + "temperature": { + "maximum": 100.5, + "minimum": -100.5, + "type": "number", + "format": "float", + "default": 0.1 + } + } + }, + "IFormFile": { + "type": "string", + "format": "binary" + }, + "IFormFileCollection": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IFormFile" + } + }, + "Item": { + "type": "object", + "properties": { + "name": { + "$ref": "#/components/schemas/Root/properties/item1/properties/name" + }, + "value": { + "type": "integer", + "format": "int32" + } + } + }, + "LocationContainer": { + "required": [ + "location" + ], + "type": "object", + "properties": { + "location": { + "$ref": "#/components/schemas/LocationDto" + } + } + }, + "LocationDto": { + "required": [ + "address" + ], + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/AddressDto" + } + } + }, + "MvcTodo": { + "required": [ + "title", + "description", + "isCompleted" + ], + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "isCompleted": { + "type": "boolean" + } + } + }, + "ParentObject": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChildObject" + } + } + } + }, + "Person": { + "required": [ + "discriminator" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/PersonStudent" + }, + { + "$ref": "#/components/schemas/PersonTeacher" + } + ], + "discriminator": { + "propertyName": "discriminator", + "mapping": { + "student": "#/components/schemas/PersonStudent", + "teacher": "#/components/schemas/PersonTeacher" + } + } + }, + "PersonStudent": { + "properties": { + "discriminator": { + "enum": [ + "student" + ], + "type": "string" + }, + "gpa": { + "type": "number", + "format": "double" + } + } + }, + "PersonTeacher": { + "required": [ + "subject" + ], + "properties": { + "discriminator": { + "enum": [ + "teacher" + ], + "type": "string" + }, + "subject": { + "type": "string" + } + } + }, + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + } + } + }, + "Project": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "description": "The project that contains Todo items." + }, + "ProjectRecord": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the project." + }, + "description": { + "type": "string", + "description": "The description of the project." + } + }, + "description": "The project that contains Todo items." + }, + "Root": { + "type": "object", + "properties": { + "item1": { + "$ref": "#/components/schemas/Item" + }, + "item2": { + "$ref": "#/components/schemas/Item" + } + } + }, + "Shape": { + "required": [ + "$type" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/ShapeTriangle" + }, + { + "$ref": "#/components/schemas/ShapeSquare" + } + ], + "discriminator": { + "propertyName": "$type", + "mapping": { + "triangle": "#/components/schemas/ShapeTriangle", + "square": "#/components/schemas/ShapeSquare" + } + } + }, + "ShapeSquare": { + "properties": { + "$type": { + "enum": [ + "square" + ], + "type": "string" + }, + "area": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "ShapeTriangle": { + "properties": { + "$type": { + "enum": [ + "triangle" + ], + "type": "string" + }, + "hypotenuse": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "Tag": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "type": "string" + } + } + }, + "Todo": { + "required": [ + "id", + "title", + "completed", + "createdAt" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The unique identifier of the to-do item.", + "format": "int32" + }, + "title": { + "type": "string", + "description": "The title of the to-do item." + }, + "completed": { + "type": "boolean", + "description": "Indicates whether the to-do item is completed." + }, + "createdAt": { + "type": "string", + "description": "The date and time when the to-do item was created.", + "format": "date-time" + } + }, + "description": "Represents a to-do item." + }, + "TodoFomInterface": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The identifier of the todo.", + "format": "int32" + }, + "name": { + "type": "string", + "description": "The name of the todo." + }, + "description": { + "type": "string", + "description": "A description of the todo." + } + }, + "description": "This is a todo item." + }, + "TodoWithDescription": { + "required": [ + "name", + "description" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The identifier of the todo, overridden.", + "format": "int32" + }, + "name": { + "type": "string", + "description": "The name of the todo, overridden." + }, + "description": { + "type": "string", + "description": "Another description of the todo." + } + } + }, + "TodoWithDueDate": { + "required": [ + "dueDate", + "id", + "title", + "completed", + "createdAt" + ], + "type": "object", + "properties": { + "dueDate": { + "type": "string", + "description": "The due date of the to-do item.", + "format": "date-time" + }, + "id": { + "type": "integer", + "description": "The unique identifier of the to-do item.", + "format": "int32" + }, + "title": { + "type": "string", + "description": "The title of the to-do item." + }, + "completed": { + "type": "boolean", + "description": "Indicates whether the to-do item is completed." + }, + "createdAt": { + "type": "string", + "description": "The date and time when the to-do item was created.", + "format": "date-time" + } + }, + "description": "Represents a to-do item with a due date." + }, + "Triangle": { + "type": "object", + "properties": { + "hypotenuse": { + "type": "number", + "format": "double" + }, + "color": { + "type": "string" + }, + "sides": { + "type": "integer", + "format": "int32" + } + } + }, + "TypeWithExamples": { + "type": "object", + "properties": { + "booleanType": { + "type": "boolean", + "example": true + }, + "integerType": { + "type": "integer", + "format": "int32", + "example": 42 + }, + "longType": { + "type": "integer", + "format": "int64", + "example": 1234567890123456789 + }, + "doubleType": { + "type": "number", + "format": "double", + "example": 3.14 + }, + "floatType": { + "type": "number", + "format": "float", + "example": 3.14 + }, + "dateTimeType": { + "type": "string", + "format": "date-time", + "example": "2022-01-01T00:00:00Z" + }, + "dateOnlyType": { + "type": "string", + "format": "date", + "example": "2022-01-01" + } + } + }, + "WeatherForecastBase": { + "required": [ + "$type" + ], + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity" + }, + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries" + }, + { + "$ref": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews" + } + ], + "discriminator": { + "propertyName": "$type", + "mapping": { + "0": "#/components/schemas/WeatherForecastBaseWeatherForecastWithCity", + "1": "#/components/schemas/WeatherForecastBaseWeatherForecastWithTimeSeries", + "2": "#/components/schemas/WeatherForecastBaseWeatherForecastWithLocalNews" + } + } + }, + "WeatherForecastBaseWeatherForecastWithCity": { + "required": [ + "city" + ], + "properties": { + "$type": { + "enum": [ + 0 + ], + "type": "integer" + }, + "city": { + "type": "string" + } + } + }, + "WeatherForecastBaseWeatherForecastWithLocalNews": { + "required": [ + "news" + ], + "properties": { + "$type": { + "enum": [ + 2 + ], + "type": "integer" + }, + "news": { + "type": "string" + } + } + }, + "WeatherForecastBaseWeatherForecastWithTimeSeries": { + "required": [ + "summary" + ], + "properties": { + "$type": { + "enum": [ + 1 + ], + "type": "integer" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "temperatureC": { + "type": "integer", + "format": "int32" + }, + "summary": { + "type": "string" + } + } + } + } + }, + "tags": [ + { + "name": "Sample" + }, + { + "name": "users" + }, + { + "name": "Xml" + }, + { + "name": "Test" + } + ] +} \ No newline at end of file diff --git a/src/SignalR/common/Shared/Utf8BufferTextWriter.cs b/src/SignalR/common/Shared/Utf8BufferTextWriter.cs index 6c993f11be7a..f86432af249a 100644 --- a/src/SignalR/common/Shared/Utf8BufferTextWriter.cs +++ b/src/SignalR/common/Shared/Utf8BufferTextWriter.cs @@ -35,6 +35,12 @@ public Utf8BufferTextWriter() _encoder = _utf8NoBom.GetEncoder(); } + public Utf8BufferTextWriter(IFormatProvider formatProvider) + : base(formatProvider) + { + _encoder = _utf8NoBom.GetEncoder(); + } + public static Utf8BufferTextWriter Get(IBufferWriter bufferWriter) { var writer = _cachedInstance; diff --git a/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs b/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs index 9b8947f8da36..0bd4ed9be2dd 100644 --- a/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs +++ b/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs @@ -330,7 +330,7 @@ private string GetDocument( _reporter.WriteInformation(Resources.FormatGeneratingDocument(documentName)); using var stream = new MemoryStream(); - using (var writer = new StreamWriter(stream, _utf8EncodingWithoutBOM, bufferSize: 1024, leaveOpen: true)) + using (var writer = new InvariantStreamWriter(stream, _utf8EncodingWithoutBOM, bufferSize: 1024, leaveOpen: true)) { var targetMethod = generateWithVersionMethod ?? generateMethod; object[] arguments = [documentName, writer]; @@ -464,6 +464,12 @@ private object InvokeMethod(MethodInfo method, object instance, object[] argumen return result; } + private sealed class InvariantStreamWriter(Stream stream, Encoding? encoding = null, int bufferSize = -1, bool leaveOpen = false) + : StreamWriter(stream, encoding, bufferSize, leaveOpen) + { + public override IFormatProvider FormatProvider => System.Globalization.CultureInfo.InvariantCulture; + } + #if NET7_0_OR_GREATER private sealed class NoopHostLifetime : IHostLifetime {