From 097ce701ed0a3aebdcf4416084966ff1e966fd73 Mon Sep 17 00:00:00 2001 From: Thomas Clegg Date: Tue, 11 Aug 2026 12:02:28 -0500 Subject: [PATCH] docs: refresh the docs for the binary metadata, build and CI changes --- CLAUDE.md | 27 ++++++++++++++++++--------- CONTRIBUTING.md | 18 ++++++++++++++++++ README.md | 36 ++++++++++++++++++++++++++++++++++++ csharp/README.md | 16 ++++++++++++++++ 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ce6c0fc36..0548c2e2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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: @@ -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 @@ -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`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f919d5fb1..83f0805f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/README.md b/README.md index 651a5f7cc..4de4fc1b6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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; @@ -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. @@ -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. diff --git a/csharp/README.md b/csharp/README.md index ceee3165c..9c98b8b6b 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -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 ------------