From 5238e087908f3265e78203c251fba7a85b56f614 Mon Sep 17 00:00:00 2001 From: avgalex <6c65787870@protonmail.ch> Date: Thu, 9 Jul 2026 10:03:39 +0300 Subject: [PATCH] fix(#222): recognize small integer types in IsNumeric so numeric bounds are emitted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validation bounds whose value was a small integer type (short/byte/ushort/uint/ ulong/sbyte) were silently dropped: the Between/Comparison rules matched, but the apply action skipped the bound because IsNumeric() did not recognize the boxed value, so minimum/maximum never reached the schema — even though the generator emits "type": "integer" for the property. This could not be worked around at the validator definition site, since e.g. InclusiveBetween on a short property only accepts short bounds. Fix: IsNumeric (shared core, MicroElements.OpenApi.FluentValidation) now recognizes all integer primitives (sbyte/byte/short/ushort/int/uint/long/ulong) alongside the floating/decimal/BigInteger types. NumericToDecimal already converted them all, so this single change fixes all three providers at once (Swashbuckle, NSwag, and the native Microsoft.AspNetCore.OpenApi transformer). Adds regression tests for InclusiveBetween/ExclusiveBetween and the comparison rules across the small integer types (verified red without the fix, green with it). Bumps version 7.1.8 -> 7.1.9 and updates CHANGELOG. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 + .../Core/Extensions.cs | 10 +- .../Issue222SmallIntegerBoundsTest.cs | 113 ++++++++++++++++++ version.props | 2 +- 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 test/MicroElements.Swashbuckle.FluentValidation.Tests/Issue222SmallIntegerBoundsTest.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f735b2..79c6c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Changes in 7.1.9 +- Fixed: numeric bounds from `short`/`byte`/`ushort`/`uint`/`ulong`/`sbyte`-typed rules were silently dropped (Issue #222) + - `IsNumeric` in the shared core (`MicroElements.OpenApi.FluentValidation`) only recognized `int`/`long`/`float`/`double`/`decimal`/`BigInteger`, so a `Between`/`Comparison` rule whose bound was a small integer type (e.g. `InclusiveBetween((short)1, (short)99)`) matched but produced no `minimum`/`maximum` — even though the generator emits `"type": "integer"` for the property. This could not be worked around at the validator definition site because those rule overloads only accept bounds of the property's own type + - Fix: `IsNumeric` now recognizes all integer primitives (`sbyte`/`byte`/`short`/`ushort`/`int`/`uint`/`long`/`ulong`) in addition to the floating/decimal/`BigInteger` types; `NumericToDecimal` already converted them all. One change in the shared core fixes all three providers (Swashbuckle, NSwag, and the native `Microsoft.AspNetCore.OpenApi` transformer) + # Changes in 7.1.8 - Security (Issue #220): closed a transitive high-severity advisory in the published package. `Swashbuckle.AspNetCore.SwaggerGen` on the net10.0 target was bumped `10.0.0` → `10.2.1`, which resolves `Microsoft.OpenApi` to the patched `2.7.5` (was `2.3.0`). This clears **GHSA-v5pm-xwqc-g5wc / CVE-2026-49451** (CWE-674 uncontrolled recursion — a circular `$ref` schema could stack-overflow the OpenAPI reader). The net8.0/net9.0 targets use Swashbuckle 8.1.1 → Microsoft.OpenApi v1 and were never in the advisory range - Media type & file size validation for `IFormFile` uploads (Issue #216): stable rollup of everything in `7.1.8-beta.1` and `7.1.8-beta.2` below (new File-level rules `.FileContentType()`, `.MaxFileSize()`, `.MinFileSize()`, `.FileSizeBetween()`; Swashbuckle / NSwag / Microsoft.AspNetCore.OpenApi emit multipart `encoding.contentType` and description annotations) diff --git a/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs b/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs index aeea6e6..3bbd2d5 100644 --- a/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs +++ b/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs @@ -16,7 +16,15 @@ public static class Extensions /// /// Is supported swagger numeric type. /// - internal static bool IsNumeric(this object value) => value is int || value is long || value is float || value is double || value is decimal || value is BigInteger; + /// + /// Issue #222: all integer primitives must be recognized. `short`/`byte`/`ushort`/`uint`/`ulong`/`sbyte` + /// bounds (e.g. from InclusiveBetween((short)1, (short)99)) were dropped because they were absent + /// here, so minimum/maximum were never emitted. already + /// converts them all via . + /// + internal static bool IsNumeric(this object value) => + value is sbyte or byte or short or ushort or int or uint or long or ulong + or float or double or decimal or BigInteger; /// /// Convert numeric to decimal. diff --git a/test/MicroElements.Swashbuckle.FluentValidation.Tests/Issue222SmallIntegerBoundsTest.cs b/test/MicroElements.Swashbuckle.FluentValidation.Tests/Issue222SmallIntegerBoundsTest.cs new file mode 100644 index 0000000..767f360 --- /dev/null +++ b/test/MicroElements.Swashbuckle.FluentValidation.Tests/Issue222SmallIntegerBoundsTest.cs @@ -0,0 +1,113 @@ +// Copyright (c) MicroElements. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; +using FluentValidation; +using Xunit; + +namespace MicroElements.Swashbuckle.FluentValidation.Tests +{ + /// + /// Issue #222: numeric bounds whose value is a small integer type (sbyte/byte/short/ushort/uint/ulong) + /// were silently dropped — the Between/Comparison rules matched, but IsNumeric() did not recognize the + /// boxed value, so minimum/maximum were never emitted. For a `short` property FluentValidation's + /// InclusiveBetween/GreaterThanOrEqualTo overloads only accept short bounds, so this could not be worked + /// around at the validator definition site. + /// https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/issues/222 + /// + public class Issue222SmallIntegerBoundsTest : UnitTestBase + { + public class Model + { + public sbyte SByteValue { get; set; } + public byte ByteValue { get; set; } + public short ShortValue { get; set; } + public ushort UShortValue { get; set; } + public uint UIntValue { get; set; } + public ulong ULongValue { get; set; } + } + + // A fresh SchemaBuilder per case: its SchemaRepository caches the generated component, + // so one builder must generate exactly one rule/schema. + + [Fact] + public void InclusiveBetween_SByte_Should_Emit_Bounds() + { + var schema = new SchemaBuilder().AddRule(x => x.SByteValue, rule => rule.InclusiveBetween((sbyte)1, (sbyte)9)); + schema.GetMinimum().Should().Be(1); + schema.GetMaximum().Should().Be(9); + } + + [Fact] + public void InclusiveBetween_Byte_Should_Emit_Bounds() + { + var schema = new SchemaBuilder().AddRule(x => x.ByteValue, rule => rule.InclusiveBetween((byte)2, (byte)200)); + schema.GetMinimum().Should().Be(2); + schema.GetMaximum().Should().Be(200); + } + + [Fact] + public void InclusiveBetween_Short_Should_Emit_Bounds() + { + var schema = new SchemaBuilder().AddRule(x => x.ShortValue, rule => rule.InclusiveBetween((short)1, (short)99)); + schema.GetMinimum().Should().Be(1); + schema.GetMaximum().Should().Be(99); + } + + [Fact] + public void InclusiveBetween_UShort_Should_Emit_Bounds() + { + var schema = new SchemaBuilder().AddRule(x => x.UShortValue, rule => rule.InclusiveBetween((ushort)3, (ushort)300)); + schema.GetMinimum().Should().Be(3); + schema.GetMaximum().Should().Be(300); + } + + [Fact] + public void InclusiveBetween_UInt_Should_Emit_Bounds() + { + var schema = new SchemaBuilder().AddRule(x => x.UIntValue, rule => rule.InclusiveBetween(4u, 400u)); + schema.GetMinimum().Should().Be(4); + schema.GetMaximum().Should().Be(400); + } + + [Fact] + public void InclusiveBetween_ULong_Should_Emit_Bounds() + { + var schema = new SchemaBuilder().AddRule(x => x.ULongValue, rule => rule.InclusiveBetween(5ul, 5000ul)); + schema.GetMinimum().Should().Be(5); + schema.GetMaximum().Should().Be(5000); + } + + [Fact] + public void ExclusiveBetween_Short_Should_Emit_Exclusive_Bounds() + { + var schema = new SchemaBuilder().AddRule(x => x.ShortValue, rule => rule.ExclusiveBetween((short)1, (short)99)); + schema.GetMinimum().Should().Be(1); + schema.GetMaximum().Should().Be(99); + schema.GetExclusiveMinimum().Should().BeTrue(); + schema.GetExclusiveMaximum().Should().BeTrue(); + } + + [Fact] + public void GreaterThanOrEqualTo_Short_Should_Emit_Minimum() + { + var schema = new SchemaBuilder().AddRule(x => x.ShortValue, rule => rule.GreaterThanOrEqualTo((short)1)); + schema.GetMinimum().Should().Be(1); + } + + [Fact] + public void LessThanOrEqualTo_Byte_Should_Emit_Maximum() + { + var schema = new SchemaBuilder().AddRule(x => x.ByteValue, rule => rule.LessThanOrEqualTo((byte)200)); + schema.GetMaximum().Should().Be(200); + } + + [Fact] + public void GreaterThan_UInt_Should_Emit_Exclusive_Minimum() + { + var schema = new SchemaBuilder().AddRule(x => x.UIntValue, rule => rule.GreaterThan(4u)); + schema.GetMinimum().Should().Be(4); + schema.GetExclusiveMinimum().Should().BeTrue(); + } + } +} diff --git a/version.props b/version.props index 554c461..54df8a4 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 7.1.8 + 7.1.9