Skip to content

feat(extensions): TryParse overloads, TypeConverter, and a real phone-number ValidationAttribute - #405

Merged
twcclegg merged 4 commits into
mainfrom
feat/extensions-binding-ergonomics
Aug 25, 2026
Merged

feat(extensions): TryParse overloads, TypeConverter, and a real phone-number ValidationAttribute#405
twcclegg merged 4 commits into
mainfrom
feat/extensions-binding-ergonomics

Conversation

@twcclegg

Copy link
Copy Markdown
Owner

Summary

  • PhoneNumber.TryParse/TryParseValid gain a 2-arg overload (region defaults to null), matching the conventional .NET TryParse(string, out T) shape.
  • PhoneNumberTypeConverter converts PhoneNumber to/from its E.164 string — for consumers that register TypeConverters (config binding, PropertyGrid, MVC model binding, etc.). Not applied automatically; register it explicitly, same pattern as the existing PhoneNumberConverter for System.Text.Json.
  • PhoneNumberAttribute is a DataAnnotations ValidationAttribute backed by PhoneNumberUtil.IsValidNumber, unlike the framework's built-in [Phone] which is a loose regex. Accepts a string (parsed via the optional Region property) or an already-parsed PhoneNumber.

Note on scope: an earlier version of this considered a TryParse(string, IFormatProvider, out PhoneNumber) shape aimed at ASP.NET Core minimal-API automatic parameter binding, but that only works if the method lives on PhoneNumbers.PhoneNumber itself (the framework reflects on the parameter's own type) — decided to keep this Extensions-only rather than touch the core faithful-port model class, so this PR is a plain convenience overload rather than automatic binding support.

netstandard2.0 needs System.ComponentModel.Annotations for ValidationAttribute; net8.0/net10.0 already have it in the shared framework — added centrally in Directory.Packages.props, referenced only on the netstandard2.0 leg like the existing System.Text.Json reference.

Test plan

  • dotnet build csharp --no-restore — 0 warnings/errors
  • dotnet test csharp/PhoneNumbers.slnx — 888 tests passed (net8.0 + net10.0), including new tests for all three additions
  • dotnet pack -c Release csharp/PhoneNumbers.Extensions -p:VersionPrefix=9.0.38 — packs and passes EnablePackageValidation against the 9.0.37 baseline (confirms the additions are purely additive, no API-compat breaks)

The bare catch in PhoneNumbers.Extensions.PhoneNumber swallowed every
exception, not just parse failures, silently masking bugs.
…-number ValidationAttribute

- PhoneNumber.TryParse/TryParseValid gain a 2-arg overload (region
  defaults to null) matching the conventional .NET TryParse shape.
- PhoneNumberTypeConverter converts PhoneNumber to/from its E.164
  string, for consumers that register TypeConverters (config binding,
  PropertyGrid, etc.) — not applied automatically.
- PhoneNumberAttribute is a DataAnnotations ValidationAttribute backed
  by PhoneNumberUtil.IsValidNumber, unlike the framework's built-in
  [Phone] which is a loose regex. Accepts a string (parsed via the
  optional Region property) or an already-parsed PhoneNumber.

netstandard2.0 needs System.ComponentModel.Annotations for
ValidationAttribute; net8.0/net10.0 already have it.
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.43%. Comparing base (decc3bd) to head (2ce156a).

Files with missing lines Patch % Lines
...honeNumbers.Extensions/PhoneNumberTypeConverter.cs 55.55% 0 Missing and 4 partials ⚠️

❌ Your patch check has failed because the patch coverage (83.33%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #405      +/-   ##
==========================================
- Coverage   87.47%   87.43%   -0.04%     
==========================================
  Files          39       41       +2     
  Lines        3839     3861      +22     
  Branches      986      991       +5     
==========================================
+ Hits         3358     3376      +18     
  Misses        283      283              
- Partials      198      202       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

📊 Benchmark Results

Commit: 2ce156a · Full run · Linux ubuntu-24.04-arm

Both sides were measured on the same runner in the same job, so the numbers are
comparable. Treat sub-percent differences as noise.

PR branch

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
InputDigitPerKeystroke 1000 4.837 ms 0.0706 ms 0.0661 ms 54.6875 3.88 MB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  Job-AMQORM : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Runtime=.NET 10.0  InvocationCount=1  IterationCount=20  
LaunchCount=1  RunStrategy=ColdStart  UnrollFactor=1  
WarmupCount=1  

Method Mean Error StdDev Allocated
CreateInstance 417.1 μs 121.8 μs 140.3 μs 119.48 KB
CreateInstanceAndLoadAllRegions 7,155.0 μs 428.4 μs 493.3 μs 1620.34 KB
FirstRegionLookup 470.6 μs 151.3 μs 174.2 μs 124.54 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ExtractPossibleNumber_CleanInput 1000 20.90 μs 0.019 μs 0.018 μs - -
ExtractPossibleNumber_WithLeadingJunk 1000 38.81 μs 0.047 μs 0.042 μs 0.6714 48360 B

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
FindNumbers_Valid 100 142.7 μs 0.29 μs 0.25 μs 0.9766 70.71 KB
FindNumbers_StrictGrouping 100 327.7 μs 0.45 μs 0.39 μs 1.4648 124.84 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
GetDescriptionForNumber 1000 1,431.60 μs 3.097 μs 2.746 μs 1.9531 196.59 KB
GetDisplayCountry 1000 18.55 μs 0.032 μs 0.028 μs 0.0916 7.56 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ParseValidateAndFormatPhoneNumbers 1000 2,655.5 μs 43.32 μs 46.35 μs 7.8125 577.71 KB
ParseOnly 1000 440.9 μs 0.79 μs 0.74 μs 4.8828 348.13 KB
ParseNationalFormat 1000 780.9 μs 2.06 μs 1.72 μs 5.8594 431.36 KB
ParseWithExtension 1000 1,124.2 μs 1.88 μs 1.57 μs 13.6719 957.38 KB
ValidateOnly 1000 784.1 μs 1.58 μs 1.32 μs - 41.11 KB
FormatOnly 1000 1,129.2 μs 2.04 μs 1.91 μs 1.9531 187.91 KB
PR base

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
InputDigitPerKeystroke 1000 4.857 ms 0.0444 ms 0.0415 ms 54.6875 3.88 MB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]     : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  Job-AMQORM : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Runtime=.NET 10.0  InvocationCount=1  IterationCount=20  
LaunchCount=1  RunStrategy=ColdStart  UnrollFactor=1  
WarmupCount=1  

Method Mean Error StdDev Allocated
CreateInstance 413.6 μs 129.5 μs 149.2 μs 119.48 KB
CreateInstanceAndLoadAllRegions 7,239.9 μs 493.7 μs 568.5 μs 1620.34 KB
FirstRegionLookup 481.4 μs 184.5 μs 212.4 μs 124.54 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ExtractPossibleNumber_CleanInput 1000 20.89 μs 0.017 μs 0.016 μs - -
ExtractPossibleNumber_WithLeadingJunk 1000 38.48 μs 0.088 μs 0.078 μs 0.6714 48360 B

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
FindNumbers_Valid 100 140.6 μs 0.54 μs 0.50 μs 0.9766 70.71 KB
FindNumbers_StrictGrouping 100 326.4 μs 0.90 μs 0.84 μs 1.4648 124.84 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
GetDescriptionForNumber 1000 1,498.67 μs 8.211 μs 7.680 μs 1.9531 196.59 KB
GetDisplayCountry 1000 18.58 μs 0.032 μs 0.030 μs 0.0916 7.56 KB

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
Neoverse-N2, 4 physical cores
.NET SDK 10.0.400
  [Host]    : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a
  .NET 10.0 : .NET 10.0.11 (10.0.11, 10.0.1126.37416), Arm64 RyuJIT armv8.0-a

Job=.NET 10.0  Runtime=.NET 10.0  

Method PhoneNumberCount Mean Error StdDev Gen0 Allocated
ParseValidateAndFormatPhoneNumbers 1000 2,717.1 μs 32.74 μs 29.03 μs 7.8125 577.71 KB
ParseOnly 1000 441.6 μs 1.11 μs 0.99 μs 4.8828 348.13 KB
ParseNationalFormat 1000 786.2 μs 2.35 μs 2.20 μs 5.8594 431.36 KB
ParseWithExtension 1000 1,126.6 μs 1.72 μs 1.53 μs 13.6719 957.38 KB
ValidateOnly 1000 781.3 μs 2.43 μs 2.16 μs - 41.11 KB
FormatOnly 1000 1,109.9 μs 2.66 μs 2.49 μs 1.9531 187.92 KB

…neNumberAttribute

- PhoneNumberTypeConverter.ConvertFrom now uses Util.Parse instead of
  ParseAndKeepRawInput. RawInput feeds Equals/GetHashCode on the
  generated PhoneNumber type, so a number converted through this
  TypeConverter previously compared unequal/hashed differently from
  the same number parsed via PhoneNumber.TryParse, even though nothing
  in ConvertTo ever reads RawInput back.
- PhoneNumberAttribute.IsValid now caches PhoneNumberUtil.GetInstance()
  in a static field, matching the pattern PhoneNumber.cs and
  PhoneNumberTypeConverter.cs already use.
@twcclegg

Copy link
Copy Markdown
Owner Author

Updated after code review (8-angle pass): fixed two findings on PhoneNumberTypeConverter/PhoneNumberAttribute (see latest commit). Also worth flagging: review surfaced a real, pre-existing bug in the core parser reachable through PhoneNumberAttribute on malformed RFC3966 input — fixed separately in #406, which should merge alongside this one and #401.

…ng-ergonomics

# Conflicts:
#	csharp/Directory.Packages.props
@twcclegg
twcclegg merged commit 9466f2b into main Aug 25, 2026
7 of 8 checks passed
@twcclegg
twcclegg deleted the feat/extensions-binding-ergonomics branch August 25, 2026 00:27
pull Bot pushed a commit to LoadsAForks/libphonenumber-csharp that referenced this pull request Aug 31, 2026
…sion methods

PhoneNumberJsonContext + PhoneNumberJsonOptions ship a source-generated
JsonSerializerContext for PhoneNumber plus ready-made JsonSerializerOptions,
so consumers building trimmed/Native AOT apps don't have to hand-write one.

The two pieces have to be combined carefully: PhoneNumber.DefaultInstanceForType
is a public get-only property returning the instance itself, so the raw
member-based JsonTypeInfo<PhoneNumber> the source generator produces recurses
into itself without end if ever invoked directly (JsonSerializer.Serialize(v,
Context.Default.PhoneNumber) throws once the writer hits its max depth, or
overflows the stack at a larger depth limit) instead of using
PhoneNumberConverter. PhoneNumberJsonOptions.Default/Create wire the context
and the converter onto the same JsonSerializerOptions so callers can't get
this wrong; PhoneNumberJsonOptions.Serialize/Deserialize additionally avoid
the IL2026/IL3050 trim/AOT warnings a consumer's own analyzer would otherwise
raise on the options-based JsonSerializer overloads. Verified with a
throwaway `dotnet publish -p:PublishAot=true` app (not part of this repo).

IsAotCompatible is now set for Extensions' modern TFMs too, matching
PhoneNumbers.csproj, so this stays enforced in CI.

PhoneNumberExtensions adds ToE164/ToNationalFormat/ToInternationalFormat/
IsValid as extension methods on PhoneNumber, for the common formatting
one-liners callers currently have to route through PhoneNumberUtil.GetInstance()
by hand.

Note: PhoneNumberAttribute (a ValidationAttribute backed by
PhoneNumberUtil.IsValidNumber) already exists on main via PR twcclegg#405 and needed
no changes here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WVGRJyQwtvBGXAmWswxyqW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant