-
Notifications
You must be signed in to change notification settings - Fork 8
How to Contribute and Build
You thought of an assertion that you'd like to add to Light.GuardClauses? That's awesome! Here is how you can add your ideas to the project:
- Create an issue: before doing anything else, please create an issue where you describe what you have in mind. This way you and I can discuss and coordinate the implementation of your idea.
- Get the source code: fork and clone the repository. Get accustomed to the way Light.GuardClauses is structured and how the functional and performance tests are written. Please use the same style guide as the rest of the source code.
- Create a branch for each feature: besides the actual implementation, each feature should be tested functionally (in the Light.GuardClauses.Tests project) and performance-wise (in the Light.GuardClauses.Performance project).
- Send me a pull request for each feature: please keep your pull requests as small as possible. Don't put two or more features in the same pull request. I think small pull requests take considerably less time to evaluate.
And that's it - I hope to see a lot of feedback and PRs from you! Let's get into the nitty-gritty details of setting up your machine.
Light.GuardClauses is build for .NET 8, .NET Standard 2.0 and .NET Standard 2.1. Thus you should at least install the corresponding .NET 8 SDK and use an appropriate IDE like Visual Studio, Visual Studio Code, or Rider.
The source code contains two solution (sln) files that hold a different amount of projects: Light.GuardClauses.sln contains
- the production code Light.GuardClauses.csproj.
- the xunit test project Light.GuardClauses.Tests.csproj.
- the Benchmark.NET project Light.GuardClauses.Performance.
This "smaller" solution can be used when implementing and testing new features. Normally, you should not need to use the second solution Light.GuardClauses.AllProjects which contains four additional projects:
- two Roslyn analyzer projects that are used to check XML comments. You can build Light.GuardClauses.InternalRoslynAnalyzers and the Light.GuardClauses project will automatically pick up the analyzer (you might need to restart Visual Studio). The analyzers will tell you when you did not choose the default XML comments for
parameterName
andmessage
parameters. - two projects that are used when merging the Light.GuardClauses source code into a single file. You can build and run Light.GuardClauses.SourceCodeTransformation to merge all code into a single file. Afterwards you can build Light.GuardClauses.Source to ensure that the generated code file can be compiled with .NET Standard 2.0.
Light.GuardClauses is built against .NET 8, .NET Standard 2.0, and .NET Standard 2.1. The test project runs on .NET 8 by default, but you can adjust this by creating a file called TargetFrameworks.props
right next to the Light.GuardClauses.Tests.csproj file and adjust the target frameworks property in there:
<Project>
<PropertyGroup>
<TargetFrameworks>net6.0;net48</TargetFrameworks>
</PropertyGroup>
</Project>
This allows you to run tests on your local dev machine for different frameworks. By default, this file is ignored by git - do not check it in.
You wonder what a typical assertion looks like? Let's take a look at the actual implementation of MustNotBeNullOrEmpty
for strings to give you an idea:
/// <summary>
/// Ensures that the specified string is not null or empty, or otherwise throws an <see cref="ArgumentNullException" /> or <see cref="EmptyStringException" />.
/// </summary>
/// <param name="parameter">The string to be checked.</param>
/// <param name="parameterName">The name of the parameter (optional).</param>
/// <param name="message">The message that will be passed to the resulting exception (optional).</param>
/// <exception cref="EmptyStringException">Thrown when <paramref name="parameter" /> is an empty string.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="parameter" /> is null.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("parameter:null => halt; parameter:notnull => notnull")]
public static string MustNotBeNullOrEmpty([NotNull, ValidatedNotNull] this string? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null, string? message = null)
{
if (parameter == null)
Throw.ArgumentNull(parameterName, message);
if (parameter!.Length == 0)
Throw.EmptyString(parameterName, message);
return parameter;
}
/// <summary>
/// Ensures that the specified string is not null or empty, or otherwise throws your custom exception.
/// </summary>
/// <param name="parameter">The string to be checked.</param>
/// <param name="exceptionFactory">The delegate that creates your custom exception. <paramref name="parameter" /> is passed to this delegate.</param>
/// <exception cref="Exception">Your custom exception thrown when <paramref name="parameter" /> is an empty string or null.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ContractAnnotation("parameter:null => halt; parameter:notnull => notnull; exceptionFactory:null => halt")]
public static string MustNotBeNullOrEmpty([NotNull, ValidatedNotNull] this string? parameter, Func<string?, Exception> exceptionFactory)
{
if (string.IsNullOrEmpty(parameter))
Throw.CustomException(exceptionFactory, parameter);
return parameter!;
}
When you want to add your own assertions, please keep the following things in mind:
- Every assertion that throws exceptions (these begin with
Must...
) usually has two overloads: one that takes an optionalparameterName
andmessage
and one that takes anexceptionFactory
to throw a custom exception. - The
parameterName
argument should be decorated with theCallerArgumentExpression
attribute. The C# 10 compiler (or newer) will then set the expression that was used to provide theparameter
argument as a string automatically. - Do not throw exceptions within the assertion, use the
Throw
class instead. This is done for performance reasons as methods that only contain a throw statement are handled differently by the runtime (they are treated as cold paths by default which increases CPU instruction throughput, reducing the need to load code from different pages). Important: measure (i.e. write a performance benchmark for your new assertion)! - Most of the assertions only check one thing, but the example above actually checks two things: the string being null and the string being empty. When implementing your assertions, keep the following things in mind (as every assertion of Light.GuardClauses works this way):
- Always pass the
parameterName
andmessage
to the resulting exception when the user specified it. - Always throw the custom exception, no matter which check fails inside an assertion.
- Always pass the
- When designing a new assertion, check if it would be useful to introduce a new exception class to the
Light.GuardClauses.Exceptions
namespace (like theEmptyStringException
in the example above). The name of these exceptions should immediately point the end-user to the problem. Also, these exceptions usually derive fromArgumentException
(directly or indirectly) as the main purpose of Light.GuardClauses is to validate parameters. Please ensure that this exception can be serialized. - When your assertion is not containing too many statements, please apply
MethodImplOptions.AggressiveInlining
. This is done for performance reasons as the JIT will usually inline the statements in the calling method which avoids the method call overhead. Again, measure! - Where possible, tell R# how the return value looks like using Contract Annotations. Also, apply the
System.Diagnostics.CodeAnalysis.NotNullAttribute
andValidatedNotNullAttribute
where possible to support .NET analyzers / FxCopAnalyzers. - Provide XML comments for your assertion
- Provide automated test that check every code path through the assertion.
- Provide a benchmark that checks the performance of your new assertion.