Skip to content

Add As* parsing extensions and enum extensions#31

Merged
KaliCZ merged 18 commits intomainfrom
claude/happy-pike-6a482e
Apr 19, 2026
Merged

Add As* parsing extensions and enum extensions#31
KaliCZ merged 18 commits intomainfrom
claude/happy-pike-6a482e

Conversation

@KaliCZ
Copy link
Copy Markdown
Owner

@KaliCZ KaliCZ commented Apr 17, 2026

Summary

  • Rename NonEmptyString.To* parsers to As* and add matching As* overloads on string?, all returning nullable primitives.
  • Add AsEnum<TEnum> on both string? and NonEmptyString — rejects numeric strings, comma-separated flag combinations, and undefined members.
  • New EnumExtensions: cached AllValues<T>(), AllFlagValues<T>() (single-bit members only), AllFlagsCombined<T>() (OR of every single-bit member), and GetFlags(this T value) decomposing a flag value into its constituent single-bit flags.

Original prompt

There's extensions for string and nonempty string which parse the string into something else. These should be called AsXXX, not ToXXX and should return a nullable value.
There should also be an extension AsEnum which would return the actual T Enum.

There should also be these extensions on every T where the T is an enum

AllValues - giving a cached IReadonlyList of values
AllFlagValues - same but for flag values = only multiples of 2 (also cached after first call)
AllFlagsCombined - gives the aggregated flag values into a single value that I can for example store in the DB. (also cached after first call)

GetFlags on an actual T value - which will translate the value into a list of individual flags it comprises of.

Afterwards create a PR and make sure that this prompt is visible there, so that everybody can see that this code was genuinely created inside this PR by claude code, therefore cannot violate any IP rights.

Test plan

  • dotnet build — solution builds clean
  • dotnet test — all 543 tests pass (including new FsCheck properties and worked examples for As*, AsEnum, AllValues, AllFlagValues, AllFlagsCombined, GetFlags)

🤖 Generated with Claude Code

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>
@KaliCZ KaliCZ self-assigned this Apr 17, 2026
KaliCZ and others added 16 commits April 17, 2026 15:57
…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>
@KaliCZ KaliCZ enabled auto-merge (squash) April 19, 2026 05:36
@KaliCZ KaliCZ disabled auto-merge April 19, 2026 05:36
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 KaliCZ enabled auto-merge (squash) April 19, 2026 05:39
@KaliCZ KaliCZ disabled auto-merge April 19, 2026 05:40
@KaliCZ KaliCZ merged commit ea5a387 into main Apr 19, 2026
2 checks passed
@KaliCZ KaliCZ deleted the claude/happy-pike-6a482e branch April 19, 2026 05:40
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant