Skip to content

Simplifying Parameter Validation with CallerArgumentExpression

TLDR

  • Use the [CallerArgumentExpression] attribute to automatically capture the variable name of an argument passed to a method, eliminating the need to pass string names manually.
  • Compared to using Expression<Func<T>>, this approach offers better performance and cleaner code.
  • When combined with attributes like [NotNull] or [DoesNotReturn], it effectively supports .NET's Nullable reference type static analysis, resolving false-positive compiler warnings.
  • It is recommended to prioritize built-in methods like ArgumentNullException.ThrowIfNull in .NET 6/7. If custom validation logic is required, this pattern can be used as a reference.

The Problem with Using Expressions for Parameter Validation

When developing libraries, developers often create an ExceptionUtils utility class to simplify parameter validation and standardize error messages. A traditional approach involves using Expression to capture the variable name, avoiding the need to repeatedly type parameter name strings:

csharp
public static class ExceptionUtils {
    public static void ThrowIfNull<T>(Expression<Func<T?>> expression) {
        _ = expression.Compile().Invoke()
            ?? throw new ArgumentNullException(GetMemberName(expression));
    }

    private static string GetMemberName<T>(Expression<Func<T>> expression) {
        if (expression.Body is not MemberExpression expressionBody) {
            throw new ArgumentException("Invalid expression.", nameof(expression));
        }
        return expressionBody.Member.Name;
    }
}

When this issue occurs: When Nullable reference type checking is enabled in a project, the Expression-based approach mentioned above fails to inform the compiler that the checked variable is not null, causing the compiler to continue issuing warnings. Additionally, using Expression requires compiling the expression, which introduces extra performance overhead.

The Solution: Introducing CallerArgumentExpression

Starting with .NET 6, the official [CallerArgumentExpression] attribute was introduced. When a method parameter is marked with this attribute, the compiler automatically passes the "variable name" used by the caller for that argument as a string.

Implementation Example

With this attribute, we can refactor the validation logic to be both concise and correctly supported by compiler checks:

csharp
using System.Runtime.CompilerServices;
using System.Diagnostics.CodeAnalysis;

public static class ExceptionUtils {
    public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
        if (argument is null) {
            throw new ArgumentNullException(paramName);
        }
    }
}

Verification Results

The test code is as follows:

csharp
string? str = "";
TestCallerArgumentExpression(str); // paramName not passed
TestCallerArgumentExpression(str, "str2"); // paramName passed

void TestCallerArgumentExpression(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
    Console.WriteLine("paramName:" + paramName);
}

Execution results:

text
paramName: str
paramName: str2

When to use this: When you need custom validation logic (e.g., checking specific formats or ranges) and want to automatically include the variable name when throwing an exception, while maintaining the compiler's correct assessment of Nullable reference types.

Conclusion and Recommendations

  1. Prioritize built-in methods: .NET 6 and .NET 7 have built-in methods such as ArgumentNullException.ThrowIfNull and ArgumentException.ThrowIfNullOrEmpty. These methods have already implemented [CallerArgumentExpression] internally and should be used first.
  2. Performance advantages: Using [CallerArgumentExpression] instead of Expression<Func<T>> avoids runtime Compile() and Invoke() operations, significantly improving performance.
  3. Compiler support: By combining this with attributes like [NotNull] or [DoesNotReturn], you can allow the compiler to correctly identify code execution paths, reducing unnecessary Nullable warnings.

Changelog

    • Initial documentation created.