Skip to content

Format persistence keys with the invariant culture [patch] - #63

Merged
matt-edmondson merged 3 commits into
mainfrom
fix/46-invariant-persistence-keys
Sep 29, 2026
Merged

matt-edmondson merged 3 commits into
mainfrom
fix/46-invariant-persistence-keys

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #46

What changed

FileSystemPersistenceProvider and TempPersistenceProvider built file names with key.ToString(), which uses the current culture. GetAllKeysAsync then parsed those names back with the invariant culture. Under de-DE, that caused two problems:

  • A double key 1.5 was stored as 1,5.json and listed back as 15.
  • A DateTime key came back with its day and month swapped.

Data written under one culture also could not be found under another.

  • New PersistenceProviderUtilities.FormatKey<TKey>. This is the one helper both providers now use to turn a key into its file-name text, so the two cannot drift apart:
    • double/float use the round-trip "R" format.
    • DateTime/DateTimeOffset use ISO 8601 "O".
    • Other IFormattable types format with the invariant culture.
    • Strings pass through unchanged.
  • TryConvertToKey now parses DateTime with DateTimeStyles.RoundtripKind, so a UTC key is not converted to local time on the way back. It also parses DateTimeOffset, which Convert.ChangeType cannot convert at all.

Compatibility note

Release note: files written before this change under a culture that differs from invariant keep their old names, and the new format will not find them. For example, a 1,5.json written under de-DE.

DateTime keys are also named differently now, in every culture. The old name was a culture-specific short date with seconds precision; the new one is the ISO 8601 round-trip form. String, integer and GUID keys are unaffected. Under invariant-like cultures, double keys are unaffected too, because .NET's default double.ToString() is already the shortest round-trippable form.

The triage suggested a fallback lookup or a release note. I went with the note, because a fallback would have to cover Exists, Retrieve, Remove and enumeration in both providers.

Tests

In PersistenceNamingTests:

  • FileSystem_Double_And_Date_Keys_Round_Trip_Across_Cultures and Temp_Double_And_Date_Keys_Round_Trip_Across_Cultures each:
    1. store double and DateTime keys under a comma-decimal, day-first culture
    2. assert that GetAllKeysAsync returns exactly those keys
    3. switch to a month-first, twelve-hour culture and assert that RetrieveAsync still finds each one
  • FormatKey_Ignores_The_Current_Culture pins the formatted text.

The test cultures are cloned from the invariant culture rather than built with new CultureInfo("de-DE"), so the tests also run on machines in globalization-invariant mode with no ICU data. This container is one of those.

Verification

  • With the provider and TryConvertToKey changes reverted (keeping only FormatKey so the tests compile), both round-trip tests fail: the expected key 1,5 is missing from the listed keys.
  • With the change, the full suite passes: 922 of 922 on net10.0.
  • Essentials and both providers build for every target, including netstandard2.1, with 0 warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CzC1o7LFYHYaoWmVy1NLcp


Generated by Claude Code

FileSystem and Temp built file names with key.ToString(), which uses the
current culture, while GetAllKeysAsync parsed them back with the invariant
culture. Under de-DE a double key 1.5 was stored as 1,5 and listed as 15,
and data stored under one culture could not be found under another. Both
providers now format keys through PersistenceProviderUtilities.FormatKey,
which uses round-trip formats, and TryConvertToKey parses DateTime and
DateTimeOffset keys with round-trip kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzC1o7LFYHYaoWmVy1NLcp
Comment thread Essentials.Tests/PersistenceNamingTests.cs Fixed
Comment thread Essentials.Tests/PersistenceNamingTests.cs Fixed
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants