Implement IConvertible on the numeric wrappers and Digit#126
Merged
Conversation
RangeAttribute's numeric constructors read the validated value via Convert.ToInt32/ToDouble, which require IConvertible; without it the InvalidCastException is swallowed and every value - in range or not - silently reports as out of range. An explicit IConvertible delegating to Value makes plain [Range(1, 100)] validate the underlying number, and Convert.ChangeType interop comes along for free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CoverageLines: 4818 / 6386 (75.4%) Branches: 2514 / 3288 (76.5%) Files changed in this PR
StrongTypes — lines 89.1% (1946/2183), branches 87.8% (1245/1418)Booleans — lines 100.0% (14/14), branches 84.6% (22/26)
Collections — lines 90.2% (276/306), branches 85.1% (114/134)
ComponentModel — lines 89.5% (17/19), branches 68.8% (11/16)
Digits — lines 82.7% (86/104), branches 93.8% (30/32)
Emails — lines 97.4% (74/76), branches 95.7% (44/46)
Enums — lines 100.0% (58/58), branches 95.5% (21/22)
Exceptions — lines 78.9% (15/19), branches 50.0% (7/14)
Intervals — lines 96.5% (382/396), branches 95.5% (231/242)
Maybe — lines 92.1% (151/164), branches 83.9% (94/112)
Nullables — lines 100.0% (12/12), branches 83.3% (20/24)
Numbers — lines 100.0% (84/84), branches 94.4% (17/18)
Result — lines 98.5% (333/338), branches 95.6% (417/436)
Strings — lines 84.7% (133/157), branches 84.4% (54/64)
generated — lines 71.3% (311/436), branches 70.3% (163/232)
StrongTypes.Analyzers — lines 92.4% (514/556), branches 83.8% (238/284)(root) — lines 92.4% (514/556), branches 83.8% (238/284)
StrongTypes.Api — lines 97.3% (391/402), branches 86.3% (88/102)(root) — lines 100.0% (11/11), branches n/a (0/0)
Controllers — lines 97.0% (226/233), branches 86.3% (88/102)
Data — lines 100.0% (102/102), branches n/a (0/0)
Entities — lines 85.2% (23/27), branches n/a (0/0)
Models — lines 100.0% (29/29), branches n/a (0/0)
StrongTypes.AspNetCore — lines 89.3% (151/169), branches 84.5% (71/84)(root) — lines 89.3% (151/169), branches 84.5% (71/84)
StrongTypes.AspNetCore.TestApi — lines 91.5% (43/47), branches 92.5% (37/40)(root) — lines 100.0% (6/6), branches n/a (0/0)
Controllers — lines 90.2% (37/41), branches 92.5% (37/40)
StrongTypes.Configuration — lines 97.5% (79/81), branches 83.3% (40/48)(root) — lines 97.5% (79/81), branches 83.3% (40/48)
StrongTypes.Configuration.Tests.NullableDisabled — lines 100.0% (3/3), branches n/a (0/0)(root) — lines 100.0% (3/3), branches n/a (0/0)
StrongTypes.EfCore — lines 95.4% (227/238), branches 81.2% (78/96)(root) — lines 95.4% (227/238), branches 81.2% (78/96)
StrongTypes.FsCheck — lines 81.9% (127/155), branches 100.0% (2/2)(root) — lines 81.9% (127/155), branches 100.0% (2/2)
StrongTypes.OpenApi.Core — lines 91.4% (447/489), branches 83.4% (297/356)(root) — lines 91.9% (262/285), branches 86.1% (167/194)
Inlining — lines 90.7% (185/204), branches 80.2% (130/162)
StrongTypes.OpenApi.Microsoft — lines 38.1% (450/1180), branches 41.2% (177/430)(root) — lines 94.6% (246/260), branches 74.6% (97/130)
Binding — lines 98.1% (103/105), branches 78.6% (55/70)
Collections — lines 100.0% (19/19), branches 100.0% (8/8)
Digits — lines 100.0% (11/11), branches 100.0% (2/2)
Emails — lines 100.0% (11/11), branches 100.0% (2/2)
Inlining — lines 100.0% (7/7), branches 50.0% (1/2)
Intervals — lines 100.0% (24/24), branches 100.0% (6/6)
Maybe — lines 100.0% (12/12), branches 100.0% (2/2)
Numbers — lines 100.0% (8/8), branches 100.0% (2/2)
Strings — lines 100.0% (9/9), branches 100.0% (2/2)
obj/Debug/net10.0/Microsoft.AspNetCore.OpenApi.SourceGenerators/Microsoft.AspNetCore.OpenApi.SourceGenerators.XmlCommentGenerator — lines 0.0% (0/714), branches 0.0% (0/204)
StrongTypes.OpenApi.Swashbuckle — lines 94.7% (266/281), branches 79.1% (174/220)(root) — lines 98.7% (77/78), branches 93.4% (71/76)
Binding — lines 88.3% (106/120), branches 64.3% (72/112)
Collections — lines 100.0% (6/6), branches 100.0% (4/4)
Digits — lines 100.0% (10/10), branches 100.0% (4/4)
Emails — lines 100.0% (10/10), branches 100.0% (4/4)
Inlining — lines 100.0% (2/2), branches n/a (0/0)
Intervals — lines 100.0% (26/26), branches 100.0% (8/8)
Maybe — lines 100.0% (13/13), branches 75.0% (3/4)
Numbers — lines 100.0% (8/8), branches 100.0% (4/4)
Strings — lines 100.0% (8/8), branches 100.0% (4/4)
StrongTypes.OpenApi.TestApi.Microsoft — lines 41.0% (163/398), branches 31.6% (65/206)(root) — lines 100.0% (14/14), branches 100.0% (2/2)
obj/Debug/net10.0/Microsoft.AspNetCore.OpenApi.SourceGenerators/Microsoft.AspNetCore.OpenApi.SourceGenerators.XmlCommentGenerator — lines 38.8% (149/384), branches 30.9% (63/204)
StrongTypes.OpenApi.TestApi.Shared — lines 0.0% (0/193), branches n/a (0/0)(root) — lines 0.0% (0/193), branches n/a (0/0)
StrongTypes.OpenApi.TestApi.Swashbuckle — lines 100.0% (11/11), branches 100.0% (2/2)(root) — lines 100.0% (11/11), branches 100.0% (2/2)
|
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #126 +/- ##
==========================================
- Coverage 85.36% 85.09% -0.27%
==========================================
Files 171 172 +1
Lines 4831 4852 +21
Branches 992 992
==========================================
+ Hits 4124 4129 +5
- Misses 478 494 +16
Partials 229 229
🚀 New features to boost your workflow:
|
The IConvertible change exists so that RangeAttribute works in consumer applications; assert that at the application level - a [ApiController] endpoint with [Required][Range(1, 100)] Positive<int>? returns 200 in range, the attribute's own 400 out of range, and a binding 400 for values the type itself rejects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Positive<decimal> under the double-ctor [Range(0.5, 2.5)] exercises the
Convert.ToDouble path end-to-end. The expected message is built with
CultureInfo.CurrentCulture because RangeAttribute renders its bounds
with the current culture ("0,5" on a cs-CZ host).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Prompted by the DataAnnotations question in #94 (comment from @drventure): an empirical sweep of
ValidationAttributes against strong-typed properties found one silent trap on the numeric side — plain[Range(1, 100)]on aPositive<int>?property reported every value as out of range, even50.RangeAttribute's numeric constructors read the value viaConvert.ToInt32/ToDouble, which requireIConvertible; the wrappers didn't implement it, andRangeAttributeswallows the resultingInvalidCastExceptionas "invalid".What
NumericWrapperGeneratornow emits an explicitIConvertibleimplementation on every generated wrapper (Positive<T>,NonNegative<T>,Negative<T>,NonPositive<T>), delegating toValue.GetTypeCode()reportsTypeCode.Object;ToString(provider)goes throughIFormattableso it works for anyINumber<T>.Digitgets the same explicit implementation by hand.INumber<T>that isn't itselfIConvertible, the delegating cast throwsInvalidCastException— exactly what happened before for everyT, so there is no new failure mode.With this, both
[Range]forms validate the underlying value correctly ([Range(typeof(Positive<int>), "1", "100")]already worked via the v2TypeConverter+IComparable):Tests
DataAnnotationsValidationTestsagainst avalidation-probeendpoint in the TestApi:[Required][Range(1, 100)] Positive<int>?through the real[ApiController]pipeline. In-range → 200; out-of-range → 400 carrying the attribute's own message; missing/null → 400 required; and a value the type itself rejects (0) → 400 at binding, before validation runs. This is the application-level behavior the change exists for.[Range(0.5, 2.5)] Positive<decimal>?, exercisingRangeAttribute's double ctor (Convert.ToDouble) end-to-end. Incidental find: the attribute renders its bounds with the current culture ("0,5" on a cs-CZ host), so the expected message is built withCultureInfo.CurrentCulture.NumericConvertibleTests— FsCheck properties for theConvert.To*/ChangeTypebridge plusValidatortheories pinning both[Range]bounds (int and double ctors) on wrapper properties.DigitTests—Convert.ToInt32and a[Range(0, 5)]theory.StrongTypes.Tests(1296), AspNetCore integration (59), Configuration, Analyzers, OpenApi.Core, and WPF suites all green. Verified end-to-end againstValidator.TryValidateObjectin a scratch harness before/after.Docs
Skill/references/numeric.mdgains theIConvertiblebullet and a "Validation attributes (DataAnnotations)" section ([Required]/[Range]composition on nullable wrapper properties, non-nullable[Required]caveat);parsing.mdnotes the same forDigit;testing.mddocuments the new integration suite.🤖 Generated with Claude Code