Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.OpenApi.Models;
Expand Down Expand Up @@ -229,8 +230,27 @@ private static void ApplyBase64Attribute(OpenApiSchema schema)

private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute rangeAttribute)
{
#if NET
if (rangeAttribute.Maximum is int maximumInteger)
{
// The range was set with the RangeAttribute(int, int) constructor
schema.Maximum = maximumInteger;
schema.Minimum = (int)rangeAttribute.Minimum;
}
else
{
// Parse the range from the RangeAttribute(double, double) or RangeAttribute(string, string) constructor.
// Use the appropriate culture as the user may have specified a culture-specific format for the numbers
// if they specified the value as a string. By default RangeAttribute uses the current culture, but it
// can be set to use the invariant culture.
var targetCulture = rangeAttribute.ParseLimitsInInvariantCulture
? CultureInfo.InvariantCulture
: CultureInfo.CurrentCulture;

schema.Maximum = Convert.ToDecimal(rangeAttribute.Maximum, targetCulture);
schema.Minimum = Convert.ToDecimal(rangeAttribute.Minimum, targetCulture);
}

#if NET
if (rangeAttribute.MinimumIsExclusive)
{
schema.ExclusiveMinimum = true;
Expand All @@ -240,16 +260,7 @@ private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute ran
{
schema.ExclusiveMaximum = true;
}

#endif

schema.Maximum = decimal.TryParse(rangeAttribute.Maximum.ToString(), out decimal maximum)
? maximum
: schema.Maximum;

schema.Minimum = decimal.TryParse(rangeAttribute.Minimum.ToString(), out decimal minimum)
? minimum
: schema.Minimum;
}

private static void ApplyRangeRouteConstraint(OpenApiSchema schema, RangeRouteConstraint rangeRouteConstraint)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.OpenApi.Models;

namespace Swashbuckle.AspNetCore.SwaggerGen.Test;

public static class OpenApiSchemaExtensionsTests
{
public static TheoryData<string, bool, RangeAttribute, string, string> 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<string, bool, RangeAttribute, string, string>();

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(1, 1234) { MaximumIsExclusive = exclusive, MinimumIsExclusive = exclusive }, "1", "1234");
Comment thread
martincostello marked this conversation as resolved.
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 OpenApiSchema();

// Act
using (CultureSwitcher.UseCulture(cultureName))
{
schema.ApplyValidationAttributes([rangeAttribute]);
}

// Assert
Assert.Equal(isExclusive ? true : null, schema.ExclusiveMinimum);
Assert.Equal(isExclusive ? true : null, schema.ExclusiveMaximum);
Assert.Equal(minimum, schema.Minimum);
Assert.Equal(maximum, schema.Maximum);
}

private sealed class CultureSwitcher : IDisposable
{
private readonly CultureInfo _previous;

private CultureSwitcher(string name)
{
_previous = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(name);
Comment thread
martincostello marked this conversation as resolved.
}

public static CultureSwitcher UseCulture(string name) => new(name);

public void Dispose()
{
if (_previous is not null)
{
CultureInfo.CurrentCulture = _previous;
}
}
}
}