Skip to content

How to Contribute and Build

Kenny Pflug edited this page Oct 29, 2024 · 13 revisions

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:

  1. 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.
  2. 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.
  3. 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).
  4. 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.

Building the source code

Visual Studio and MSBuild

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.

Overview of the different projects

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 and message 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.

Changing the TargetFrameworks for your local development machine

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.

An example of a typical assertion

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 optional parameterName and message and one that takes an exceptionFactory to throw a custom exception.
  • The parameterName argument should be decorated with the CallerArgumentExpression attribute. The C# 10 compiler (or newer) will then set the expression that was used to provide the parameter 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 and message to the resulting exception when the user specified it.
    • Always throw the custom exception, no matter which check fails inside an assertion.
  • When designing a new assertion, check if it would be useful to introduce a new exception class to the Light.GuardClauses.Exceptions namespace (like the EmptyStringException in the example above). The name of these exceptions should immediately point the end-user to the problem. Also, these exceptions usually derive from ArgumentException (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 and ValidatedNotNullAttribute 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.
Clone this wiki locally