Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[0.3.0] - 2026-09-26
A performance release. Guards run on every call of the code they protect, so this release removes their remaining costs.
Highlights
- A passing guard no longer allocates and costs about as much as a
hand-written
if: about 1 ns instead of 5–13 ns and up to 160 bytes. - Selectors that capture local variables or use operators (
+,??,?:,new) are cached like other selectors: about 0.5–1 µs per call instead of 50–85 µs and 9–10 KB. - Selectors whose path goes through an indexer, an array element or a method
call (
o => o.Items[0],o => o.Name.Trim()) work; they used to throw. - The benchmarks now run on .NET 8 and .NET 10 and report allocations.
Upgrading from 0.2.0
The public API is unchanged, so assemblies compiled against 0.2.0 work
without being rebuilt. One behaviour change: Is.NullOrEmpty with a selector
now applies the same rules as ThrowIf.NullOrEmpty, so a nullable value type
holding its default value (int? x = 0) counts as empty.
Changed
- A passing
ThrowIf.*guard no longer allocates. Guards used to pass a throwing lambda to an internal helper, which allocated a closure (and on .NET 8 a delegate) on every call, 32–120 bytes, even when nothing was thrown. A passing guard now costs about as much as a hand-writtenif(about 0.6 ns for anintcomparison, down from 5–10 ns). Callbacks are invoked as before:Failurejust before the guard throws,Successotherwise, with exceptions from the callback ignored. - Selectors that capture local variables (
_ => captured.Name,o => o.Items[index]) are now cached like other selectors: the compiled delegate is shared and each call passes its own captured values. They used to be compiled on every call, about 85 µs and 10 KB each; a call now takes about 0.5 µs, most of it spent building the expression tree at the call site. - Selectors that use operators (
+,==,!, ...),??,?:,is,newor array creation are cached too; they were compiled on every call, about 50 µs and 9 KB each, and now take about 1 µs. Only selectors with a nested lambda, an invocation or a member or collection initializer are still compiled on every call. - Each value on a selector's path is read once. Paths were evaluated again for every null check, so a getter on the path ran once per level below it.
Is.NullOrEmptywith a selector now applies the same rules to the selected value asThrowIf.NullOrEmpty. A nullable value type holding its default value (int? x = 0) now counts as empty for both, as it does without a selector.- Passing guards no longer box
decimaloperands on .NET 8. The argument null checks calledArgumentNullException.ThrowIfNull, which takes anobject, and the NaN check matched type patterns on the operands; a passingThrowIf.Betweenondecimalallocated 160 bytes. NullOrEmptychecks a reference-typed value for collections before the value-type patterns and skips the default-value comparison, which is already covered by the null check: about 0.7 ns instead of 5–7 ns for a list or array.Is.Any,Is.All,ThrowIf.AnyandThrowIf.Allread arrays andList<T>as spans, so they no longer allocate an enumerator on .NET 8.- The
ThrowIf.*<TException>overloads with anew()constraint create the exception withnew TException()instead of reflection.
Fixed
NullOrEmptyselectors whose path goes through an indexer, an array element or a method call (o => o.Items[0],o => o.Tags[0],o => o.Name.Trim()), or that combine members (o => o.First + o.Last), threwArgumentExceptionorInvalidOperationExceptionwhile being compiled. They now work: a null on the path counts as empty, as with member paths, and other expressions are evaluated as written.- The XML documentation of
Is.Betweendescribed the bounds as exclusive; both bounds are inclusive, as the code and the rest of the documentation state. TheThrowIf.Between<TException>string overload no longer calls the bounds an "allowed range".
Documentation
- Warnings that
ThrowIf.Betweenthrows when the value is inside the range, thatNullOrEmptyenumerates lazy sequences (runningIQueryablequeries) and calls property getters when a selector points at a complex type, and that callbacks are not suitable for audit or security logging because their exceptions are swallowed. - The performance guide recommends
Is.*overThrowIf.*for input that is often invalid or can be sent invalid on purpose, since each rejected request pays for a throw (about 2 µs on .NET 10, 13 µs on .NET 8). - The benchmark results in
SGuard.Benchmark/benchmarks/are re-recorded on .NET 8 and .NET 10 with allocations, and the README summarises them. The benchmark project accepts BenchmarkDotNet's command-line options (--filter,--runtimes,--job).
[0.2.0] - 2026-09-26
This release contains breaking changes; see Changed and Removed. Assemblies compiled against 0.1.2 must be rebuilt.
Added
Is.Emailvalidates email addresses with a built-in ASCII pattern (at most 254 characters, no trailing line break), or with a custom pattern that stops afterIs.DefaultEmailRegexTimeout(1 second) or an explicitmatchTimeoutand then throwsRegexMatchTimeoutException.ReadOnlySpan<T>overloads forIs.NullOrEmpty,ThrowIf.NullOrEmpty,Is.All,Is.Any,ThrowIf.AllandThrowIf.Any.SGuardOptions.IncludeValuesInExceptionsto include checked values in built-in exception messages andException.Data. Values are written withToString()and truncated to 64 characters.- The NuGet package now contains XML documentation (IntelliSense) and a
symbol package (
.snupkg) for Source Link.
Changed
- Built-in exceptions (
NullOrEmptyException,BetweenException,GreaterThanException,GreaterThanOrEqualException,LessThanException,LessThanOrEqualException,AllException,AnyException) now derive fromArgumentExceptioninstead ofException.ParamNameholds the caller's argument expression (e.g.request.Age); the message is unchanged. - Built-in exceptions no longer include the checked values in
MessageorException.DataunlessSGuardOptions.IncludeValuesInExceptionsis enabled. When enabled,Exception.Dataholds the formatted strings instead of the values. ThrowIf.Between,GreaterThan,GreaterThanOrEqual,LessThan,LessThanOrEqual,NullOrEmptyand the matchingThrow.*Exceptionhelpers take optional[CallerArgumentExpression]parameters.- Comparison guards treat a floating-point NaN operand (
double,float,Half) as failing:Is.*returnsfalseandThrowIf.*throws. NaN previously passedThrowIf.GreaterThan/GreaterThanOrEqualand madeIs.LessThanreturntrue. Is.BetweenandThrowIf.BetweenthrowArgumentExceptionwhenminis greater thanmax(for bounds of the same type, and for the string overloads). Reversed bounds previously matched nothing, so the guard never fired.Is.AllandThrowIf.Allon an emptyReadOnlySpan<T>(which arrays bind to on C# 14) now behave like theIEnumerable<T>overload andEnumerable.All:Is.AllreturnstrueandThrowIf.Allthrows. Previously an empty array and an empty list gave opposite results.- The span overloads of
Is.All,Is.Any,ThrowIf.AllandThrowIf.Anyvalidate their arguments even when the span is empty. Is.NullOrEmptyandThrowIf.NullOrEmptyon aReadOnlySpan<T>treat only an empty span as empty. A span whose elements were allnullused to count as empty, while the same array or list did not.
Removed
- The
net6.0andnet7.0targets. The package targetsnet8.0,net9.0andnet10.0.
Fixed
- Selector-based
NullOrEmptyguards cache compiled selectors by expression structure. The previous cache was keyed by expression instance and never hit, so every call recompiled the selector; calls are now roughly 40–50x faster with about 90% less allocation. Selectors that read captured variables are still compiled on every call. - Selector-based
NullOrEmptyguards no longer overflow the stack on self-referencing or recursively generic types. A type already being inspected on the same path, or nested more than 8 complex types deep, is only checked for null. Indexed properties are skipped instead of throwing. - Selector-based
NullOrEmptychecks on enumerable members dispose the enumerator they create. ThrowIf.NullOrEmpty(value, selector)throws a newNullOrEmptyExceptionon every failure, with a message naming the selector (e.g.Value 'o => o.Name' is null or empty.). It previously rethrew one shared instance, whose stack trace and data were overwritten by concurrent callers.- Built-in exception messages name the caller's argument expressions (e.g.
left=request.Age). They previously always showed the guard's own parameter names (value=value). ThrowIf.AnyandThrowIf.Allinvoke the callback once instead of twice.ThrowIf.AllandThrowIf.Anyno longer allocate their default exception when the guard passes.
[0.1.2] - 2025-10-14
Added
- Added a "📊 Benchmarks" section to the main README.md, providing a direct link to the SGuard.Benchmark/benchmarks/ folder for easy developer access to performance results. (#28)
- Ensured all benchmark results are discoverable and documented for each guard method (Is.* and ThrowIf.*), including All, Any, Between, GreaterThan, LessThan, and NullOrEmpty.
- No breaking changes to the core library or APIs.
- Improved developer experience and documentation clarity.
This update makes it much easier for contributors and users to find and review performance benchmarks for all guard methods.
[0.1.1] - 2025-09-05
Changed
- Throw.cs has been released for public use.
- ExceptionActivator.cs has been released for public use.
- Improved code readability and maintainability.
Added
- Added XML documentation comments.
Notes
[0.1.0] - 2025-09-04
Changed
- Versioning reset: re-released the package starting from
0.1.0. - Previous NuGet versions have been unlisted/removed.
Added
- README updates: badges, “What’s New in 0.1.0”, and a “Test and Coverage Status” section with auto-updated results.
- Continuous integration workflow that runs tests, generates coverage, and updates README badges/summary.
- Packaging ensures README, LICENSE, and icon are included.
Notes
- No functional breaking changes are expected for consumers adopting this version.
[2.1.0] - 2025-01-03
Changed
- BREAKING CHANGE: Changed license from GPL-3.0 to MIT
- Updated assembly version to 2.1.0
- Updated package metadata
Added
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- Enhanced documentation
[2.0.x] - Previous versions
- Previous functionality under GPL-3.0 license