Add As* parsing extensions and enum extensions#31
Merged
Conversation
Rename the NonEmptyString To* parsers to As* for consistency with the new As* overloads on string, add AsEnum<T> on both receivers, and introduce EnumExtensions exposing cached AllValues / AllFlagValues / AllFlagsCombined plus a per-value GetFlags decomposition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ters - Move to C# 14 extension blocks so the struct/Enum constraint is written once per receiver shape instead of on every member. - Add Parse / TryParse / Create / TryCreate as static extension members that delegate to Enum.Parse / Enum.TryParse (Create/TryCreate are the repo's factory-named aliases). - Route AsEnum<T> through Enum.TryParse — drops the previous ad-hoc comma / numeric / case rejections so the contract matches TryParse. - Swap the bit-math representation from ulong to long, using compiled expression trees for the TEnum <-> long round-trip (cached per T). - Make every cache its own Lazy so enums that never ask for flag data never scan for it, and vice versa. - AllFlagValues, AllFlagsCombined, and GetFlags now throw InvalidOperationException when the enum is missing [Flags]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches TryParse / TryCreate. The BCL overloads are annotated non-null, so we pass through with the null-forgiving operator and let Enum.Parse raise ArgumentNullException on null — callers no longer need to null-forgive at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lazy<T>.PublicationOnly had the same race shape as ??= (idempotent recompute under contention) while adding an allocation and an indirection per read, with no safety win. ??= is cheaper and clearer. Value-type caches (FlagData tuple, AllFlagsCombined) use a small reference-typed holder — a record class for FlagData, a boxed object for the TEnum — because Nullable<T> writes over multi-word structs are not atomic. Also revert Parse / Create to non-nullable string; callers that need to pass a nullable can apply ! themselves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Look up FlagsAttribute once per closed generic type into a static readonly bool, so the guard on AllFlagValues / AllFlagsCombined / GetFlags is a field load instead of a reflection call. - Fold the tuple holder and the boxed-TEnum holder into a single FlagMeta record class (Values + Bits + Combined), so one pointer assignment publishes every flag-related field atomically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The flag metadata now lives in three dedicated static fields plus a _flagMetaReady bool. The writer fills the value fields and then Volatile.Writes the flag to true; readers Volatile.Read the flag first and only touch the fields when it's set, so they can never observe a half-initialized state. Drops the per-type FlagMeta allocation. Under contention multiple threads may compute, but the compute is deterministic so identical bits land in each field regardless of interleaving. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Eagerly compile ToLong / FromLong into static readonly delegates so call sites pay a plain field read instead of a null-check + branch on every use (matters for GetFlags and OrAllFlagValues). - Split FlagValues and FlagsCombined back into independent ??= caches so touching one never forces the other to compute. - Cache HasFlagsAttribute once and branch off a bool, keeping the [Flags] guard O(1) on every subsequent access. - Drop the published-flag bool, the FlagMeta record, and the paired *Unchecked getters; the file is now a fraction of the size. Trade: Nullable<TEnum> can tear under concurrent first-access of a long-backed flag enum (16-byte struct). Acceptable for the narrow edge; int-and-smaller underlying types are atomic on 64-bit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FlagsCombined now uses a plain TEnum field paired with a bool + Volatile release-store. The value fits atomically in a standalone field (max 8 bytes on 64-bit), and a reader that sees the bool set is guaranteed to see the prior value write. Drops the tearing edge that Nullable<TEnum> had on long-backed enums. ScanForFlagValues is a single-line LINQ Where/ToArray — same result, easier to read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both FlagValues and FlagsCombined now go through LazyInitializer.EnsureInitialized, which does double-checked locking via Monitor: fast path is a bool + value read, slow path takes a lazily-allocated lock, re-checks, runs the factory once, publishes. Exactly-once compute — no more redundant factory calls under first-access contention — and no size/atomicity edge cases from the backing field, since the bool gates the read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Well-formed flag enums no longer pay the attribute check on every access to AllFlagValues/AllFlagsCombined/GetFlags — just the LazyInitializer fast path. Validation runs once inside ScanForFlagValues; factory throws propagate without caching so non-flag enums still throw on every access. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One receiver-named extension block holds both static members (Parse, AllValues, AllFlagValues, AllFlagsCombined, ...) and the instance GetFlags, since C# extension blocks permit both. Receiver renamed to `source` to avoid clashing with the `value` parameter on Parse etc. FlagValues now uses a plain `??= ScanForFlagValues()`: reference assignment is atomic and the scan is deterministic, so a race that runs the factory twice is harmless. FlagsCombined still needs the LazyInitializer + bool flag because TEnum isn't atomic on 32-bit and default(TEnum) == 0 can be a valid computed result. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Non-flag enums touching AllValues no longer trigger the Expression-tree compilation for ToLong/FromLong or the [Flags] reflection check — those move into FlagEnumMeta<TEnum>, whose cctor only fires when flag APIs are used. EnumMeta<TEnum> now holds just Enum.GetValues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
max_line_length = 160 matches the longest one-liners already in the codebase. CLAUDE.md gets a Style bullet telling contributors — human or AI — not to pre-emptively wrap lambdas, method chains, or throws that fit within the limit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So it shows up in Solution Explorer alongside license.txt and readme.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expression-bodied methods/properties/getters/local functions stay on a single line when they fit. If they don't, switch to a block body rather than wrapping the => expression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Shorter name fits the GitHub Actions UI better. Renames the workflow file, the top-level name, the job name, and the publish job's needs reference. Updates the readme badge to point at build.yml. Branch protection rules referencing the old "build-and-test" check name will need to be updated to "build". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KaliCZ
added a commit
that referenced
this pull request
Apr 19, 2026
* Add As* parsing extensions and enum extensions Rename the NonEmptyString To* parsers to As* for consistency with the new As* overloads on string, add AsEnum<T> on both receivers, and introduce EnumExtensions exposing cached AllValues / AllFlagValues / AllFlagsCombined plus a per-value GetFlags decomposition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rework EnumExtensions: extension blocks, lazy caches, compiled converters - Move to C# 14 extension blocks so the struct/Enum constraint is written once per receiver shape instead of on every member. - Add Parse / TryParse / Create / TryCreate as static extension members that delegate to Enum.Parse / Enum.TryParse (Create/TryCreate are the repo's factory-named aliases). - Route AsEnum<T> through Enum.TryParse — drops the previous ad-hoc comma / numeric / case rejections so the contract matches TryParse. - Swap the bit-math representation from ulong to long, using compiled expression trees for the TEnum <-> long round-trip (cached per T). - Make every cache its own Lazy so enums that never ask for flag data never scan for it, and vice versa. - AllFlagValues, AllFlagsCombined, and GetFlags now throw InvalidOperationException when the enum is missing [Flags]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Accept nullable string in Parse / Create Matches TryParse / TryCreate. The BCL overloads are annotated non-null, so we pass through with the null-forgiving operator and let Enum.Parse raise ArgumentNullException on null — callers no longer need to null-forgive at the call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop Lazy<T> in favor of ??= field caches Lazy<T>.PublicationOnly had the same race shape as ??= (idempotent recompute under contention) while adding an allocation and an indirection per read, with no safety win. ??= is cheaper and clearer. Value-type caches (FlagData tuple, AllFlagsCombined) use a small reference-typed holder — a record class for FlagData, a boxed object for the TEnum — because Nullable<T> writes over multi-word structs are not atomic. Also revert Parse / Create to non-nullable string; callers that need to pass a nullable can apply ! themselves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Cache the [Flags] check and consolidate flag data - Look up FlagsAttribute once per closed generic type into a static readonly bool, so the guard on AllFlagValues / AllFlagsCombined / GetFlags is a field load instead of a reflection call. - Fold the tuple holder and the boxed-TEnum holder into a single FlagMeta record class (Values + Bits + Combined), so one pointer assignment publishes every flag-related field atomically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Replace FlagMeta record with published-flag init pattern The flag metadata now lives in three dedicated static fields plus a _flagMetaReady bool. The writer fills the value fields and then Volatile.Writes the flag to true; readers Volatile.Read the flag first and only touch the fields when it's set, so they can never observe a half-initialized state. Drops the per-type FlagMeta allocation. Under contention multiple threads may compute, but the compute is deterministic so identical bits land in each field regardless of interleaving. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Simplify EnumMeta cache: eager converters, per-property lazy flag data - Eagerly compile ToLong / FromLong into static readonly delegates so call sites pay a plain field read instead of a null-check + branch on every use (matters for GetFlags and OrAllFlagValues). - Split FlagValues and FlagsCombined back into independent ??= caches so touching one never forces the other to compute. - Cache HasFlagsAttribute once and branch off a bool, keeping the [Flags] guard O(1) on every subsequent access. - Drop the published-flag bool, the FlagMeta record, and the paired *Unchecked getters; the file is now a fraction of the size. Trade: Nullable<TEnum> can tear under concurrent first-access of a long-backed flag enum (16-byte struct). Acceptable for the narrow edge; int-and-smaller underlying types are atomic on 64-bit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Close the Nullable<TEnum> tear; collapse ScanForFlagValues to LINQ FlagsCombined now uses a plain TEnum field paired with a bool + Volatile release-store. The value fits atomically in a standalone field (max 8 bytes on 64-bit), and a reader that sees the bool set is guaranteed to see the prior value write. Drops the tearing edge that Nullable<TEnum> had on long-backed enums. ScanForFlagValues is a single-line LINQ Where/ToArray — same result, easier to read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Use LazyInitializer for exactly-once flag caches Both FlagValues and FlagsCombined now go through LazyInitializer.EnsureInitialized, which does double-checked locking via Monitor: fast path is a bool + value read, slow path takes a lazily-allocated lock, re-checks, runs the factory once, publishes. Exactly-once compute — no more redundant factory calls under first-access contention — and no size/atomicity edge cases from the backing field, since the bool gates the read. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Move [Flags] check into FlagValues factory Well-formed flag enums no longer pay the attribute check on every access to AllFlagValues/AllFlagsCombined/GetFlags — just the LazyInitializer fast path. Validation runs once inside ScanForFlagValues; factory throws propagate without caching so non-flag enums still throw on every access. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Merge enum extension blocks; simplify FlagValues caching One receiver-named extension block holds both static members (Parse, AllValues, AllFlagValues, AllFlagsCombined, ...) and the instance GetFlags, since C# extension blocks permit both. Receiver renamed to `source` to avoid clashing with the `value` parameter on Parse etc. FlagValues now uses a plain `??= ScanForFlagValues()`: reference assignment is atomic and the scan is deterministic, so a race that runs the factory twice is harmless. FlagsCombined still needs the LazyInitializer + bool flag because TEnum isn't atomic on 32-bit and default(TEnum) == 0 can be a valid computed result. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Split EnumMeta into EnumMeta + FlagEnumMeta Non-flag enums touching AllValues no longer trigger the Expression-tree compilation for ToLong/FromLong or the [Flags] reflection check — those move into FlagEnumMeta<TEnum>, whose cctor only fires when flag APIs are used. EnumMeta<TEnum> now holds just Enum.GetValues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * CR * Add .editorconfig with max_line_length; document wrapping preference max_line_length = 160 matches the longest one-liners already in the codebase. CLAUDE.md gets a Style bullet telling contributors — human or AI — not to pre-emptively wrap lambdas, method chains, or throws that fit within the limit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add .editorconfig to solution Other folder So it shows up in Solution Explorer alongside license.txt and readme.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Updated max line length * Document expression-bodied member style Expression-bodied methods/properties/getters/local functions stay on a single line when they fit. If they don't, switch to a block body rather than wrapping the => expression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rename build-and-test workflow to build Shorter name fits the GitHub Actions UI better. Renames the workflow file, the top-level name, the job name, and the publish job's needs reference. Updates the readme badge to point at build.yml. Branch protection rules referencing the old "build-and-test" check name will need to be updated to "build". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NonEmptyString.To*parsers toAs*and add matchingAs*overloads onstring?, all returning nullable primitives.AsEnum<TEnum>on bothstring?andNonEmptyString— rejects numeric strings, comma-separated flag combinations, and undefined members.EnumExtensions: cachedAllValues<T>(),AllFlagValues<T>()(single-bit members only),AllFlagsCombined<T>()(OR of every single-bit member), andGetFlags(this T value)decomposing a flag value into its constituent single-bit flags.Original prompt
Test plan
dotnet build— solution builds cleandotnet test— all 543 tests pass (including new FsCheck properties and worked examples forAs*,AsEnum,AllValues,AllFlagValues,AllFlagsCombined,GetFlags)🤖 Generated with Claude Code