Simplifying Parameter Validation with CallerArgumentExpression
When developing packages, to ensure that constructor or method parameters meet expectations, we typically perform parameter validation for null or empty strings. To simplify operations and standardize error messages, I usually write a static ExceptionUtils class to perform these checks, as shown in the example below:
public static class ExceptionUtils {
public static void ThrowIfNull<T>(Expression<Func<T?>> expression) {
_ = expression.Compile().Invoke()
?? throw new ArgumentNullException(GetMemberName(expression));
}
public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression) {
string? value = expression.Compile().Invoke();
if (string.IsNullOrWhiteSpace(value)) {
throw new ArgumentException("Must not be null or whitespace.", GetMemberName(expression));
}
}
private static string GetMemberName<T>(Expression<Func<T>> expression) {
if (expression.Body is not MemberExpression expressionBody) {
throw new ArgumentException("Expression error.", nameof(expression));
}
return expressionBody.Member.Name;
}
}This allows for parameter checking in the following way. Using Expression is intended to avoid passing both the parameter value and the parameter name, thereby simplifying usage:
public void Method(string str) {
ExceptionUtils.ThrowIfNullOrWhiteSpace(() => str);
}However, .NET 6 introduced the Nullable reference type checking mechanism. Typically, after we perform a null check, the compiler can recognize that the variable will not be null:
string ToLower(string? str) {
if (str is null) {
throw new ArgumentNullException(nameof(str));
}
// Since null has been checked, the compiler will no longer issue a null warning for str
return str.ToLower();
}But because my ExceptionUtils uses Expression rather than checking the parameter directly, I cannot add [NotNull] to the parameter to let the compiler know that the checked parameter is not null. Therefore, I adjusted the code as follows:
public static class ExceptionUtils {
public static void ThrowIfNull<T>(Expression<Func<T>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
_ = expression.Compile().Invoke()
?? throw new ArgumentNullException(GetMemberName(expression));
}
public static void ThrowIfNullOrWhiteSpace(Expression<Func<string?>> expression, [DoesNotReturnIf(true)] bool isNull = true) {
string? value = expression.Compile().Invoke();
if (string.IsNullOrWhiteSpace(value)) {
throw new ArgumentException("Must not be null or whitespace.", GetMemberName(expression));
}
}
private static string GetMemberName<T>(Expression<Func<T>> expression) {
return expression.Body is not MemberExpression expressionBody
? throw new ArgumentException("Expression error.", nameof(expression))
: expressionBody.Member.Name;
}
}The isNull parameter is included only because using [DoesNotReturn] directly triggers the warning: "Methods marked with [DoesNotReturn] should not return." I had to use [DoesNotReturnIf(true)] combined with the meaningless isNull parameter to handle this. Of course, I have never been very satisfied with the above solution. After .NET 6 and .NET 7, the official team provided some simplified static check methods:
// Added in .NET 6
ArgumentNullException.ThrowIfNull(object? argument, string? paramName = null);
// Added in .NET 7
ArgumentNullException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentNullException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);
// Added in .NET 7
ArgumentException.ThrowIfNullOrEmpty(string? argument, string? paramName = null);
ArgumentException.ThrowIfNullOrWhiteSpace(string? argument, string? paramName = null);I recently looked at the source code for ThrowIfNull, and the [CallerArgumentExpression] within it reminded me of a book I borrowed from a junior colleague, which mentioned that it seems to be used to automatically retrieve parameter names.
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
if (argument is null) {
Throw(paramName);
}
}Therefore, I wrote the following program to test it:
string? str = "";
Console.Write("When paramName is not passed, ");
TestCallerArgumentExpression(str);
Console.Write("When paramName is passed, ");
TestCallerArgumentExpression(str, "str2");
void TestCallerArgumentExpression(object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) {
Console.WriteLine("paramName:" + paramName);
}The results are as follows:
When paramName is not passed, paramName: str
When paramName is passed, paramName: str2When paramName is not passed, it automatically uses the variable name of the argument passed to argument as the value for paramName. This approach is more concise than my original Expression solution and also allows the use of [NotNull] to support Nullable reference checks.
Change Log
- 2024-10-13 Initial document creation.
