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 @@ [![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` 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! + ![Light Libraries logo](Images/light_logo.png) 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...");