diff --git a/CHANGELOG.md b/CHANGELOG.md
index fffe567..942c85a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+# Changes in 7.1.2
+- Added: `BigInteger` support for min/max validation constraints in OpenAPI schema generation (Issue #146)
+ - `IsNumeric()` and `NumericToDecimal()` now handle `BigInteger` values
+ - `BigInteger` properties with GreaterThan, LessThan, InclusiveBetween, ExclusiveBetween rules produce correct `minimum`/`maximum` in Swagger
+ - NSwag provider updated with the same `BigInteger` support
+ - Out-of-range `BigInteger` values (exceeding `decimal` range) are handled gracefully via existing try/catch
+- Fixed: Shared schema mutation when multiple models reference the same `BigInteger` type with different constraints (net10.0)
+ - `ResolveRefProperty` creates an isolated shallow copy before applying rule mutations
+ - Prevents `$ref`-based schema corruption across models in `SchemaRepository`
+- Fixed: Replaced deprecated `PackageLicenseUrl` with `PackageLicenseExpression` (Issue #144)
+- Fixed: Replaced deprecated `PackageIconUrl` with embedded `PackageIcon`
+
# Changes in 7.1.1
- Fixed: Nested object validation not applied for `[FromQuery]` parameters (Issue #162)
- When Swashbuckle decomposes `[FromQuery]` models with nested objects into flat parameters (e.g., `operation.op`), the full dot-path name was used for schema property matching instead of the leaf name (`op`)
diff --git a/src/MicroElements.NSwag.FluentValidation/NSwagFluentValidationRuleProvider.cs b/src/MicroElements.NSwag.FluentValidation/NSwagFluentValidationRuleProvider.cs
index c5d5e59..4cef4c9 100644
--- a/src/MicroElements.NSwag.FluentValidation/NSwagFluentValidationRuleProvider.cs
+++ b/src/MicroElements.NSwag.FluentValidation/NSwagFluentValidationRuleProvider.cs
@@ -157,7 +157,7 @@ public FluentValidationRule[] CreateDefaultRules()
if (comparisonValidator.ValueToCompare.IsNumeric())
{
- var valueToCompare = Convert.ToDecimal(comparisonValidator.ValueToCompare);
+ var valueToCompare = comparisonValidator.ValueToCompare.NumericToDecimal();
var schema = context.Schema.Schema;
var schemaProperty = schema.Properties[context.PropertyKey];
@@ -199,11 +199,11 @@ public FluentValidationRule[] CreateDefaultRules()
{
if (betweenValidator.GetType().IsSubClassOfGeneric(typeof(ExclusiveBetweenValidator<,>)))
{
- schemaProperty.ExclusiveMinimum = Convert.ToDecimal(betweenValidator.From);
+ schemaProperty.ExclusiveMinimum = betweenValidator.From.NumericToDecimal();
}
else
{
- schemaProperty.Minimum = Convert.ToDecimal(betweenValidator.From);
+ schemaProperty.Minimum = betweenValidator.From.NumericToDecimal();
}
if (ShouldSetNotNullableForNumericMinimum)
@@ -214,11 +214,11 @@ public FluentValidationRule[] CreateDefaultRules()
{
if (betweenValidator.GetType().IsSubClassOfGeneric(typeof(ExclusiveBetweenValidator<,>)))
{
- schemaProperty.ExclusiveMaximum = Convert.ToDecimal(betweenValidator.To);
+ schemaProperty.ExclusiveMaximum = betweenValidator.To.NumericToDecimal();
}
else
{
- schemaProperty.Maximum = Convert.ToDecimal(betweenValidator.To);
+ schemaProperty.Maximum = betweenValidator.To.NumericToDecimal();
}
}
},
diff --git a/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs b/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs
index 454fcc5..aeea6e6 100644
--- a/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs
+++ b/src/MicroElements.OpenApi.FluentValidation/Core/Extensions.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Numerics;
namespace MicroElements.OpenApi.Core
{
@@ -15,12 +16,12 @@ 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;
+ 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;
///
- /// Convert numeric to double.
+ /// Convert numeric to decimal.
///
- internal static decimal NumericToDecimal(this object value) => Convert.ToDecimal(value);
+ internal static decimal NumericToDecimal(this object value) => value is BigInteger bigInt ? (decimal)bigInt : Convert.ToDecimal(value);
///
/// Returns not null enumeration.
diff --git a/src/MicroElements.Swashbuckle.FluentValidation/OpenApi/OpenApiSchemaCompatibility.cs b/src/MicroElements.Swashbuckle.FluentValidation/OpenApi/OpenApiSchemaCompatibility.cs
index 0a40c3b..0c82b5c 100644
--- a/src/MicroElements.Swashbuckle.FluentValidation/OpenApi/OpenApiSchemaCompatibility.cs
+++ b/src/MicroElements.Swashbuckle.FluentValidation/OpenApi/OpenApiSchemaCompatibility.cs
@@ -10,6 +10,7 @@
#else
using Microsoft.OpenApi.Models;
#endif
+using Swashbuckle.AspNetCore.SwaggerGen;
namespace MicroElements.OpenApi
{
@@ -185,12 +186,14 @@ public static int PropertiesCount(OpenApiSchema schema)
///
/// Gets property from schema by key.
+ /// On OPENAPI_V2, if the property is a $ref, resolves it via the repository and returns the shared schema.
+ /// The returned schema must not be mutated; use ResolveRefProperty when mutation isolation is needed.
///
- public static OpenApiSchema? GetProperty(OpenApiSchema schema, string key)
+ public static OpenApiSchema? GetProperty(OpenApiSchema schema, string key, SchemaRepository? repository = null)
{
#if OPENAPI_V2
if (schema.Properties?.TryGetValue(key, out var property) == true)
- return property as OpenApiSchema; // Returns null for OpenApiSchemaReference (e.g. enums)
+ return ResolveProperty(property, repository);
return null;
#else
if (schema.Properties?.TryGetValue(key, out var property) == true)
@@ -201,15 +204,18 @@ public static int PropertiesCount(OpenApiSchema schema)
///
/// Tries to get property from schema by key.
+ /// On OPENAPI_V2, if the property is a $ref, resolves it via the repository and returns the shared schema.
+ /// The returned schema must not be mutated; use ResolveRefProperty when mutation isolation is needed.
///
- public static bool TryGetProperty(OpenApiSchema schema, string key, out OpenApiSchema? property)
+ public static bool TryGetProperty(OpenApiSchema schema, string key, out OpenApiSchema? property, SchemaRepository? repository = null)
{
#if OPENAPI_V2
if (schema.Properties?.TryGetValue(key, out var prop) == true)
{
- property = prop as OpenApiSchema;
+ property = ResolveProperty(prop, repository);
return property != null;
}
+
property = null;
return false;
#else
@@ -223,6 +229,54 @@ public static bool TryGetProperty(OpenApiSchema schema, string key, out OpenApiS
#endif
}
+#if OPENAPI_V2
+ ///
+ /// Resolves a property that may be an to an .
+ /// Issue #146, #176: BigInteger and enums are rendered as $ref on OpenAPI v2.
+ ///
+ private static OpenApiSchema? ResolveProperty(IOpenApiSchema property, SchemaRepository? repository)
+ {
+ if (property is OpenApiSchema openApiSchema)
+ return openApiSchema;
+
+ if (property is OpenApiSchemaReference schemaRef && repository != null)
+ {
+ var refId = schemaRef.Reference?.Id;
+ if (refId != null && repository.Schemas.TryGetValue(refId, out var resolved) && resolved is OpenApiSchema resolvedSchema)
+ return resolvedSchema;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Resolves a $ref property, replaces it with an isolated shallow copy in the parent schema,
+ /// and returns the copy. If the property is already a concrete OpenApiSchema, returns it as-is.
+ /// This prevents validation rules from mutating the shared schema in SchemaRepository.
+ ///
+ public static OpenApiSchema? ResolveRefProperty(OpenApiSchema schema, string key, SchemaRepository? repository)
+ {
+ if (schema.Properties == null || !schema.Properties.TryGetValue(key, out var prop))
+ return null;
+
+ if (prop is OpenApiSchema openApiSchema)
+ return openApiSchema;
+
+ if (prop is OpenApiSchemaReference schemaRef && repository != null)
+ {
+ var refId = schemaRef.Reference?.Id;
+ if (refId != null && repository.Schemas.TryGetValue(refId, out var resolved) && resolved is OpenApiSchema resolvedSchema)
+ {
+ var copy = (OpenApiSchema)resolvedSchema.CreateShallowCopy();
+ schema.Properties[key] = copy;
+ return copy;
+ }
+ }
+
+ return null;
+ }
+#endif
+
///
/// Sets ExclusiveMinimum on schema.
///
diff --git a/src/MicroElements.Swashbuckle.FluentValidation/OpenApiRuleContext.cs b/src/MicroElements.Swashbuckle.FluentValidation/OpenApiRuleContext.cs
index 4bbf9e2..83147c1 100644
--- a/src/MicroElements.Swashbuckle.FluentValidation/OpenApiRuleContext.cs
+++ b/src/MicroElements.Swashbuckle.FluentValidation/OpenApiRuleContext.cs
@@ -9,6 +9,7 @@
#if !OPENAPI_V2
using Microsoft.OpenApi.Models;
#endif
+using Swashbuckle.AspNetCore.SwaggerGen;
namespace MicroElements.Swashbuckle.FluentValidation
{
@@ -45,7 +46,11 @@ public OpenApiSchema Property
throw new ApplicationException($"Schema for type '{schemaType}' does not contain property '{PropertyKey}'.\nRegister {typeof(INameResolver)} if name in type differs from name in json.");
}
+#if OPENAPI_V2
+ var schemaProperty = OpenApiSchemaCompatibility.ResolveRefProperty(Schema, PropertyKey, _schemaRepository);
+#else
var schemaProperty = OpenApiSchemaCompatibility.GetProperty(Schema, PropertyKey);
+#endif
// Property is a schema reference (enum, nested class) - return empty schema to skip validation
// Issue #176: https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/issues/176
@@ -64,6 +69,8 @@ public OpenApiSchema Property
///
private ValidationRuleContext ValidationRuleInfo { get; }
+ private readonly SchemaRepository? _schemaRepository;
+
///
/// Initializes a new instance of the class.
///
@@ -71,16 +78,19 @@ public OpenApiSchema Property
/// Property name.
/// ValidationRuleInfo.
/// Property validator.
+ /// Optional schema repository for resolving references.
public OpenApiRuleContext(
OpenApiSchema schema,
string propertyKey,
ValidationRuleContext validationRuleInfo,
- IPropertyValidator propertyValidator)
+ IPropertyValidator propertyValidator,
+ SchemaRepository? schemaRepository = null)
{
Schema = schema;
PropertyKey = propertyKey;
ValidationRuleInfo = validationRuleInfo;
PropertyValidator = propertyValidator;
+ _schemaRepository = schemaRepository;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/MicroElements.Swashbuckle.FluentValidation/SchemaGenerationContext.cs b/src/MicroElements.Swashbuckle.FluentValidation/SchemaGenerationContext.cs
index b69b1ff..098232b 100644
--- a/src/MicroElements.Swashbuckle.FluentValidation/SchemaGenerationContext.cs
+++ b/src/MicroElements.Swashbuckle.FluentValidation/SchemaGenerationContext.cs
@@ -67,7 +67,7 @@ public IRuleContext Create(
ValidationRuleContext validationRuleContext,
IPropertyValidator propertyValidator)
{
- return new OpenApiRuleContext(Schema, schemaPropertyName, validationRuleContext, propertyValidator);
+ return new OpenApiRuleContext(Schema, schemaPropertyName, validationRuleContext, propertyValidator, SchemaRepository);
}
///
diff --git a/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationDocumentFilter.cs b/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationDocumentFilter.cs
index 45f7605..879c343 100644
--- a/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationDocumentFilter.cs
+++ b/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationDocumentFilter.cs
@@ -217,8 +217,8 @@ IEnumerable GetParameters()
var schema = item.Schema;
if (schema != null && parameterSchema != null && schemaPropertyName != null)
{
- if (OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName.ToLowerCamelCase(), out var property)
- || OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName, out property))
+ if (OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName.ToLowerCamelCase(), out var property, context.SchemaRepository)
+ || OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName, out property, context.SchemaRepository))
{
if (property != null)
{
diff --git a/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs b/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs
index 8992a6a..04cf160 100644
--- a/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs
+++ b/src/MicroElements.Swashbuckle.FluentValidation/Swashbuckle/FluentValidationOperationFilter.cs
@@ -188,8 +188,8 @@ private void ApplyRulesToParameters(OpenApiOperation operation, OperationFilterC
var parameterSchema = operationParameter.Schema;
if (parameterSchema != null)
{
- if (OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName.ToLowerCamelCase(), out var property)
- || OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName, out property))
+ if (OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName.ToLowerCamelCase(), out var property, context.SchemaRepository)
+ || OpenApiSchemaCompatibility.TryGetProperty(schema, schemaPropertyName, out property, context.SchemaRepository))
{
if (property != null)
{
diff --git a/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/AspNetCoreOpenApiTests.cs b/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/AspNetCoreOpenApiTests.cs
index 7d5dc34..edac7be 100644
--- a/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/AspNetCoreOpenApiTests.cs
+++ b/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/AspNetCoreOpenApiTests.cs
@@ -85,6 +85,41 @@ public async Task ComparisonRules_ShouldApplyCorrectly()
quantity.GetProperty("maximum").GetInt32().Should().Be(1000);
}
+ ///
+ /// Issue #146: BigInteger properties should have validation constraints applied.
+ ///
+ [Fact]
+ public async Task BigIntegerProperty_ShouldHaveValidationConstraints()
+ {
+ var schemas = await GetSchemasAsync();
+
+ var bigIntModel = schemas.GetProperty("TestBigIntegerModel");
+ var props = bigIntModel.GetProperty("properties");
+
+ // Name should have standard string constraints
+ var name = props.GetProperty("name");
+ name.GetProperty("minLength").GetInt32().Should().Be(1);
+ name.GetProperty("maxLength").GetInt32().Should().Be(100);
+
+ // Value (BigInteger): InclusiveBetween(0, 999) => minimum: 0, maximum: 999
+ // In ASP.NET Core OpenAPI, BigInteger is serialized as a $ref to the component schema.
+ // The transformer applies validation rules to the shared component schema object,
+ // so constraints appear on the BigInteger component rather than inline on the property.
+ var value = props.GetProperty("value");
+ if (value.TryGetProperty("$ref", out _))
+ {
+ // Follow the $ref — constraints are on the BigInteger component schema
+ var bigInteger = schemas.GetProperty("BigInteger");
+ bigInteger.GetProperty("minimum").GetInt32().Should().Be(0);
+ bigInteger.GetProperty("maximum").GetInt32().Should().Be(999);
+ }
+ else
+ {
+ value.GetProperty("minimum").GetInt32().Should().Be(0);
+ value.GetProperty("maximum").GetInt32().Should().Be(999);
+ }
+ }
+
[Fact]
public void TransformerCanResolveWithoutScope()
{
diff --git a/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/Program.cs b/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/Program.cs
index a56fda9..a74c526 100644
--- a/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/Program.cs
+++ b/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/Program.cs
@@ -17,5 +17,6 @@
app.MapOpenApi();
app.MapPost("/api/customers", (TestCustomer customer) => Results.Ok(customer));
app.MapPost("/api/orders", (TestOrder order) => Results.Ok(order));
+app.MapPost("/api/biginteger", (TestBigIntegerModel model) => Results.Ok(model));
app.Run();
diff --git a/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/TestModels.cs b/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/TestModels.cs
index 7e85790..29480bd 100644
--- a/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/TestModels.cs
+++ b/test/MicroElements.AspNetCore.OpenApi.FluentValidation.Tests/TestModels.cs
@@ -1,6 +1,7 @@
// Copyright (c) MicroElements. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System.Numerics;
using FluentValidation;
// Marker class for WebApplicationFactory
@@ -52,3 +53,19 @@ public TestOrderValidator()
RuleFor(x => x.Items).NotEmpty();
}
}
+
+// BigInteger model for Issue #146
+public class TestBigIntegerModel
+{
+ public BigInteger Value { get; set; }
+ public string Name { get; set; } = string.Empty;
+}
+
+public class TestBigIntegerModelValidator : AbstractValidator
+{
+ public TestBigIntegerModelValidator()
+ {
+ RuleFor(x => x.Value).InclusiveBetween(new BigInteger(0), new BigInteger(999));
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
+ }
+}
diff --git a/test/MicroElements.Swashbuckle.FluentValidation.Tests/BigIntegerParameterIntegrationTests.cs b/test/MicroElements.Swashbuckle.FluentValidation.Tests/BigIntegerParameterIntegrationTests.cs
new file mode 100644
index 0000000..4caeb0c
--- /dev/null
+++ b/test/MicroElements.Swashbuckle.FluentValidation.Tests/BigIntegerParameterIntegrationTests.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 System.Linq;
+using System.Net.Http;
+using System.Numerics;
+using System.Text.Json;
+using System.Threading.Tasks;
+using FluentAssertions;
+using FluentValidation;
+using MicroElements.Swashbuckle.FluentValidation.AspNetCore;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace MicroElements.Swashbuckle.FluentValidation.Tests
+{
+ ///
+ /// Integration tests verifying that BigInteger validation rules are applied
+ /// to operation parameters (via OperationFilter/DocumentFilter) on all TFMs.
+ /// Issue #146: BigInteger rendered as $ref on net10.0 should still get validation constraints.
+ ///
+ public class BigIntegerParameterIntegrationTests
+ {
+ public record BigIntegerRequest(BigInteger Limit, string Name);
+
+ public class BigIntegerRequestValidator : AbstractValidator
+ {
+ public BigIntegerRequestValidator()
+ {
+ RuleFor(x => x.Limit).InclusiveBetween(new BigInteger(1), new BigInteger(1000));
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(50);
+ }
+ }
+
+ private async Task<(JsonElement Doc, WebApplication App)> CreateAppAndGetSwaggerJson()
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.Services.AddEndpointsApiExplorer();
+ builder.Services.AddSwaggerGen();
+ builder.Services.AddFluentValidationRulesToSwagger();
+ builder.Services.AddScoped, BigIntegerRequestValidator>();
+
+ var app = builder.Build();
+ app.Urls.Add("http://127.0.0.1:0");
+ app.UseSwagger();
+ app.MapGet("/api/data", ([AsParameters] BigIntegerRequest request) => Results.Ok());
+
+ await app.StartAsync();
+
+ // Fetch Swagger JSON via HTTP to avoid OpenApi serialization API differences across TFMs
+ var address = app.Urls.First();
+ using var client = new HttpClient { BaseAddress = new System.Uri(address) };
+ var json = await client.GetStringAsync("/swagger/v1/swagger.json");
+ var doc = JsonDocument.Parse(json);
+
+ return (doc.RootElement, app);
+ }
+
+ ///
+ /// Verifies that BigInteger [AsParameters] properties have validation constraints
+ /// applied in the Swagger output. This tests the full pipeline including
+ /// OperationFilter's TryGetProperty → copy step.
+ ///
+ [Fact]
+ public async Task BigInteger_AsParameters_Should_Have_Validation_Constraints()
+ {
+ var (doc, app) = await CreateAppAndGetSwaggerJson();
+
+ try
+ {
+ var parameters = doc.GetProperty("paths")
+ .GetProperty("/api/data")
+ .GetProperty("get")
+ .GetProperty("parameters");
+
+ // Find Limit parameter
+ JsonElement? limitParam = null;
+ JsonElement? nameParam = null;
+ foreach (var param in parameters.EnumerateArray())
+ {
+ var name = param.GetProperty("name").GetString();
+ if (name == "Limit" || name == "limit") limitParam = param;
+ if (name == "Name" || name == "name") nameParam = param;
+ }
+
+ limitParam.Should().NotBeNull("Limit parameter should exist");
+ nameParam.Should().NotBeNull("Name parameter should exist");
+
+ // Name should have standard string constraints (works on all TFMs)
+ var nameSchema = nameParam!.Value.GetProperty("schema");
+ nameSchema.GetProperty("minLength").GetInt32().Should().Be(1, "NotEmpty sets minLength=1");
+ nameSchema.GetProperty("maxLength").GetInt32().Should().Be(50, "MaximumLength(50) sets maxLength=50");
+
+ // Limit (BigInteger) should have min/max constraints
+ // This is the key assertion: on net10.0, BigInteger is $ref → TryGetProperty must resolve it
+ var limitSchema = limitParam!.Value.GetProperty("schema");
+ limitSchema.TryGetProperty("minimum", out var minimum).Should().BeTrue(
+ "InclusiveBetween(1, 1000) should set minimum on BigInteger parameter");
+ minimum.GetInt32().Should().Be(1);
+
+ limitSchema.TryGetProperty("maximum", out var maximum).Should().BeTrue(
+ "InclusiveBetween(1, 1000) should set maximum on BigInteger parameter");
+ maximum.GetInt32().Should().Be(1000);
+ }
+ finally
+ {
+ await app.StopAsync();
+ }
+ }
+ }
+}
diff --git a/test/MicroElements.Swashbuckle.FluentValidation.Tests/SchemaGenerationTests.cs b/test/MicroElements.Swashbuckle.FluentValidation.Tests/SchemaGenerationTests.cs
index da37faf..d722c0a 100644
--- a/test/MicroElements.Swashbuckle.FluentValidation.Tests/SchemaGenerationTests.cs
+++ b/test/MicroElements.Swashbuckle.FluentValidation.Tests/SchemaGenerationTests.cs
@@ -12,6 +12,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
+using System.Numerics;
using System.Text.Json.Serialization;
using Xunit;
@@ -564,6 +565,74 @@ public void ILengthValidator_ProperlyAppliesMinMax_ToArrays(int min, int max)
schema.GetProperty(nameof(MinMaxLength.Qualities))!.MaxItems.Should().BeNull();
}
+ ///
+ /// https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/issues/146
+ /// BigInteger within decimal range should emit min/max constraints.
+ ///
+ public class BigIntegerModel
+ {
+ public BigInteger Value { get; set; }
+ }
+
+ public class BigIntegerModelValidator : AbstractValidator
+ {
+ public BigIntegerModelValidator()
+ {
+ RuleFor(x => x.Value).InclusiveBetween(new BigInteger(0), new BigInteger(12345678900));
+ }
+ }
+
+ [Fact]
+ public void BigInteger_InclusiveBetween_Should_Set_MinMax()
+ {
+ var schemaRepository = new SchemaRepository();
+ var referenceSchema = SchemaGenerator(new BigIntegerModelValidator()).GenerateSchema(typeof(BigIntegerModel), schemaRepository);
+ var schema = schemaRepository.GetSchema(referenceSchema.GetRefId()!);
+
+ var valueProp = schema.GetProperty("Value", schemaRepository)!;
+ valueProp.GetMinimum().Should().Be(0);
+ valueProp.GetMaximum().Should().Be(12345678900m);
+ }
+
+ ///
+ /// https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/issues/146
+ /// BigInteger exceeding decimal range should silently skip (no crash).
+ ///
+ [Fact]
+ public void BigInteger_ExceedingDecimalRange_Should_Not_Crash()
+ {
+ var overflowValue = BigInteger.Parse("999999999999999999999999999999999");
+ var validator = new InlineValidator();
+ validator.RuleFor(x => x.Value).InclusiveBetween(overflowValue, overflowValue);
+
+ var schemaRepository = new SchemaRepository();
+ var referenceSchema = SchemaGenerator(validator).GenerateSchema(typeof(BigIntegerModel), schemaRepository);
+ var schema = schemaRepository.GetSchema(referenceSchema.GetRefId()!);
+
+ // Should not crash; min/max should not be set (overflow on all TFMs)
+ schema.GetProperty("Value", schemaRepository)!.GetMinimum().Should().BeNull();
+ schema.GetProperty("Value", schemaRepository)!.GetMaximum().Should().BeNull();
+ }
+
+ ///
+ /// https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/issues/146
+ /// BigInteger GreaterThan should emit minimum constraint.
+ ///
+ [Fact]
+ public void BigInteger_GreaterThan_Should_Set_Minimum()
+ {
+ var validator = new InlineValidator();
+ validator.RuleFor(x => x.Value).GreaterThan(new BigInteger(10));
+
+ var schemaRepository = new SchemaRepository();
+ var referenceSchema = SchemaGenerator(validator).GenerateSchema(typeof(BigIntegerModel), schemaRepository);
+ var schema = schemaRepository.GetSchema(referenceSchema.GetRefId()!);
+
+ var valueProp = schema.GetProperty("Value", schemaRepository)!;
+ valueProp.GetMinimum().Should().Be(10);
+ valueProp.GetExclusiveMinimum().Should().Be(true);
+ }
+
[Fact]
// See the issue https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation/issues/156
public void DerivedSample_ShouldHave_ValidationRulesApplied()
diff --git a/test/MicroElements.Swashbuckle.FluentValidation.Tests/SchemaReferenceResolutionTests.cs b/test/MicroElements.Swashbuckle.FluentValidation.Tests/SchemaReferenceResolutionTests.cs
new file mode 100644
index 0000000..c5b3973
--- /dev/null
+++ b/test/MicroElements.Swashbuckle.FluentValidation.Tests/SchemaReferenceResolutionTests.cs
@@ -0,0 +1,159 @@
+// Copyright (c) MicroElements. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Numerics;
+using FluentAssertions;
+using FluentValidation;
+using MicroElements.OpenApi;
+#if OPENAPI_V2
+using Microsoft.OpenApi;
+#else
+using Microsoft.OpenApi.Models;
+#endif
+using Swashbuckle.AspNetCore.SwaggerGen;
+using Xunit;
+
+namespace MicroElements.Swashbuckle.FluentValidation.Tests
+{
+ ///
+ /// Tests for OpenApiSchemaReference resolution in GetProperty/TryGetProperty.
+ /// Issue #146: BigInteger (and other $ref types) should have validation rules applied on net10.0.
+ ///
+ public class SchemaReferenceResolutionTests : UnitTestBase
+ {
+ ///
+ /// Verifies that OpenApiSchemaCompatibility.GetProperty resolves OpenApiSchemaReference
+ /// through SchemaRepository when the property is a $ref.
+ ///
+ [Fact]
+ public void GetProperty_Should_Resolve_SchemaReference_Via_Repository()
+ {
+ // Arrange: generate schema for a type that contains BigInteger
+ var schemaRepository = new SchemaRepository();
+ var validator = new InlineValidator();
+ validator.RuleFor(x => x.Value).InclusiveBetween(new BigInteger(0), new BigInteger(100));
+
+ var schemaGenerator = SchemaGenerator(validator);
+ var referenceSchema = schemaGenerator.GenerateSchema(typeof(SchemaGenerationTests.BigIntegerModel), schemaRepository);
+ var schema = schemaRepository.GetSchema(referenceSchema.GetRefId()!);
+
+ // Act: GetProperty WITH repository should resolve even if property is OpenApiSchemaReference
+ var property = OpenApiSchemaCompatibility.GetProperty(schema, "Value", schemaRepository);
+
+ // Assert
+ property.Should().NotBeNull("GetProperty with repository should resolve $ref properties");
+ }
+
+ ///
+ /// Verifies that OpenApiSchemaCompatibility.TryGetProperty resolves OpenApiSchemaReference
+ /// through SchemaRepository when the property is a $ref.
+ ///
+ [Fact]
+ public void TryGetProperty_Should_Resolve_SchemaReference_Via_Repository()
+ {
+ // Arrange
+ var schemaRepository = new SchemaRepository();
+ var validator = new InlineValidator();
+ validator.RuleFor(x => x.Value).GreaterThan(new BigInteger(5));
+
+ var schemaGenerator = SchemaGenerator(validator);
+ var referenceSchema = schemaGenerator.GenerateSchema(typeof(SchemaGenerationTests.BigIntegerModel), schemaRepository);
+ var schema = schemaRepository.GetSchema(referenceSchema.GetRefId()!);
+
+ // Act
+ var found = OpenApiSchemaCompatibility.TryGetProperty(schema, "Value", out var property, schemaRepository);
+
+ // Assert
+ found.Should().BeTrue("TryGetProperty with repository should resolve $ref properties");
+ property.Should().NotBeNull();
+ }
+
+ ///
+ /// Verifies that GetProperty works after the schema filter has resolved $ref properties.
+ /// On OPENAPI_V2, the schema filter replaces $ref entries with concrete schemas during processing,
+ /// so GetProperty works even without a repository after the filter has run.
+ ///
+ [Fact]
+ public void GetProperty_Without_Repository_After_Filter_Processing()
+ {
+ // Arrange
+ var schemaRepository = new SchemaRepository();
+ var validator = new InlineValidator();
+ validator.RuleFor(x => x.Value).InclusiveBetween(new BigInteger(0), new BigInteger(100));
+
+ var schemaGenerator = SchemaGenerator(validator);
+ var referenceSchema = schemaGenerator.GenerateSchema(typeof(SchemaGenerationTests.BigIntegerModel), schemaRepository);
+ var schema = schemaRepository.GetSchema(referenceSchema.GetRefId()!);
+
+ // Act: GetProperty WITHOUT repository — after the schema filter has already processed the schema,
+ // the $ref is replaced with a concrete OpenApiSchema, so it works without repository.
+ var property = OpenApiSchemaCompatibility.GetProperty(schema, "Value");
+
+ // After filter processing, the property should be available regardless of OPENAPI version
+ property.Should().NotBeNull("property should be available after schema filter processing");
+ }
+
+ ///
+ /// Two models sharing the same BigInteger $ref type but with different validator ranges
+ /// should get independent min/max constraints (no shared schema mutation).
+ ///
+ public class ModelA
+ {
+ public BigInteger Amount { get; set; }
+ }
+
+ public class ModelB
+ {
+ public BigInteger Amount { get; set; }
+ }
+
+ public class ModelAValidator : AbstractValidator
+ {
+ public ModelAValidator()
+ {
+ RuleFor(x => x.Amount).InclusiveBetween(new BigInteger(10), new BigInteger(100));
+ }
+ }
+
+ public class ModelBValidator : AbstractValidator
+ {
+ public ModelBValidator()
+ {
+ RuleFor(x => x.Amount).InclusiveBetween(new BigInteger(500), new BigInteger(1000));
+ }
+ }
+
+ [Fact]
+ public void SharedRef_Should_Not_Corrupt_Between_Models()
+ {
+ // Arrange: both models share BigInteger which may be a $ref in the SchemaRepository
+ var schemaRepository = new SchemaRepository();
+ var schemaGenerator = SchemaGenerator(new ModelAValidator(), new ModelBValidator());
+
+ // Act: generate schemas for both models into the same repository
+ var refA = schemaGenerator.GenerateSchema(typeof(ModelA), schemaRepository);
+ var schemaA = schemaRepository.GetSchema(refA.GetRefId()!);
+
+ var refB = schemaGenerator.GenerateSchema(typeof(ModelB), schemaRepository);
+ var schemaB = schemaRepository.GetSchema(refB.GetRefId()!);
+
+ // Assert: each model should have its own min/max constraints
+ // Use OpenApiSchemaCompatibility.GetProperty which handles $ref resolution on OPENAPI_V2
+ var propertyA = OpenApiSchemaCompatibility.GetProperty(schemaA, "Amount", schemaRepository);
+ var propertyB = OpenApiSchemaCompatibility.GetProperty(schemaB, "Amount", schemaRepository);
+
+ propertyA.Should().NotBeNull("ModelA should have Amount property");
+ propertyB.Should().NotBeNull("ModelB should have Amount property");
+
+ var minA = OpenApiSchemaCompatibility.GetMinimum(propertyA!);
+ var maxA = OpenApiSchemaCompatibility.GetMaximum(propertyA!);
+ var minB = OpenApiSchemaCompatibility.GetMinimum(propertyB!);
+ var maxB = OpenApiSchemaCompatibility.GetMaximum(propertyB!);
+
+ minA.Should().Be(10, "ModelA minimum should be 10");
+ maxA.Should().Be(100, "ModelA maximum should be 100");
+ minB.Should().Be(500, "ModelB minimum should be 500");
+ maxB.Should().Be(1000, "ModelB maximum should be 1000");
+ }
+ }
+}
diff --git a/test/MicroElements.Swashbuckle.FluentValidation.Tests/TestCompatibility.cs b/test/MicroElements.Swashbuckle.FluentValidation.Tests/TestCompatibility.cs
index 1a9463f..42f079e 100644
--- a/test/MicroElements.Swashbuckle.FluentValidation.Tests/TestCompatibility.cs
+++ b/test/MicroElements.Swashbuckle.FluentValidation.Tests/TestCompatibility.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using MicroElements.OpenApi;
#if OPENAPI_V2
using Microsoft.OpenApi;
#else
@@ -115,19 +116,12 @@ public static bool IsNullable(this OpenApiSchema schema)
}
///
- /// Gets property from schema.
+ /// Gets property from schema. Delegates to production
+ /// to avoid duplicating $ref resolution logic.
///
- public static OpenApiSchema? GetProperty(this OpenApiSchema schema, string key)
+ public static OpenApiSchema? GetProperty(this OpenApiSchema schema, string key, SchemaRepository? repository = null)
{
-#if OPENAPI_V2
- if (schema.Properties?.TryGetValue(key, out var prop) == true)
- return prop as OpenApiSchema;
- return null;
-#else
- if (schema.Properties?.TryGetValue(key, out var prop) == true)
- return prop;
- return null;
-#endif
+ return OpenApiSchemaCompatibility.GetProperty(schema, key, repository);
}
///
diff --git a/version.props b/version.props
index daa6c8b..f9cd927 100644
--- a/version.props
+++ b/version.props
@@ -1,6 +1,6 @@
- 7.1.1
+ 7.1.2