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.ThrowIfNullin .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:
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:
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:
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:
paramName: str
paramName: str2When 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
- Prioritize built-in methods: .NET 6 and .NET 7 have built-in methods such as
ArgumentNullException.ThrowIfNullandArgumentException.ThrowIfNullOrEmpty. These methods have already implemented[CallerArgumentExpression]internally and should be used first. - Performance advantages: Using
[CallerArgumentExpression]instead ofExpression<Func<T>>avoids runtimeCompile()andInvoke()operations, significantly improving performance. - 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.