You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Identifiers, codes, tokens, hashes, and other normalized strings commonly have invariants about every character they contain. Callers currently express those invariants with repeated loops, regular expressions, or parsing APIs, obscuring intent and often introducing avoidable allocations. Add named predicates and fluent guards for digit, alphanumeric, whitespace, casing, Base64, and hexadecimal content.
The additions must mirror the character-oriented IsAscii overload shapes, behave identically across all package targets, remain allocation-free per invocation, use optimized .NET 10 framework primitives where their semantics match, and participate in the customizable single-file source distribution.
Acceptance Criteria
ContainsOnlyDigits / MustContainOnlyDigits, ContainsOnlyLettersOrDigits / MustContainOnlyLettersOrDigits, IsUpperCase / MustBeUpperCase, IsLowerCase / MustBeLowerCase, IsBase64 / MustBeBase64, and IsHexadecimal / MustBeHexadecimal are available for string, Span<char>, ReadOnlySpan<char>, Memory<char>, and ReadOnlyMemory<char> on .NET Standard 2.0, .NET Standard 2.1, and .NET 10.
MustNotContainWhiteSpace is available for the same five receiver shapes and target frameworks.
Digit and alphanumeric scans use the same Unicode character-classification semantics as the existing IsDigit(char) and IsLetterOrDigit(char) predicates; hexadecimal accepts only ASCII 0-9, A-F, and a-f.
IsUpperCase means that the input contains no character classified as lowercase, and IsLowerCase means that it contains no character classified as uppercase. Uncased letters, digits, punctuation, symbols, and whitespace do not by themselves violate either predicate.
MustNotContainWhiteSpace rejects every character recognized by the existing IsWhiteSpace(char) predicate, including non-ASCII Unicode whitespace, and accepts an empty input.
IsBase64 validates standard, decodable Base64 without decoding it: it accepts the A-Z, a-z, 0-9, +, and / alphabet; enforces valid quartet and trailing-padding structure; permits only space, tab, carriage return, and line feed as ignorable whitespace anywhere; and rejects Base64Url-only characters and malformed input.
Empty strings and buffers satisfy every predicate and throwing guard. A null string returns false from every predicate and causes every default throwing guard to throw ArgumentNullException.
Every throwing overload returns the original successfully validated receiver in its exact shape, captures the guarded expression for default exceptions, accepts an optional custom message, and has a custom-exception-factory counterpart consistent with the existing string, span, and memory APIs. Invalid non-null content throws StringException by default.
The existing MustBeAscii overloads for string, Span<char>, ReadOnlySpan<char>, Memory<char>, and ReadOnlyMemory<char> also throw StringException for invalid non-null content; the scalar char and byte overloads and all byte-buffer overloads retain ArgumentException because they do not validate string content.
All scans short-circuit once the outcome is known, use O(n) time and constant additional space, and perform no per-invocation allocation, copying, regular-expression evaluation, LINQ enumeration, normalization, decoding, or case conversion.
The .NET 10 Base64 implementation uses Base64.IsValid; other .NET 10 implementations use vectorized or otherwise optimized framework span APIs only where they preserve the defined semantics and benchmarks demonstrate an advantage over the portable scalar loop. Portable assets remain allocation-free and observably identical.
Automated tests cover null and empty values; valid and invalid content at the beginning, middle, and end; every receiver and fluent return shape; default StringException contracts and custom messages; caller argument expressions; custom factories; Unicode digits, letters, casing, and whitespace; uncased content; both hexadecimal cases; representative valid and malformed Base64 alphabet, length, padding, whitespace, and Base64Url cases; and the revised character-sequence versus retained scalar/byte MustBeAscii exception behavior.
Semantic conformance tests exhaustively classify every UTF-16 code-unit value for the digit, alphanumeric, whitespace, casing, and hexadecimal contracts, and directly compare the actual portable Base64 validator with .NET 10 Base64.IsValid over a deterministic corpus of valid and malformed inputs.
Microbenchmarks compare representative short and long successful and failing scans with equivalent imperative checks, include ASCII and mixed Unicode scenarios for proposed modern fast paths, report allocations, confirm 0 B/op for predicates and successful guards after any one-time framework lookup initialization, and provide the evidence for retaining any fast path that can otherwise add a second scan.
The source-export whitelist catalog and committed settings contain every new assertion family, and focused source-export tests verify guard-to-predicate/helper reachability, framework-conditional code, and exception-factory trimming.
The committed .NET Standard 2.0 single-file distribution is regenerated with the portable implementations, and generated source validates for both .NET Standard 2.0 and .NET 10.
XML documentation and the assertion overview describe the supported receiver shapes, null and empty behavior, Unicode versus ASCII classification, casing definition, Base64 whitespace and padding rules, allocation behavior, and target-specific acceleration.
The complete solution restores and builds without warnings in Release configuration, and all automated tests pass on the pinned SDK.
Technical Details
Follow the existing Check.IsAscii.cs and Check.MustBeAscii.cs organization and delegation model. Each predicate family should have one ReadOnlySpan<char> implementation; string, mutable span, and both memory shapes delegate to it without copying. Each guard should likewise centralize validation in its read-only span overload, then return the original mutable span or memory value. String guards retain nullable-flow annotations and first apply the established null behavior. Use ReadOnlySpanExceptionFactory<char> for span-backed custom factories, consistent with MustBeAscii.
The requested families are:
ContainsOnlyDigits and MustContainOnlyDigits;
ContainsOnlyLettersOrDigits and MustContainOnlyLettersOrDigits;
MustNotContainWhiteSpace (throwing-only);
IsUpperCase and MustBeUpperCase;
IsLowerCase and MustBeLowerCase;
IsBase64 and MustBeBase64; and
IsHexadecimal and MustBeHexadecimal.
Digit and alphanumeric semantics deliberately compose with the library's existing scalar predicates rather than introducing an ASCII-only interpretation under an unqualified name. Classify UTF-16 code units with char.IsDigit / char.IsLetterOrDigit (or the existing extension predicates), including their Unicode behavior. A .NET 10 vectorized ASCII allowed-set or range search may accept the common all-ASCII case quickly; if it encounters a non-ASCII code unit, continue with the Unicode scalar classifier so the result remains identical to portable targets. Retain such a fast path only when the required benchmarks show that its additional complexity and possible second scan improve the intended workloads. Do not silently reject Unicode decimal digits or letters in the optimized path.
For casing, scan for the prohibited category rather than requiring at least one cased letter: IsUpperCase fails on any code unit for which char.IsLower is true, while IsLowerCase fails on any code unit for which char.IsUpper is true. This intentionally makes empty and entirely uncased inputs satisfy both predicates. Use invariant Unicode classification only; culture-sensitive conversion or comparison would make guard behavior depend on the current culture and would allocate or transform the input.
Whitespace follows char.IsWhiteSpace, matching the library's existing scalar predicate. Do not implement it with an ASCII-only search or with string.IsNullOrWhiteSpace, because the former changes Unicode behavior and the latter answers whether all characters are whitespace rather than whether any character is whitespace. A Boolean DoesNotContainWhiteSpace counterpart is out of scope because it was not requested.
Hexadecimal is a character-set scan, not a numeric parse: length, evenness, prefixes such as 0x and #, sign, separators, and numeric range are irrelevant. On .NET 10, use char.IsAsciiHexDigit or a benchmark-supported optimized span allowed-set; portable code checks the same three ASCII ranges directly. The per-call path must not construct an allowed-character collection.
Base64 is a structural scan but does not produce decoded bytes. Its contract must match .NET 10System.Buffers.Text.Base64.IsValid(ReadOnlySpan<char>), including vacuous validity for empty or whitespace-only input and the exact ignorable whitespace set (' ', '\t', '\r', '\n'). Portable targets need a small stateful validator for alphabet, effective non-whitespace length, terminal = placement, and padding count; do not use Convert.FromBase64String, temporary byte buffers, or regexes. Keep the actual portable implementation directly testable against Base64.IsValid under .NET 10 so the differential test does not validate a duplicated test-side algorithm. Use a deterministic corpus that varies effective length, alphabet classes, whitespace placement, padding placement and count, and Base64Url-only characters. Keep this distinct from Base64Url, and do not add decoded-length outputs or byte-span overloads as part of this issue.
Use framework conditionals already understood by the source exporter. Suitable .NET 10 building blocks include MemoryExtensions.ContainsAnyExceptInRange, reusable static SearchValues<char> instances with ContainsAnyExcept / IndexOfAnyExcept, and Base64.IsValid; choose the smallest primitive that preserves the defined semantics, and require benchmark evidence before keeping a non-Base64 fast path that can perform extra work on fallback. Verify the generated modern source contains the selected fast paths while the portable output contains no unavailable APIs. One-time static lookup construction is acceptable, but steady-state predicate and successful-guard invocations must allocate nothing and remain Native AOT compatible.
Exhaustive single-code-unit tests are practical because the char domain contains only 65,536 values. Compare each value with the defining char.IsDigit, char.IsLetterOrDigit, char.IsWhiteSpace, char.IsLower / char.IsUpper, or ASCII hexadecimal classification as appropriate, then retain multi-character boundary and mixed-content tests to verify scan composition and short-circuiting.
Default non-null content failures use the existing StringException, preserving the library's string-specific exception hierarchy while remaining catch-compatible with ArgumentException. Use focused throw helpers with concise messages naming the guarded expression and required invariant without embedding the full input, which may be large or sensitive. Null strings continue to use ArgumentNullException, and custom factories receive null or invalid values exactly as MustBeAscii does. Existing specialized exceptions such as SubstringException, StringDoesNotMatchException, and WhiteSpaceStringException do not match these contracts, and no new public exception type is required.
Align the unpublished MustBeAscii character-sequence API with this contract: invalid string, Span<char>, ReadOnlySpan<char>, Memory<char>, and ReadOnlyMemory<char> values throw StringException. Keep ArgumentException for char, byte, Span<byte>, ReadOnlySpan<byte>, Memory<byte>, and ReadOnlyMemory<byte>, whose receivers are scalars or byte buffers rather than strings. Preserve ArgumentNullException for a null string and all existing custom-factory behavior. Update its XML documentation, tests, throw-helper reachability, and generated single-file output together with the new families.
Add every family to AssertionWhitelist and settings.json, extend source-export reachability and trimming coverage, regenerate Light.GuardClauses.SingleFile.cs as the .NET Standard 2.0 artifact, and document the APIs in the string section of docs/assertion-overview.md. Benchmark the read-only span core because all public receiver shapes delegate to it, while also including representative string predicates and successful guards to detect accidental wrapper allocations.
Rationale
Identifiers, codes, tokens, hashes, and other normalized strings commonly have invariants about every character they contain. Callers currently express those invariants with repeated loops, regular expressions, or parsing APIs, obscuring intent and often introducing avoidable allocations. Add named predicates and fluent guards for digit, alphanumeric, whitespace, casing, Base64, and hexadecimal content.
The additions must mirror the character-oriented
IsAsciioverload shapes, behave identically across all package targets, remain allocation-free per invocation, use optimized .NET 10 framework primitives where their semantics match, and participate in the customizable single-file source distribution.Acceptance Criteria
ContainsOnlyDigits/MustContainOnlyDigits,ContainsOnlyLettersOrDigits/MustContainOnlyLettersOrDigits,IsUpperCase/MustBeUpperCase,IsLowerCase/MustBeLowerCase,IsBase64/MustBeBase64, andIsHexadecimal/MustBeHexadecimalare available forstring,Span<char>,ReadOnlySpan<char>,Memory<char>, andReadOnlyMemory<char>on .NET Standard 2.0, .NET Standard 2.1, and .NET 10.MustNotContainWhiteSpaceis available for the same five receiver shapes and target frameworks.IsDigit(char)andIsLetterOrDigit(char)predicates; hexadecimal accepts only ASCII0-9,A-F, anda-f.IsUpperCasemeans that the input contains no character classified as lowercase, andIsLowerCasemeans that it contains no character classified as uppercase. Uncased letters, digits, punctuation, symbols, and whitespace do not by themselves violate either predicate.MustNotContainWhiteSpacerejects every character recognized by the existingIsWhiteSpace(char)predicate, including non-ASCII Unicode whitespace, and accepts an empty input.IsBase64validates standard, decodable Base64 without decoding it: it accepts theA-Z,a-z,0-9,+, and/alphabet; enforces valid quartet and trailing-padding structure; permits only space, tab, carriage return, and line feed as ignorable whitespace anywhere; and rejects Base64Url-only characters and malformed input.falsefrom every predicate and causes every default throwing guard to throwArgumentNullException.StringExceptionby default.MustBeAsciioverloads forstring,Span<char>,ReadOnlySpan<char>,Memory<char>, andReadOnlyMemory<char>also throwStringExceptionfor invalid non-null content; the scalarcharandbyteoverloads and all byte-buffer overloads retainArgumentExceptionbecause they do not validate string content.Base64.IsValid; other .NET 10 implementations use vectorized or otherwise optimized framework span APIs only where they preserve the defined semantics and benchmarks demonstrate an advantage over the portable scalar loop. Portable assets remain allocation-free and observably identical.StringExceptioncontracts and custom messages; caller argument expressions; custom factories; Unicode digits, letters, casing, and whitespace; uncased content; both hexadecimal cases; representative valid and malformed Base64 alphabet, length, padding, whitespace, and Base64Url cases; and the revised character-sequence versus retained scalar/byteMustBeAsciiexception behavior.Base64.IsValidover a deterministic corpus of valid and malformed inputs.0 B/opfor predicates and successful guards after any one-time framework lookup initialization, and provide the evidence for retaining any fast path that can otherwise add a second scan.Technical Details
Follow the existing
Check.IsAscii.csandCheck.MustBeAscii.csorganization and delegation model. Each predicate family should have oneReadOnlySpan<char>implementation;string, mutable span, and both memory shapes delegate to it without copying. Each guard should likewise centralize validation in its read-only span overload, then return the original mutable span or memory value. String guards retain nullable-flow annotations and first apply the established null behavior. UseReadOnlySpanExceptionFactory<char>for span-backed custom factories, consistent withMustBeAscii.The requested families are:
ContainsOnlyDigitsandMustContainOnlyDigits;ContainsOnlyLettersOrDigitsandMustContainOnlyLettersOrDigits;MustNotContainWhiteSpace(throwing-only);IsUpperCaseandMustBeUpperCase;IsLowerCaseandMustBeLowerCase;IsBase64andMustBeBase64; andIsHexadecimalandMustBeHexadecimal.Digit and alphanumeric semantics deliberately compose with the library's existing scalar predicates rather than introducing an ASCII-only interpretation under an unqualified name. Classify UTF-16 code units with
char.IsDigit/char.IsLetterOrDigit(or the existing extension predicates), including their Unicode behavior. A .NET 10 vectorized ASCII allowed-set or range search may accept the common all-ASCII case quickly; if it encounters a non-ASCII code unit, continue with the Unicode scalar classifier so the result remains identical to portable targets. Retain such a fast path only when the required benchmarks show that its additional complexity and possible second scan improve the intended workloads. Do not silently reject Unicode decimal digits or letters in the optimized path.For casing, scan for the prohibited category rather than requiring at least one cased letter:
IsUpperCasefails on any code unit for whichchar.IsLoweris true, whileIsLowerCasefails on any code unit for whichchar.IsUpperis true. This intentionally makes empty and entirely uncased inputs satisfy both predicates. Use invariant Unicode classification only; culture-sensitive conversion or comparison would make guard behavior depend on the current culture and would allocate or transform the input.Whitespace follows
char.IsWhiteSpace, matching the library's existing scalar predicate. Do not implement it with an ASCII-only search or withstring.IsNullOrWhiteSpace, because the former changes Unicode behavior and the latter answers whether all characters are whitespace rather than whether any character is whitespace. A BooleanDoesNotContainWhiteSpacecounterpart is out of scope because it was not requested.Hexadecimal is a character-set scan, not a numeric parse: length, evenness, prefixes such as
0xand#, sign, separators, and numeric range are irrelevant. On .NET 10, usechar.IsAsciiHexDigitor a benchmark-supported optimized span allowed-set; portable code checks the same three ASCII ranges directly. The per-call path must not construct an allowed-character collection.Base64 is a structural scan but does not produce decoded bytes. Its contract must match
.NET 10System.Buffers.Text.Base64.IsValid(ReadOnlySpan<char>), including vacuous validity for empty or whitespace-only input and the exact ignorable whitespace set (' ','\t','\r','\n'). Portable targets need a small stateful validator for alphabet, effective non-whitespace length, terminal=placement, and padding count; do not useConvert.FromBase64String, temporary byte buffers, or regexes. Keep the actual portable implementation directly testable againstBase64.IsValidunder .NET 10 so the differential test does not validate a duplicated test-side algorithm. Use a deterministic corpus that varies effective length, alphabet classes, whitespace placement, padding placement and count, and Base64Url-only characters. Keep this distinct from Base64Url, and do not add decoded-length outputs or byte-span overloads as part of this issue.Use framework conditionals already understood by the source exporter. Suitable .NET 10 building blocks include
MemoryExtensions.ContainsAnyExceptInRange, reusable staticSearchValues<char>instances withContainsAnyExcept/IndexOfAnyExcept, andBase64.IsValid; choose the smallest primitive that preserves the defined semantics, and require benchmark evidence before keeping a non-Base64 fast path that can perform extra work on fallback. Verify the generated modern source contains the selected fast paths while the portable output contains no unavailable APIs. One-time static lookup construction is acceptable, but steady-state predicate and successful-guard invocations must allocate nothing and remain Native AOT compatible.Exhaustive single-code-unit tests are practical because the
chardomain contains only 65,536 values. Compare each value with the definingchar.IsDigit,char.IsLetterOrDigit,char.IsWhiteSpace,char.IsLower/char.IsUpper, or ASCII hexadecimal classification as appropriate, then retain multi-character boundary and mixed-content tests to verify scan composition and short-circuiting.Default non-null content failures use the existing
StringException, preserving the library's string-specific exception hierarchy while remaining catch-compatible withArgumentException. Use focused throw helpers with concise messages naming the guarded expression and required invariant without embedding the full input, which may be large or sensitive. Null strings continue to useArgumentNullException, and custom factories receive null or invalid values exactly asMustBeAsciidoes. Existing specialized exceptions such asSubstringException,StringDoesNotMatchException, andWhiteSpaceStringExceptiondo not match these contracts, and no new public exception type is required.Align the unpublished
MustBeAsciicharacter-sequence API with this contract: invalidstring,Span<char>,ReadOnlySpan<char>,Memory<char>, andReadOnlyMemory<char>values throwStringException. KeepArgumentExceptionforchar,byte,Span<byte>,ReadOnlySpan<byte>,Memory<byte>, andReadOnlyMemory<byte>, whose receivers are scalars or byte buffers rather than strings. PreserveArgumentNullExceptionfor a null string and all existing custom-factory behavior. Update its XML documentation, tests, throw-helper reachability, and generated single-file output together with the new families.Add every family to
AssertionWhitelistandsettings.json, extend source-export reachability and trimming coverage, regenerateLight.GuardClauses.SingleFile.csas the .NET Standard 2.0 artifact, and document the APIs in the string section ofdocs/assertion-overview.md. Benchmark the read-only span core because all public receiver shapes delegate to it, while also including representative string predicates and successful guards to detect accidental wrapper allocations.