Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/release-on-nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions .idea/.idea.Light.GuardClauses/.idea/indexLayout.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project>
<PropertyGroup>
<Version>14.0.0</Version>
<Version>15.0.0</Version>
<LangVersion>14</LangVersion>
<Authors>Kenny Pflug</Authors>
<Company>Kenny Pflug</Company>
<Copyright>Copyright © Kenny Pflug 2016, 2025</Copyright>
<Copyright>Copyright © Kenny Pflug 2016, 2026</Copyright>
<IsPackable>false</IsPackable>
</PropertyGroup>
</Project>
6 changes: 3 additions & 3 deletions Light.GuardClauses.SingleFile.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -13554,4 +13554,4 @@ public CallerArgumentExpressionAttribute(string parameterName)

public string ParameterName { get; }
}
}
}
52 changes: 35 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@
[![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
public class Foo
{
private readonly IBar _bar;

public Foo(IBar? bar)
{
_bar = bar.MustNotBeNull();
}
public Foo(IBar? bar) => _bar = bar.MustNotBeNull();
}
```

Expand All @@ -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/).
Expand All @@ -61,12 +77,14 @@ Light.GuardClauses is available from [NuGet](https://www.nuget.org/packages/Ligh
- Package Manager Console: `Install-Package Light.GuardClauses`
- Project file: `<PackageReference Include="Light.GuardClauses" />`

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

The library supports nullable reference types, .NET code-analysis attributes, JetBrains contract annotations, and Native AOT. Its functional behavior is covered by the test suite, and performance-sensitive assertions can be measured with the current [benchmark project](benchmarks/Light.GuardClauses.Performance/).

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)
12 changes: 10 additions & 2 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,18 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>
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
</PackageReleaseNotes>
</PropertyGroup>

Expand Down
6 changes: 4 additions & 2 deletions src/Light.GuardClauses/Light.GuardClauses.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
<ProjectReference
Include="../../tools/analyzers/Light.GuardClauses.InternalRoslynAnalyzers/Light.GuardClauses.InternalRoslynAnalyzers.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
ReferenceOutputAssembly="false"
GlobalPropertiesToRemove="SignAssembly;AssemblyOriginatorKeyFile" />
<ProjectReference
Include="../../tools/analyzers/Light.GuardClauses.InternalRoslynAnalyzers.CodeFixes/Light.GuardClauses.InternalRoslynAnalyzers.CodeFixes.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
ReferenceOutputAssembly="false"
GlobalPropertiesToRemove="SignAssembly;AssemblyOriginatorKeyFile" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,11 @@ public static bool TryGetNextLine(this ReadOnlySpan<char> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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");
Comment thread
feO2x marked this conversation as resolved.

// Make types internal if necessary
if (options.ChangePublicTypesToInternalTypes)
Expand Down Expand Up @@ -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...");
Expand Down
Loading