Library / SDK
twcclegg/libphonenumber-csharp avatar
twcclegg/libphonenumber-csharp

libphonenumber-csharp and the regex decision that shapes a .NET phone number library

Offical C# port of https://github.com/googlei18n/libphonenumber

925 stars158 forksC#Apache-2.0

At a glance

What is it?
libphonenumber-csharp is the official C# port of Google's libphonenumber, rewritten from the Java source with metadata compiled into the assembly. It is aimed at .NET teams that need parsing, formatting and validation of phone numbers, and it makes a deliberate trade-off against compiled regexes.
Who is it for?
Adopt libphonenumber-csharp if your .NET application needs to parse, format or validate phone numbers and you want the metadata kept current without tracking the upstream repository yourself. Skip it if your workload concentrates very high volume on a single region and you have measured that compiled metadata patterns pay off, since the library deliberately leaves them uncompiled, and if your application cannot accept a dependency that carries full phone number metadata.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A direct C# rewrite of the Java library, not a wrapper

The library is described as the official C# port of Google's libphonenumber, and the important word in the implementation note is rewritten. The code was ported from the Java source mostly unchanged, so the API shape you already know from the Java library carries over rather than being reshaped around C# conventions.

The Apache License 2.0 from the original was preserved, which is the correct thing to do in a port and also means the licensing terms for a .NET user match those of the Java project.

The practical effect of the mostly unchanged translation is that sample code from the upstream documentation applies here. The project documentation points you at the original documentation for sample code and at its own API reference, which is generated from the library's XML doc comments, for the port's own types and members. That split is worth noting: the API reference is the authority for this port, the upstream docs are a shortcut for familiar tasks.

For a team already using the Java or Python versions, the migration story is close to a rename. The namespaces and type names change, the language changes, and the behaviour is intended to match.

The topics attached to the repository include e164, java-source and phonenumbers, which tells you the lineage and the format the library cares most about.

Installing from NuGet and the target frameworks it covers

The library ships on NuGet under the package name libphonenumber-csharp, and adding it to a project is a single command.

bash
dotnet add package libphonenumber-csharp

The package targets `netstandard2.0`, `net8.0` and `net10.0`. That combination covers the older .NET Framework compatible surface through netstandard, the current long term release, and the newest target, so most .NET projects can take it without a shim.

There is a second package, `libphonenumber-csharp.extensions`, described as an optional companion with helpers that suit C# better than the ported Java API. Two of its behaviours differ from what a Java developer would expect. `PhoneNumber.TryParse` and `PhoneNumber.TryParseValid` return a `bool` instead of throwing, which removes the try and catch from the common path. And `PhoneNumberConverter` is a `System.Text.Json` converter for `PhoneNumber`, so a phone number can be serialised and deserialised through the JSON stack that modern .NET applications already use.

That companion package is where the ergonomics of a native C# library show up, while the main package keeps parity with the upstream API. Taking both is usually the right call for a new .NET codebase.

Metadata compiled into the assembly, and why that matters for trimming

This is the part of the design that differs most from how the metadata library is usually deployed, and it has a concrete consequence for modern .NET applications.

The library is annotated as trim- and AOT-compatible, and the trim and AOT analyzers run as part of its own build. All metadata, including the geocoding, carrier and time zone prefix maps, is compiled to a binary form at build time and embedded in the assembly as compressed resources. No XML is parsed and no file is read from disk at run time.

This matters because the alternative approach, shipping metadata as data files next to the assembly, breaks two things that .NET teams care about. Native AOT compilation cannot reason about files that are not part of the assembly, and trimming cannot know which data files are needed at run time. Embedding the data solves both, at the cost of a larger assembly.

The demonstration of that claim is the interactive demo, described as a Blazor WebAssembly app that runs this library trimmed in the browser. A library that reads metadata off disk simply cannot work in that environment, so the demo is the proof rather than the marketing.

If your application publishes trimmed or Native AOT, this packaging choice is the reason the library works at all.

Why the metadata regexes are not compiled

Validation and formatting are driven by regexes built from the bundled metadata, and there are thousands of them. Each is built the first time some caller touches that region, which means a cold start pays for whatever regions it touches.

The library makes a deliberate choice here, and the reasoning is documented in detail. Those metadata regexes are deliberately not built with `RegexOptions.Compiled`. Compiling one costs around 1.5 ms of IL emit and saves roughly 0.044 µs per match, so a pattern has to be matched on the order of 30,000 times before compiling it breaks even. Metadata patterns rarely reach that, because a workload spread across regions matches each one comparatively few times.

The measured end-to-end numbers on net8.0, total wall time including startup plus parse, validate and format, are striking. Compiling them is 34x slower for 1,000 operations across 245 regions, 3.7x slower for 100,000, and still 1.7x slower at 1,000,000. It wins only for a process concentrating very high volume on one or two regions, and then by 6 to 20 percent.

The library's own fixed regexes are treated differently. There is a small, fixed number of them, each built once per process and hot for its whole life, so those are still compiled.

There is no per-call knob for this, and the documentation asks that anyone who measures a workload where compiling wins open an issue with the numbers rather than reaching for internal types. That is the right way to handle an optimisation that flips depending on usage shape.

Metadata updates on a schedule, and the release train that follows

Phone number metadata changes as numbering plans change, and the upstream Google repository updates its metadata approximately every two weeks. The port does not require you to track that repository.

This library is automatically updated by a scheduled GitHub action to include the latest metadata, usually within a day, and the documentation explains how that works and how to run it manually. So the metadata freshness is not something your team has to manage, which removes the most common reason people get stuck maintaining their own phone number handling.

The release list shows the cadence on the packaging side. v9.0.41 was released on 2026-10-08, v9.0.40 on 2026-09-24 and v9.0.39 on 2026-09-10, three releases in a single month. The last push was on 2026-10-20 and the repository is not archived.

The version number is the thing to understand. A metadata update produces a new package version, so if you pin an exact version you are also pinning the metadata date, and you will need to move that pin forward periodically to pick up new numbering plans. Whether you take that as a chore or a feature depends on how quickly the numbering changes affect your own users.

Symbols, the API surface and what the port does not decide for you

Debugging into the library is supported rather than left to guesswork. Symbols are published to the NuGet.org symbol server as a `.snupkg` alongside each release, with Source Link wired up, so enabling symbol server support in your debugger lets you step into the library rather than only into its public surface.

The API surface itself is a faithful port. `PhoneNumberUtil.GetInstance()` returns the shared utility instance, `Parse` takes a string and a region code, and there are calls for `Format`, `IsValidNumber`, `GetNumberType` and `GetRegionCodeForNumber`. The format parameter takes `PhoneNumberFormat.INTERNATIONAL` or `PhoneNumberFormat.NATIONAL`, and the number type comes back as a `PhoneNumberType` value, with the list of possible types in the `PhoneNumberType.cs` file under the `csharp/PhoneNumbers` directory.

What the library does not decide for you is validation policy. `IsValidNumber` tells you whether a number is valid according to the metadata, and it is up to you to decide what to do with an invalid one. The same applies to country codes, and to the choice between throwing and the boolean returning helpers in the extensions package.

The repository also carries AGENTS.md and CLAUDE.md alongside the usual CONTRIBUTING.md, SECURITY.md and CODE_OF_CONDUCT.md, and the docs are built with docfx given the docfx directory at the root. The csharp/ directory holds the ported sources, and the resources/ directory is where the metadata lives before it is compiled into the assembly.

Editorial conclusion

Adopt libphonenumber-csharp if your .NET application needs to parse, format or validate phone numbers and you want the metadata kept current without tracking the upstream repository yourself. Skip it if your workload concentrates very high volume on a single region and you have measured that compiled metadata patterns pay off, since the library deliberately leaves them uncompiled, and if your application cannot accept a dependency that carries full phone number metadata. Before adopting, add the package with `dotnet add package libphonenumber-csharp`, confirm your target framework is one of netstandard2.0, net8.0 or net10.0, and check the metadata update cadence against the release you pin.

Frequently asked questions

How do I add libphonenumber-csharp to a .NET project?

Run dotnet add package libphonenumber-csharp. The package is on NuGet as libphonenumber-csharp and targets netstandard2.0, net8.0 and net10.0.

Is libphonenumber-csharp trim and AOT compatible?

Yes. The library is annotated as trim- and AOT-compatible, with the analyzers running in its own build, and all metadata including geocoding, carrier and time zone prefix maps is compiled into the assembly as compressed resources so nothing is parsed from disk at run time.

Why does libphonenumber-csharp not compile its metadata regexes?

Compiling one metadata pattern costs around 1.5 ms of IL emit and saves roughly 0.044 µs per match, so it needs on the order of 30,000 matches to break even. The library's own fixed regexes are still compiled, and there is no per-call knob to change the metadata behaviour.

How often does the phone number metadata get updated?

Metadata is updated in the upstream Google repository approximately every two weeks, and this library is updated automatically by a scheduled GitHub action, usually within a day, so a new package version normally carries the latest metadata.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. README
  4. Releases
  5. twcclegg/libphonenumber-csharp on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/twcclegg-libphonenumber-csharp.svg)](https://hysenlabs.com/projects/twcclegg-libphonenumber-csharp)

Was this useful?