Skip to content

Commit 4df28e3

Browse files
committed
Harden parsing of [Range] attribute values (#59043)
1 parent a7d5576 commit 4df28e3

File tree

2 files changed

+77
-2
lines changed

2 files changed

+77
-2
lines changed

src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,23 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable
9191
}
9292
else if (attribute is RangeAttribute rangeAttribute)
9393
{
94-
schema[OpenApiSchemaKeywords.MinimumKeyword] = decimal.Parse(rangeAttribute.Minimum.ToString()!, CultureInfo.InvariantCulture);
95-
schema[OpenApiSchemaKeywords.MaximumKeyword] = decimal.Parse(rangeAttribute.Maximum.ToString()!, CultureInfo.InvariantCulture);
94+
// Use InvariantCulture if explicitly requested or if the range has been set via the
95+
// RangeAttribute(double, double) or RangeAttribute(int, int) constructors.
96+
var targetCulture = rangeAttribute.ParseLimitsInInvariantCulture || rangeAttribute.Minimum is double || rangeAttribute.Maximum is int
97+
? CultureInfo.InvariantCulture
98+
: CultureInfo.CurrentCulture;
99+
100+
var minString = rangeAttribute.Minimum.ToString();
101+
var maxString = rangeAttribute.Maximum.ToString();
102+
103+
if (decimal.TryParse(minString, NumberStyles.Any, targetCulture, out var minDecimal))
104+
{
105+
schema[OpenApiSchemaKeywords.MinimumKeyword] = minDecimal;
106+
}
107+
if (decimal.TryParse(maxString, NumberStyles.Any, targetCulture, out var maxDecimal))
108+
{
109+
schema[OpenApiSchemaKeywords.MaximumKeyword] = maxDecimal;
110+
}
96111
}
97112
else if (attribute is RegularExpressionAttribute regularExpressionAttribute)
98113
{

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
using System.ComponentModel;
55
using System.ComponentModel.DataAnnotations;
6+
using System.Globalization;
67
using System.Text.Json;
78
using System.Text.Json.Serialization;
89
using Microsoft.AspNetCore.Builder;
910
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.InternalTesting;
1012
using Microsoft.AspNetCore.Mvc;
1113
using Microsoft.AspNetCore.Routing;
1214
using Microsoft.Extensions.DependencyInjection;
@@ -338,6 +340,7 @@ await VerifyOpenApiDocument(action, document =>
338340
[([MinLength(2)] int[] id) => {}, (OpenApiSchema schema) => Assert.Equal(2, schema.MinItems)],
339341
[([Length(4, 8)] int[] id) => {}, (OpenApiSchema schema) => { Assert.Equal(4, schema.MinItems); Assert.Equal(8, schema.MaxItems); }],
340342
[([Range(4, 8)]int id) => {}, (OpenApiSchema schema) => { Assert.Equal(4, schema.Minimum); Assert.Equal(8, schema.Maximum); }],
343+
[([Range(typeof(DateTime), "2024-02-01", "2024-02-031")] DateTime id) => {}, (OpenApiSchema schema) => { Assert.Null(schema.Minimum); Assert.Null(schema.Maximum); }],
341344
[([StringLength(10)] string name) => {}, (OpenApiSchema schema) => { Assert.Equal(10, schema.MaxLength); Assert.Equal(0, schema.MinLength); }],
342345
[([StringLength(10, MinimumLength = 5)] string name) => {}, (OpenApiSchema schema) => { Assert.Equal(10, schema.MaxLength); Assert.Equal(5, schema.MinLength); }],
343346
[([Url] string url) => {}, (OpenApiSchema schema) => { Assert.Equal("string", schema.Type); Assert.Equal("uri", schema.Format); }],
@@ -365,6 +368,63 @@ await VerifyOpenApiDocument(builder, document =>
365368
});
366369
}
367370

371+
public static object[][] RouteParametersWithRangeAttributes =>
372+
[
373+
[([Range(4, 8)] int id) => {}, (OpenApiSchema schema) => { Assert.Equal(4, schema.Minimum); Assert.Equal(8, schema.Maximum); }],
374+
[([Range(int.MinValue, int.MaxValue)] int id) => {}, (OpenApiSchema schema) => { Assert.Equal(int.MinValue, schema.Minimum); Assert.Equal(int.MaxValue, schema.Maximum); }],
375+
[([Range(0, double.MaxValue)] double id) => {}, (OpenApiSchema schema) => { Assert.Equal(0, schema.Minimum); Assert.Null(schema.Maximum); }],
376+
[([Range(typeof(double), "0", "1.79769313486232E+308")] double id) => {}, (OpenApiSchema schema) => { Assert.Equal(0, schema.Minimum); Assert.Null(schema.Maximum); }],
377+
[([Range(typeof(long), "-9223372036854775808", "9223372036854775807")] long id) => {}, (OpenApiSchema schema) => { Assert.Equal(long.MinValue, schema.Minimum); Assert.Equal(long.MaxValue, schema.Maximum); }],
378+
[([Range(typeof(DateTime), "2024-02-01", "2024-02-031")] DateTime id) => {}, (OpenApiSchema schema) => { Assert.Null(schema.Minimum); Assert.Null(schema.Maximum); }],
379+
];
380+
381+
[Theory]
382+
[MemberData(nameof(RouteParametersWithRangeAttributes))]
383+
public async Task GetOpenApiParameters_HandlesRouteParametersWithRangeAttributes(Delegate requestHandler, Action<OpenApiSchema> verifySchema)
384+
{
385+
// Arrange
386+
var builder = CreateBuilder();
387+
388+
// Act
389+
builder.MapGet("/api/{id}", requestHandler);
390+
391+
// Assert
392+
await VerifyOpenApiDocument(builder, document =>
393+
{
394+
var operation = document.Paths["/api/{id}"].Operations[OperationType.Get];
395+
var parameter = Assert.Single(operation.Parameters);
396+
verifySchema(parameter.Schema);
397+
});
398+
}
399+
400+
public static object[][] RouteParametersWithRangeAttributes_CultureInfo =>
401+
[
402+
[([Range(typeof(DateTime), "2024-02-01", "2024-02-031")] DateTime id) => {}, (OpenApiSchema schema) => { Assert.Null(schema.Minimum); Assert.Null(schema.Maximum); }],
403+
[([Range(typeof(decimal), "1,99", "3,99")] decimal id) => {}, (OpenApiSchema schema) => { Assert.Equal(1.99m, schema.Minimum); Assert.Equal(3.99m, schema.Maximum); }],
404+
[([Range(typeof(decimal), "1,99", "3,99", ParseLimitsInInvariantCulture = true)] decimal id) => {}, (OpenApiSchema schema) => { Assert.Equal(199, schema.Minimum); Assert.Equal(399, schema.Maximum); }],
405+
[([Range(1000, 2000)] int id) => {}, (OpenApiSchema schema) => { Assert.Equal(1000, schema.Minimum); Assert.Equal(2000, schema.Maximum); }]
406+
];
407+
408+
[Theory]
409+
[MemberData(nameof(RouteParametersWithRangeAttributes_CultureInfo))]
410+
[UseCulture("fr-FR")]
411+
public async Task GetOpenApiParameters_HandlesRouteParametersWithRangeAttributes_CultureInfo(Delegate requestHandler, Action<OpenApiSchema> verifySchema)
412+
{
413+
// Arrange
414+
var builder = CreateBuilder();
415+
416+
// Act
417+
builder.MapGet("/api/{id}", requestHandler);
418+
419+
// Assert
420+
await VerifyOpenApiDocument(builder, document =>
421+
{
422+
var operation = document.Paths["/api/{id}"].Operations[OperationType.Get];
423+
var parameter = Assert.Single(operation.Parameters);
424+
verifySchema(parameter.Schema);
425+
});
426+
}
427+
368428
[Fact]
369429
public async Task GetOpenApiParameters_HandlesParametersWithRequiredAttribute()
370430
{

0 commit comments

Comments
 (0)