From 69dea93326f848b53098ca6323d83a25870788e8 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 06:33:39 +0200 Subject: [PATCH 1/9] build: updated version to 15.0.0 Signed-off-by: Kenny Pflug --- Directory.Build.props | 2 +- src/Directory.Build.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c580c19..b5af4e3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 14.0.0 + 15.0.0 14 Kenny Pflug Kenny Pflug diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5ff4371..b6f5222 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -17,7 +17,7 @@ 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 From 67c957d6c164aca25d4b772574cf74fe1e112c1f Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 07:03:52 +0200 Subject: [PATCH 2/9] docs: prepare README.md for v15 release Signed-off-by: Kenny Pflug --- README.md | 52 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d542af6..895448d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,16 @@ [![Documentation](https://img.shields.io/badge/Docs-Repository-yellowgreen.svg?style=for-the-badge)](docs/README.md) [![Changelog](https://img.shields.io/badge/Docs-Changelog-yellowgreen.svg?style=for-the-badge)](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` produce 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! + ![Light Libraries logo](Images/light_logo.png) From d6d2e10ba9b6a8b46b06246e7fa0df790b0d53b8 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 07:13:50 +0200 Subject: [PATCH 3/9] chore: attach .github folder to Rider Solution view Signed-off-by: Kenny Pflug --- .idea/.idea.Light.GuardClauses/.idea/indexLayout.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml b/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml index 95beddb..1619d77 100644 --- a/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml +++ b/.idea/.idea.Light.GuardClauses/.idea/indexLayout.xml @@ -2,6 +2,7 @@ + .github ai-plans From 8fbba505b14d459484ecd8725f720f389911a1dc Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 07:15:18 +0200 Subject: [PATCH 4/9] chore: update Copyright current year Signed-off-by: Kenny Pflug --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index b5af4e3..7567f66 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ 14 Kenny Pflug Kenny Pflug - Copyright © Kenny Pflug 2016, 2025 + Copyright © Kenny Pflug 2016, 2026 false From 3d328112d7306ff94b26427e03f5bc4021b81e28 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 07:22:38 +0200 Subject: [PATCH 5/9] docs: add package release notes for v15 release Signed-off-by: Kenny Pflug --- src/Directory.Build.props | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b6f5222..2210502 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,7 +20,15 @@ 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 From 6df261f05a1295a9130231e2307c35fcd92e3563 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 07:23:12 +0200 Subject: [PATCH 6/9] chore: SourceFileMerger now produces LF instead of CLRF, updated copyright Signed-off-by: Kenny Pflug --- Light.GuardClauses.SingleFile.cs | 4 ++-- .../SourceFileMergerFrameworkTests.cs | 13 +++++++++++++ .../CleanupStep.cs | 6 +++--- .../SourceFileMerger.cs | 4 ++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Light.GuardClauses.SingleFile.cs b/Light.GuardClauses.SingleFile.cs index d9f0761..ad38e25 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 diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs index 1b9bb2c..d414e4d 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs @@ -80,6 +80,19 @@ public static void NetStandardExportIncludesPolyfillAttributes() sourceCode.Should().Contain("class CallerArgumentExpressionAttribute"); } + [Fact] + public static void ExportUsesLfLineEndings() + { + using var temporaryDirectory = new TemporaryDirectory(); + var targetFile = Path.Combine(temporaryDirectory.DirectoryPath, "LfLineEndings.cs"); + + SourceFileMerger.CreateSingleSourceFile(CreateOptions(targetFile, SourceTargetFramework.NetStandard2_0)); + var sourceCode = File.ReadAllText(targetFile); + + sourceCode.Should().Contain("\n"); + sourceCode.Should().NotContain("\r"); + } + [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 91bdd33..ea6834d 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 61a771e..beeccde 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) From 16f415261d57591a527c05d4a9605b2807c6cf30 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 07:39:38 +0200 Subject: [PATCH 7/9] ci: Roslyn analyzer projects are no longer signed Signed-off-by: Kenny Pflug --- .github/workflows/release-on-nuget.yml | 7 ++++--- src/Light.GuardClauses/Light.GuardClauses.csproj | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-on-nuget.yml b/.github/workflows/release-on-nuget.yml index f159928..ccd31c7 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/src/Light.GuardClauses/Light.GuardClauses.csproj b/src/Light.GuardClauses/Light.GuardClauses.csproj index d519dc6..14e427e 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" /> From 538fe265bc1059340b478b882818ad1bc6b1d642 Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 07:58:38 +0200 Subject: [PATCH 8/9] chore: fix typo in README.md Signed-off-by: Kenny Pflug --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 895448d..80692fb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - 🧰 **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` produce clear exceptions without repetitive `nameof` calls. +- 🏷️ **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. From fede23556de0d0ac5bb9672a4e71a230a6d99e9a Mon Sep 17 00:00:00 2001 From: Kenny Pflug Date: Wed, 15 Jul 2026 08:09:21 +0200 Subject: [PATCH 9/9] chore: SourceCodeTransformation now always emits LF Signed-off-by: Kenny Pflug --- Light.GuardClauses.SingleFile.cs | 2 +- .../SourceFileMergerFrameworkTests.cs | 22 ++++++++++++++++++- .../SourceFileMerger.cs | 5 +++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Light.GuardClauses.SingleFile.cs b/Light.GuardClauses.SingleFile.cs index ad38e25..05890bf 100644 --- a/Light.GuardClauses.SingleFile.cs +++ b/Light.GuardClauses.SingleFile.cs @@ -13554,4 +13554,4 @@ public CallerArgumentExpressionAttribute(string parameterName) public string ParameterName { get; } } -} \ No newline at end of file +} diff --git a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs index d414e4d..685627c 100644 --- a/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs +++ b/tests/Light.GuardClauses.SourceCodeTransformation.Tests/SourceFileMergerFrameworkTests.cs @@ -84,13 +84,33 @@ public static void NetStandardExportIncludesPolyfillAttributes() 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(CreateOptions(targetFile, SourceTargetFramework.NetStandard2_0)); + 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] diff --git a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs index beeccde..55ea5f3 100644 --- a/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs +++ b/tools/source-export/Light.GuardClauses.SourceCodeTransformation/SourceFileMerger.cs @@ -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...");