diff --git a/.github/workflows/release-on-nuget.yml b/.github/workflows/release-on-nuget.yml
index f159928c..ccd31c70 100644
--- a/.github/workflows/release-on-nuget.yml
+++ b/.github/workflows/release-on-nuget.yml
@@ -20,12 +20,13 @@ jobs:
env:
LIGHT_GUARDCLAUSES_SNK: ${{ secrets.LIGHT_GUARDCLAUSES_SNK }}
run: |
- echo $LIGHT_GUARDCLAUSES_SNK | base64 --decode > ./src/Light.GuardClauses/Light.GuardClauses.snk
+ printf '%s' "$LIGHT_GUARDCLAUSES_SNK" | base64 --decode > "./src/Light.GuardClauses/Light.GuardClauses.snk"
- name: Create NuGet packages
run: dotnet pack ./src/Light.GuardClauses/Light.GuardClauses.csproj -c Release /p:SignAssembly=true /p:AssemblyOriginatorKeyFile=Light.GuardClauses.snk /p:ContinuousIntegrationBuild=true
- name: Delete SNK file
- run: rm ./src/Light.GuardClauses/Light.GuardClauses.snk
+ if: always()
+ run: rm -f "./src/Light.GuardClauses/Light.GuardClauses.snk"
- name: Push nupkg package
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
- run: dotnet nuget push "./src/Light.GuardClauses/bin/Release/Light.GuardClauses.*.nupkg" --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json
+ run: dotnet nuget push "./src/Light.GuardClauses/bin/Release/Light.GuardClauses.*.nupkg" --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json
diff --git a/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml b/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml
index 95beddb6..1619d777 100644
--- a/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml
+++ b/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml
@@ -2,6 +2,7 @@
+ .github
ai-plans
diff --git a/Directory.Build.props b/Directory.Build.props
index c580c199..7567f664 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,10 +1,10 @@
- 14.0.0
+ 15.0.0
14
Kenny Pflug
Kenny Pflug
- Copyright © Kenny Pflug 2016, 2025
+ Copyright © Kenny Pflug 2016, 2026
false
diff --git a/Light.GuardClauses.SingleFile.cs b/Light.GuardClauses.SingleFile.cs
index d9f0761b..05890bf9 100644
--- a/Light.GuardClauses.SingleFile.cs
+++ b/Light.GuardClauses.SingleFile.cs
@@ -1,11 +1,11 @@
/* ------------------------------
- Light.GuardClauses 14.0.0
+ Light.GuardClauses 15.0.0
------------------------------
License information for Light.GuardClauses
The MIT License (MIT)
-Copyright (c) 2016, 2025 Kenny Pflug mailto:kenny.pflug@live.de
+Copyright (c) 2016, 2026 Kenny Pflug mailto:kenny.pflug@live.de
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -13554,4 +13554,4 @@ public CallerArgumentExpressionAttribute(string parameterName)
public string ParameterName { get; }
}
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index d542af68..80692fbb 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,16 @@
[](docs/README.md)
[](https://github.com/feO2x/Light.GuardClauses/releases)
+## Why Light.GuardClauses?
+
+- 🧰 **130+ assertions** cover nullability, collections, text, numbers, ranges, dates, URIs, types, streams, and more.
+- ⚡ **As fast as handwritten guards** — most assertions are optimized and benchmarked against equivalent imperative checks using a dedicated BenchmarkDotNet suite.
+- 🏷️ **Automatic parameter names** - with C# 10 and newer, `CallerArgumentExpression` produces clear exceptions without repetitive `nameof` calls.
+- 🔄 **Validation and assignment in one statement** works because throwing guards return the successfully validated value.
+- 🧩 **Custom exception factories** let you control exception construction when the built-in exception does not fit your application.
+- 🧠 **Tooling-aware contracts** support Nullable Reference Types for Roslyn, .NET code analysis, and JetBrains annotations.
+- 🚀 **Flexible deployment** spans broad .NET compatibility, Native AOT, and optional single-file source inclusion.
+
Light.GuardClauses replaces repetitive parameter checks with expressive extension methods:
```csharp
@@ -15,10 +25,7 @@ public class Foo
{
private readonly IBar _bar;
- public Foo(IBar? bar)
- {
- _bar = bar.MustNotBeNull();
- }
+ public Foo(IBar? bar) => _bar = bar.MustNotBeNull();
}
```
@@ -35,24 +42,33 @@ public void SetMovieRating(Guid movieId, int numberOfStars)
}
```
-See the [documentation index](docs/README.md), [assertion overview](docs/assertion-overview.md), and [usage guide](docs/structuring-precondition-checks.md) for more.
-
-## Supported platforms
+Custom exception factories let you replace a guard's default exception with one that fits your application. Some factories receive the values involved in the validation:
-The NuGet package contains these assets:
-
-| Target | Purpose |
-| --- | --- |
-| .NET Standard 2.0 | Broad compatibility, including implementations of .NET Standard 2.0 |
-| .NET Standard 2.1 | Implementations of .NET Standard 2.1 |
-| .NET 10 | The current .NET asset, including modern generic-math, span, and memory overloads and Native AOT compatibility |
+```csharp
+numberOfStars.MustBeIn(
+ Range.InclusiveBetween(0, 5),
+ (rating, range) => new InvalidOperationException(
+ $"The rating {rating} is outside the allowed {range}."
+ )
+);
+```
-Caller argument expressions are understood by C# 10 and newer compilers and automatically capture expressions such as `bar` for the exception parameter name. This is independent of the target framework. With an older C# compiler, pass the name explicitly:
+With C# 10 or newer, caller argument expressions automatically capture expressions such as `movieId` or `numberOfStars` for the exception parameter name. When using an older compiler, pass the name explicitly:
```csharp
-bar.MustNotBeNull(nameof(bar));
+movieId.MustNotBeEmpty(nameof(movieId));
```
+See the [documentation index](docs/README.md), [assertion overview](docs/assertion-overview.md), and [usage guide](docs/structuring-precondition-checks.md) for more.
+
+## Target frameworks
+
+The NuGet package contains three target-framework assets. NuGet automatically selects the best compatible asset for the consuming project:
+
+- **.NET Standard 2.0** provides the portable Light.GuardClauses API with the broadest runtime compatibility. It is selected for .NET Framework and other implementations that support .NET Standard 2.0 but not 2.1.
+- **.NET Standard 2.1** provides the portable Light.GuardClauses API for newer .NET implementations. It is selected for .NET Core 3.0+, .NET 5–9, and other implementations that support .NET Standard 2.1.
+- **.NET 10** provides the full API for .NET 10 and later, including generic-math overloads, additional span and memory overloads, trimming annotations, framework-optimized implementations, and declared Native AOT compatibility.
+
## Installation
Light.GuardClauses is available from [NuGet](https://www.nuget.org/packages/Light.GuardClauses/).
@@ -61,7 +77,7 @@ Light.GuardClauses is available from [NuGet](https://www.nuget.org/packages/Ligh
- Package Manager Console: `Install-Package Light.GuardClauses`
- Project file: ``
-To embed the library without a DLL dependency, use the committed [.NET Standard 2.0 single-file distribution](Light.GuardClauses.SingleFile.cs) or create a tailored file with the [source exporter](docs/source-code-inclusion.md).
+To embed the library without a DLL dependency, use the committed [.NET Standard 2.0 single-file distribution](Light.GuardClauses.SingleFile.cs) or create a tailored file with the [Light.GuardClauses.SourceCodeTransformation project](docs/source-code-inclusion.md).
## Design and quality
@@ -69,4 +85,6 @@ The library supports nullable reference types, .NET code-analysis attributes, Je
For the design history behind guard clauses and design by contract, see [Guard clause background](docs/guard-clause-background.md).
+## Let there be Light!
+

diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 5ff4371a..22105020 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -17,10 +17,18 @@
true
README.md
- Light.GuardClauses 14.0.0
+ Light.GuardClauses 15.0.0
--------------------------------
- - Breaking Change: Check.Contains(string, string, System.StringComparison) now exists in Light.GuardClauses.FrameworkExtensions to avoid conflict with other polyfill libraries
+ - new assertions: IsUuidVersion7, MustBeUuidVersion7, IsFinite, MustBeFinite, IsAscii, MustBeAscii, and MustHaveCountIn
+ - new numeric sign guards: MustBePositive, MustBeNegative, MustNotBePositive, MustNotBeNegative, and MustNotBeZero
+ - new dictionary key guards: MustContainKey and MustNotContainKey
+ - new collection content guards: MustNotContainNull and MustNotContainNullOrWhiteSpace
+ - new string inspection assertions: ContainsOnlyDigits, MustContainOnlyDigits, ContainsOnlyLettersOrDigits, MustContainOnlyLettersOrDigits, IsUpperCase, MustBeUpperCase, IsLowerCase, MustBeLowerCase, IsBase64, MustBeBase64, IsHexadecimal, MustBeHexadecimal, and MustNotContainWhiteSpace
+ - new stream capability guards: MustBeReadable, MustBeWritable, and MustBeSeekable
+ - new collection count guard: MustHaveSameCountAs
+ - expanded span and memory support for MustNotBeEmpty, MustHaveLength, MustHaveLengthIn, and MustNotBeEmptyOrWhiteSpace, plus DateTimeOffset support for MustBeUtc
+ - breaking: the modern package target was updated from .NET 8 to .NET 10; .NET Standard 2.0 and 2.1 remain supported
diff --git a/src/Light.GuardClauses/Light.GuardClauses.csproj b/src/Light.GuardClauses/Light.GuardClauses.csproj
index d519dc64..14e427e8 100644
--- a/src/Light.GuardClauses/Light.GuardClauses.csproj
+++ b/src/Light.GuardClauses/Light.GuardClauses.csproj
@@ -16,11 +16,13 @@
+ ReferenceOutputAssembly="false"
+ GlobalPropertiesToRemove="SignAssembly;AssemblyOriginatorKeyFile" />
+ ReferenceOutputAssembly="false"
+ GlobalPropertiesToRemove="SignAssembly;AssemblyOriginatorKeyFile" />
diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs
index 1b9bb2c1..685627c9 100644
--- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs
+++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs
@@ -80,6 +80,39 @@ public static void NetStandardExportIncludesPolyfillAttributes()
sourceCode.Should().Contain("class CallerArgumentExpressionAttribute");
}
+ [Fact]
+ public static void ExportUsesLfLineEndings()
+ {
+ using var temporaryDirectory = new TemporaryDirectory();
+ var sourceFolder = Path.Combine(temporaryDirectory.DirectoryPath, "Source");
+ Directory.CreateDirectory(sourceFolder);
+ File.WriteAllText(
+ Path.Combine(sourceFolder, "Check.cs"),
+ "namespace Light.GuardClauses; public static partial class Check { }"
+ );
+ File.WriteAllText(
+ Path.Combine(sourceFolder, "Throw.cs"),
+ "namespace Light.GuardClauses.ExceptionFactory; public static partial class Throw { }"
+ );
+ File.WriteAllText(
+ Path.Combine(sourceFolder, "CrLfComment.cs"),
+ "namespace Light.GuardClauses;\r\n/* First line\r\nSecond line */\r\npublic static class CrLfComment { }"
+ );
+ var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "LfLineEndings.cs");
+ var options = CreateOptions(targetFile, SourceTargetFramework.NetStandard2_0) with
+ {
+ SourceFolder = sourceFolder,
+ };
+
+ SourceFileMerger.CreateSingleSourceFile(options);
+ var sourceCode = File.ReadAllText(targetFile);
+
+ sourceCode.Should().Contain("First line\nSecond line");
+ sourceCode.Should().Contain("\n");
+ sourceCode.Should().NotContain("\r");
+ sourceCode.Should().EndWith("\n");
+ }
+
[Fact]
public static void Net10ExportValidatesAgainstNet10()
{
diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/CleanupStep.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/CleanupStep.cs
index 91bdd338..ea6834d1 100644
--- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/CleanupStep.cs
+++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/CleanupStep.cs
@@ -110,11 +110,11 @@ public static bool TryGetNextLine(this ReadOnlySpan text, int startIndex,
}
nextLine = text.Slice(startIndex);
- var newLineIndex = nextLine.IndexOf(Environment.NewLine);
+ var newLineIndex = nextLine.IndexOf('\n');
if (newLineIndex == -1)
return true;
- nextLine = nextLine.Slice(0, newLineIndex + Environment.NewLine.Length);
+ nextLine = nextLine.Slice(0, newLineIndex + 1);
return true;
}
-}
\ No newline at end of file
+}
diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs
index 61a771e8..55ea5f3c 100644
--- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs
+++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs
@@ -40,7 +40,7 @@ public static void CreateSingleSourceFile(SourceFileMergeOptions options)
$@"License information for Light.GuardClauses
The MIT License (MIT)
-Copyright (c) 2016, 2025 Kenny Pflug mailto:kenny.pflug@live.de
+Copyright (c) 2016, 2026 Kenny Pflug mailto:kenny.pflug@live.de
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the ""Software""), to deal
@@ -544,7 +544,7 @@ public CallerArgumentExpressionAttribute(string parameterName)
targetRoot = targetRoot.ReplaceNodes(replacedNodes.Keys, (originalNode, _) => replacedNodes[originalNode]);
targetRoot = RemoveConditionalCompilationTrivia(targetRoot);
- targetRoot = targetRoot.NormalizeWhitespace();
+ targetRoot = targetRoot.NormalizeWhitespace(eol: "\n");
// Make types internal if necessary
if (options.ChangePublicTypesToInternalTypes)
@@ -660,6 +660,11 @@ node is ClassDeclarationSyntax
Console.WriteLine("File is cleaned up...");
targetFileContent = CleanupStep.Cleanup(targetFileContent, options).ToString();
+ targetFileContent = targetFileContent.ReplaceLineEndings("\n");
+ if (!targetFileContent.EndsWith('\n'))
+ {
+ targetFileContent += '\n';
+ }
// Write the target file
Console.WriteLine("File is written to disk...");