Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,35 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

C# port of Google's [libphonenumber](https://github.com/google/libphonenumber). Code was rewritten from the Java source mostly unchanged — when in doubt about behavior, the Java upstream is the source of truth.

The library tracks upstream metadata releases (~every two weeks) via the `create_new_release_on_new_metadata_update.yml` GitHub Action; see commits like "feat: automatic upgrade to vX.Y.Z" for what those changes look like.
The library tracks upstream metadata releases (~every two weeks) via the `create_new_release_on_new_metadata_update.yml` GitHub Action; see commits like "feat: automatic upgrade to vX.Y.Z" for what those changes look like. The action stops when the upstream diff touches `.java` or `.proto` files, since those may need porting by hand; README.md ("Metadata updates") documents the dry-run and check-override options.

## Repository layout

- `csharp/Directory.Build.props` — build settings shared by every project: `LangVersion`, `TreatWarningsAsErrors` (so warnings break the build), the repo-wide `NoWarn` baseline, repository metadata, and the reproducible-build/Source Link switches. Set things here rather than per-csproj.
- `csharp/Directory.Build.props` — build settings shared by every project: `LangVersion`, `TreatWarningsAsErrors` (so warnings break the build), the repo-wide `NoWarn` baseline, repository metadata, `RestorePackagesWithLockFile`, the NuGet audit settings, symbol packaging (`.snupkg`), and the reproducible-build/Source Link switches. Set things here rather than per-csproj.
- `csharp/Directory.Packages.props` — Central Package Management. Every package version lives here; a `PackageReference` carrying its own `Version` is an error (`NU1008`).
- `csharp/PhoneNumbers/` — main library (NuGet `libphonenumber-csharp`). Multi-targets `netstandard2.0;net8.0;net10.0`.
- `csharp/PhoneNumbers.Test/` — xUnit tests, ported from the Java tests. Multi-targets `net8.0;net10.0`.
- `csharp/PhoneNumbers.Extensions/` — separate NuGet (`libphonenumber-csharp.extensions`) with C#-idiomatic helpers that don't exist in the Java library.
- `csharp/PhoneNumbers.Extensions.Test/` — xUnit tests for the Extensions package.
- `csharp/PhoneNumbers.PerformanceTest/` — BenchmarkDotNet harness.
- `csharp/PhoneNumbers.MetadataBuilder/` — build-time tool that converts XML metadata + geocoding/timezone text files into per-region binary files. Source-links a small set of files from `PhoneNumbers/` so it doesn't depend on (and can't cycle with) the main library at build time.
- `csharp/PhoneNumbers.Demo/` — Blazor WebAssembly demo, deployed to GitHub Pages by `deploy-demo.yml`. Doubles as proof the library works trimmed under WASM.
- `csharp/PhoneNumbers.Demo.Tests/` — bUnit tests for the demo.
- `csharp/coverlet.runsettings` — keeps the generated data tables out of coverage instrumentation; passed by the coverage workflow.
- `resources/` — XML metadata (`PhoneNumberMetadata.xml`, `ShortNumberMetadata.xml`, `PhoneNumberAlternateFormats.xml`, `PhoneNumberMetadataForTesting.xml`), plus `geocoding/`, `carrier/`, `timezones/`. **These are copied verbatim from upstream** — do not hand-edit. The library no longer reads them at runtime: the build pipeline emits binary equivalents under `obj/metadata/`, `obj/geocoding/`, `obj/timezones/` which are embedded into the published assembly.
- `lib/github-actions-metadata-update.sh` + `lib/DumpLocale.java` — automation that pulls upstream resources and regenerates `csharp/PhoneNumbers/LocaleData.cs`.

## Common commands

All `dotnet` commands run from the `csharp/` directory unless noted.
All commands below run from the repository root, which is what the `csharp/…` paths in them assume.

Metadata is built from XML/text into per-region binary files at build time by
`csharp/PhoneNumbers.MetadataBuilder/` (see the `BuildBinaryMetadata`,
`BuildGeocodingBins`, and `BuildTimezoneBin` MSBuild targets in `PhoneNumbers.csproj`).
You don't need to run anything by hand — `dotnet build` invokes the tool. The previous
`geocoding.zip` / `testgeocoding.zip` workflow is gone; the runtime reads binary files
directly via `IMetadataLoader` / `BuildPrefixMapFromBin`.
You don't need to run anything by hand — `dotnet build` invokes the tool. At run time those
binaries are read straight out of the assembly's embedded resources (gzip-compressed) via
`IMetadataLoader` / `BuildPrefixMapFromBin` — no XML or text resource is parsed, and no zip
archive or file on disk is involved.

Build / test:

Expand Down Expand Up @@ -74,10 +79,12 @@ dotnet run -c Release --framework net10.0 -- --filter "*PhoneNumberWorkflowBench
- `AsYouTypeFormatter` — incremental formatting.
- `PhoneNumberMatcher` / `PhoneNumberMatch` — find numbers in free text.
- `ShortNumberInfo` — short codes / SMS shortcodes (separate metadata file).
- `PhoneNumberOfflineGeocoder`, `PhoneNumberToTimeZonesMapper` — geo/tz lookups; backed by `geocoding.zip` / `timezones/map_data.txt`.
- `PhoneNumberOfflineGeocoder`, `PhoneNumberToCarrierMapper`, `PhoneNumberToTimeZonesMapper` — geo / carrier / tz lookups, backed by the binary prefix maps embedded at build time.
- `AreaCodeMap` + `AreaCodeMapStorageStrategy` / `DefaultMapStorage` / `FlyweightMapStorage` — prefix → string lookup used by geocoder/carrier/timezone mappers.
- **Regex caching.** Use `RegexCache` / `PhoneRegex` rather than constructing `Regex` ad hoc on hot paths — phone parsing is regex-heavy and the cache matters for throughput.
- **Nullable reference types** are enabled on every target except `netstandard2.0` (see csproj `Condition`). New code should still annotate.
- **Trim/AOT clean.** `IsAotCompatible` is set on the modern TFMs, so the trim, single-file and AOT analyzers run during the build and their warnings are errors. Keep reflection and dynamic code off any path reachable from the public API — the Blazor WASM demo depends on this too.
- **Hot-path allocation.** Parsing and formatting are deliberately allocation-light: match against spans and slices instead of materialising substrings or `Match` objects, and build lookup tables once into frozen collections. Measure a hot-path change with `PhoneNumbers.PerformanceTest` rather than reasoning about it — `run_performance_tests.yml` benchmarks the base commit on the same runner and posts a comparison to the PR.

## Working with this port vs. upstream Java

Expand All @@ -88,5 +95,7 @@ dotnet run -c Release --framework net10.0 -- --filter "*PhoneNumberWorkflowBench
## CI and release

- CI is GitHub Actions only, on `ubuntu-24.04-arm`. There are no Windows runners.
- PRs trigger `build_and_run_unit_tests_linux.yml` (net10.0 only) and `run_all_tests_and_upload_code_coverage.yml` (whole solution, every TFM, uploads to Codecov).
- Releases are tag-driven: a `vX.Y.Z` tag fires `publish_nuget.yml`, which packs both projects at the tag's version and pushes to nuget.org via trusted publishing (GitHub OIDC, `NuGet/login`) — there is no API key secret. Metadata-bump tags are created by `create_new_release_on_new_metadata_update.yml`.
- PRs trigger `build_and_run_unit_tests_linux.yml` (net10.0 only), `run_all_tests_and_upload_code_coverage.yml` (whole solution, every TFM, uploads to Codecov), and `codeql.yml`. Two more are path-filtered: `run_performance_tests.yml` (library or benchmark changes, with `post_performance_test_comment.yml` posting the result) and `build_and_run_demo_tests.yml` (demo changes). `scorecard.yml` runs on `main` and on branch-protection changes.
- **Restore is locked.** Every project has a committed `packages.lock.json` and CI fails if a restore would change one. After touching a `PackageReference` or `Directory.Packages.props`, run `dotnet restore csharp` and commit the updated lock files.
- `global.json` pins the SDK to 10.0.100 with `latestFeature` roll-forward, and CI verifies the build is reproducible. `EnablePackageValidation` is on for both packable projects, so a change that breaks the public surface — or that makes it inconsistent across TFMs — fails the build rather than shipping.
- Releases are tag-driven: a `vX.Y.Z` tag fires `publish_nuget.yml`, which packs both projects at the tag's version and pushes them, each with its `.snupkg`, to nuget.org via trusted publishing (GitHub OIDC, `NuGet/login`) — there is no API key secret. Metadata-bump tags are created by `create_new_release_on_new_metadata_update.yml`.
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,21 @@ We use and recommend the following workflow:
- The next official build will automatically include your change.

Essentially, we are following trunk based development

### Building and testing

```bash
dotnet restore csharp
dotnet build csharp --no-restore
dotnet test csharp/PhoneNumbers.sln -p:TargetFrameworks=net10.0 # what the PR check runs
```

A few things that will fail the build or CI if missed:

* **Warnings are errors.** `TreatWarningsAsErrors` is on for every project, including the trim and AOT analyzers on the modern targets.
* **Package versions live in one place.** Add or change versions in `csharp/Directory.Packages.props`, never in a `PackageReference` — Central Package Management rejects an inline `Version`.
* **Lock files are committed.** After any dependency change run `dotnet restore csharp` and commit the updated `packages.lock.json` files; CI verifies they are current.
* **The public API is validated.** Package validation compares the packable projects' surface across target frameworks, so a member added on only one target fails the build.
* **Some files are generated.** `LocaleData.cs` and `CountryCodeToRegionCodeMap.cs` are produced by the metadata tooling, and everything under `resources/` is copied verbatim from upstream — metadata fixes belong in [google/libphonenumber](https://github.com/google/libphonenumber), since the next automated sync overwrites local edits.

Changes under `csharp/PhoneNumbers/` also trigger a benchmark run that posts a before/after comparison to the pull request. If you are changing a hot path, look at that comment rather than guessing.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ dotnet add package libphonenumber-csharp

Available on NuGet as package [`libphonenumber-csharp`](https://www.nuget.org/packages/libphonenumber-csharp).

Targets `netstandard2.0`, `net8.0` and `net10.0`.

[`libphonenumber-csharp.extensions`](https://www.nuget.org/packages/libphonenumber-csharp.extensions) is an optional companion package with helpers that suit C# better than the ported Java API — `PhoneNumber.TryParse` and `PhoneNumber.TryParseValid` return a `bool` instead of throwing, and `PhoneNumberConverter` is a `System.Text.Json` converter for `PhoneNumber`.

### Trimming and Native AOT

The library is annotated as trim- and AOT-compatible, and the trim/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, so no XML is parsed and no file is read from disk at run time. The [interactive demo](https://twcclegg.github.io/libphonenumber-csharp/) is a Blazor WebAssembly app that runs this library trimmed, in the browser.

### Debugging and symbols

Symbols are published to the NuGet.org symbol server as a `.snupkg` alongside each release, with [Source Link](https://learn.microsoft.com/dotnet/standard/library-guidance/sourcelink) wired up — enable symbol server support in your debugger to step into the library.

## Examples

### Parsing a phone number
Expand Down Expand Up @@ -89,6 +101,20 @@ var regionCode = phoneNumberUtil.GetRegionCodeForNumber(phoneNumber);
Console.WriteLine(regionCode); // US
```

### Get the location of a phone number
```csharp
using PhoneNumbers;

var phoneNumberUtil = PhoneNumberUtil.GetInstance();
var geocoder = PhoneNumberOfflineGeocoder.GetInstance();
var phoneNumber = phoneNumberUtil.Parse("+12128120000", null);
var description = geocoder.GetDescriptionForNumber(phoneNumber, Locale.English);

Console.WriteLine(description); // New York, NY
```

The lookup is entirely offline. Detail varies by region — some yield a city, others only a state or the country name — and non-geographic or invalid numbers return the country name or an empty string. Pass a user region to omit it from the description for local numbers, or use `GetDescriptionForValidNumber` to skip the internal validity check.

### Get the time zones for a phone number
```csharp
using PhoneNumbers;
Expand Down Expand Up @@ -129,6 +155,8 @@ Console.WriteLine(carrierName); // Aircel
* AsYouTypeFormatter - formats phone numbers on-the-fly when users enter each digit.
* FindNumbers - finds numbers in text input
* PhoneNumberToCarrierMapper - looks up the carrier name originally assigned to a mobile or pager number, with locale-aware output and a safe-display mode for regions with mobile number portability.
* PhoneNumberOfflineGeocoder - describes where a number is from, in a requested language, without a network call.
* PhoneNumberToTimeZonesMapper - maps a number to its IANA time zone identifiers.

See [PhoneNumberUtil.cs](csharp/PhoneNumbers/PhoneNumberUtil.cs) for the various methods and properties available.

Expand All @@ -154,9 +182,17 @@ For more information on metadata usage, please refer to the [main repository faq
## Running tests locally

```bash
# Every project, every target framework.
dotnet test csharp/PhoneNumbers.sln

# Faster, and what the pull request check runs.
dotnet test csharp/PhoneNumbers.sln -p:TargetFrameworks=net10.0
```

The binary metadata the library reads at run time is generated during the build, so a plain
`dotnet build` is all that is needed first — there is no separate generation step.
See [CONTRIBUTING.md](CONTRIBUTING.md) for the build settings that will fail CI if missed.

## Metadata updates

The [`create_new_release_on_new_metadata_update`](https://github.com/twcclegg/libphonenumber-csharp/actions/workflows/create_new_release_on_new_metadata_update.yml) workflow runs daily and drives [`lib/github-actions-metadata-update.sh`](lib/github-actions-metadata-update.sh). When the latest `google/libphonenumber` release is newer than the published NuGet package, it copies the upstream `resources/`, regenerates `csharp/PhoneNumbers/LocaleData.cs`, builds and tests, then commits, pushes and creates a matching GitHub release.
Expand Down
16 changes: 16 additions & 0 deletions csharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ Original Java code is Copyright (C) 2009-2022 Google Inc.
`PhoneNumbers.Test/`
Port of libphonenumber Java tests in xunit format.

`PhoneNumbers.Extensions/`
C#-idiomatic helpers with no Java counterpart, shipped as a separate package.

`PhoneNumbers.Extensions.Test/`
Tests for the above.

`PhoneNumbers.MetadataBuilder/`
Build-time tool that converts the XML metadata and the geocoding, carrier and timezone
text files into the per-region binary files the library embeds.

`PhoneNumbers.PerformanceTest/`
BenchmarkDotNet harness.

`PhoneNumbers.Demo/` and `PhoneNumbers.Demo.Tests/`
Blazor WebAssembly demo deployed to GitHub Pages, and its bUnit tests.


Known Issues
------------
Expand Down