C# 10 new feature CallerArgumentExpression, argument check and more

The CallerArgumentExpression has been discussed for years, it was supposed to a part of C# 8.0 but got delayed. Finally this month it is delivered along with C# 10 and .NET 6.

CallerArgumentExpressionAttribute and argument compilation

In C# 10, [CallerArgumentExpression(parameterName)] can be used to direct the compiler to capture the specified argument’s expression as text. For example:

using System.Runtime.CompilerServices;

void Function(int a, TimeSpan b, [CallerArgumentExpression("a")] string c = "", [CallerArgumentExpression("b")] string d = "")
{
    Console.WriteLine($"Called with value {a} from expression '{c}'");
    Console.WriteLine($"Called with value {b} from expression '{d}'");
}

When calling above function, The magic happens at compile time:

Function(1, default);
// Compiled to: 
Function(1, default, "1", "default");

int x = 1;
TimeSpan y = TimeSpan.Zero;
Function(x, y);
// Compiled to:
Function(x, y, "x", "y");

Function(int.Parse("2") + 1 + Math.Max(2, 3), TimeSpan.Zero - TimeSpan.MaxValue);
// Compiled to:
Function(int.Parse("2") + 1 + Math.Max(2, 3), TimeSpan.Zero - TimeSpan.MaxValue, "int.Parse(\"2\") + 1 + Math.Max(2, 3)", "TimeSpan.Zero - TimeSpan.MaxValue");

Function’s parameter c is decorated with [CallerArgumentExpression("a")]. So when calling Function, C# compiler will pickup whatever expression passed to a, and use that expression’s text for c. Similarly, whatever expression is used for b, that expression’s text is used for d.

Argument check

The most useful scenario of this feature is argument check. In the past, a lot of argument check utility methods are created like this:

public static partial class Argument
{
    public static void NotNull<T>([NotNull] T? value, string name) where T : class
    {
        if (value is null)
        {
            throw new ArgumentNullException(name);
        }
    }

    public static void NotNullOrWhiteSpace([NotNull] string? value, string name)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeEmpty, name));
        }
    }

    public static void NotNegative(int value, string name)
    {
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException(name, value, string.Format(CultureInfo.CurrentCulture, Resources.ArgumentCannotBeNegative, name));
        }
    }
}

So they can be used as:

public partial record Person
{
    public Person(string name, int age, Uri link)
    {
        Argument.NotNullOrWhiteSpace(name, nameof(name));
        Argument.NotNegative(age, nameof(age));
        Argument.NotNull(link, nameof(link));

        this.Name = name;
        this.Age = age;
        this.Link = link.ToString();
    }

    public string Name { get; }
    public int Age { get; }
    public string Link { get; }
}

The problem is, it is very annoying to pass argument name every time. There are some ways to get rid of manually passing argument name, but these approaches introduces other issues. For example, a lambda expression with closure can be used:

public partial record Person
{
    public Person(Uri link)
    {
        Argument.NotNull(() => link);

        this.Link = link.ToString();
    }
}

And this version of NotNull can take a function:

public static partial class Argument
{
    public static void NotNull<T>(Func<T> value)
    {
        if (value() is null)
        {
            throw new ArgumentNullException(GetName(value));
        }
    }

    private static string GetName<T>(Func<T> func)
    {
        // func: () => arg is compiled to DisplayClass with a field and a method. That method is func.
        object displayClassInstance = func.Target!;
        FieldInfo closure = displayClassInstance.GetType()
            .GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
            .Single();
        return closure.Name;
    }
}

See my post on what is closure and how C# compiles closure.

Lambda expression can be also compiled to expression tree. So NotNull can be implemented to take an expression too (See my post on what is expression tree and how C# compiles expression tree):

public static partial class Argument
{
    public static void NotNull<T>(Expression<Func<T>> value)
    {
        if (value.Compile().Invoke() is null)
        {
            throw new ArgumentNullException(GetName(value));
        }
    }

    private static string GetName<T>(Expression<Func<T>> expression)
    {
        // expression: () => arg is compiled to DisplayClass with a field. Here expression body is to access DisplayClass instance's field.
        MemberExpression displayClassInstance = (MemberExpression)expression.Body;
        MemberInfo closure = displayClassInstance.Member;
        return closure.Name;
    }
}

These approaches introduce the lambda syntax and performance overhead at runtime. And they are extremely fragile too. Now C# 10’s CallerArgumentExpression finally provides a cleaner solution:

public static partial class Argument
{
    public static T NotNull<T>([NotNull] this T? value, [CallerArgumentExpression("value")] string name = "")
        where T : class =>
        value is null ? throw new ArgumentNullException(name) : value;

    public static string NotNullOrWhiteSpace([NotNull] this string? value, [CallerArgumentExpression("value")] string name = "") =>
        string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeEmpty, name), name)
            : value;

    public static int NotNegative(this int value, [CallerArgumentExpression("value")] string name = "") =>
        value < 0
            ? throw new ArgumentOutOfRangeException(name, value, string.Format(CultureInfo.CurrentCulture, Resources.ArgumentCannotBeNegative, name))
            : value;
}

Now the argument check can be shorter and fluent:

public record Person
{
    public Person(string name, int age, Uri link) => 
        (this.Name, this.Age, this.Link) = (name.NotNullOrWhiteSpace(), age.NotNegative(), link.NotNull().ToString());
        // Compiled to:
        // this.Name = Argument.NotNullOrWhiteSpace(name, "name");
        // this.Age = Argument.NotNegative(age, "age");
        // this.Link = Argument.NotNull(link, "link").ToString();

    public string Name { get; }
    public int Age { get; }
    public string Link { get; }
}

The argument name is generated at compile time and there is no performance overhead at runtime at all.

Assertion and logging

The other useful scenarios could be assertion and logging:

[Conditional("DEBUG")]
static void Assert(bool condition, [CallerArgumentExpression("condition")] string expression = "")
{
    if (!condition)
    {
        Environment.FailFast($"'{expression}' is false and should be true.");
    }
}

Assert(y > TimeSpan.Zero);
// Compiled to:
Assert(y > TimeSpan.Zero, "y > TimeSpan.Zero");

[Conditional("DEBUG")]
static void Log<T>(T value, [CallerArgumentExpression("value")] string expression = "")
{
    Trace.WriteLine($"'{expression}' has value '{value}'");
}

Log(Math.Min(Environment.ProcessorCount, x));
// Compiled to:
Log(Math.Min(Environment.ProcessorCount, x), "Math.Min(Environment.ProcessorCount, x)");

Use for older projects

If .NET 6.0 SDK is installed, C# 10 is available, where CallerArgumentExpression can be used targeting to .NET 5 and .NET 6. For older project targeting older .NET or .NET Standard, CallerArgumentExpressionAttribute is not available. Fortunately C# 10 and this feature can still be used with them, as long as .NET 6.0 SDK is installed. Just manually add the CallerArgumentExpressionAttribute class to your project and use it like the built-in attribute:

#if !NET5_0 && !NET6_0
namespace System.Runtime.CompilerServices;

/// <summary>
/// Allows capturing of the expressions passed to a method.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class CallerArgumentExpressionAttribute : Attribute
{
    /// <summary>
    /// Initializes a new instance of the <see cref="T:System.Runtime.CompilerServices.CallerArgumentExpressionAttribute" /> class.
    /// </summary>
    /// <param name="parameterName">The name of the targeted parameter.</param>
    public CallerArgumentExpressionAttribute(string parameterName) => this.ParameterName = parameterName;

    /// <summary>
    /// Gets the target parameter name of the <c>CallerArgumentExpression</c>.
    /// </summary>
    /// <returns>
    /// The name of the targeted parameter of the <c>CallerArgumentExpression</c>.
    /// </returns>
    public string ParameterName { get; }
}
#endif

It should be internal so that when this assembly is referenced by another assembly, there won’t conflict with the built-in version of [CallerArgumentExpression]. Then C# 10‘s compiler will pick it up and the above magic will happen.

13 Comments

  • Nah, I’ll pass. That code looks horrible, the intent of syntax sugar is usually to clean up code.

  • Typical of many C# features released in recent years - horrible, unreadable code, all for the sake of avoiding typing a couple more lines of code.

  • features are pretty amazing, loved to read

  • public Person(string name, int age, Uri link) => please explain this query

  • It is awesome to assign a value to a variable simultaneously from both left and right, and like in Highlander, only one remains.

    [CallerArgumentExpression("a")] string c = ""

    Looks like the C# team is envy of C, where you can not be sure to distinguish a valid source code from something get out of a CRC-failed zip file with a single look.

  • @Ad Trens

    That is just a syntactic sugar.

    public Point(int x, int y, int z)
    {
    this.X = x;
    this.Y = y;
    this.Z = z;
    }

    can be written as:

    public Point(int x, int y, int z) =>
    (this.X, this.Y, this.Z) = (x, y, z)

    The compilation result are the same.

  • I really start to hate C#. It's become such a bloated language that it's become absolutely gross to use. What is the purpose of this? Making everything unreadably terse is an awful goal to have as a language designer.

    This is just awful:

    public Person(string name, int age, Uri link) =>
    (this.Name, this.Age, this.Link) = (name.NotNullOrWhiteSpace(), age.NotNegative(), link.NotNull().ToString());

    So much magic that a developer has to first learn before they can read this code and make up their mind if this is actually doing what you'd want it to do or not. Absolutely gross.

    Just keep it simple. A simple if (x == null) throw ArgumentNullException(...); is really not that bad that it needed "improvement".

    C# is becoming an awful language. As a very senior developer at my company I increasingly hate to use C# for new projects because new developers have such a hard time to become good at it. When we realised that .NET and C# keep changing fundamentally around the time netstandard2.0 was discontinued we started to use nodejs and Go for all our new projects and most of our teams have seen a boost in productivity. Productivity doesn't come from terse code, it comes from easy to read code which someone can understand fast and make quick changes to it without having to constantly google the meaning of obscure features every 5 minutes.

  • Great. So `CallLibraryFunction( EncodeString("My vulnerable data") );` suddenly becomes broken?

  • > Great. So `CallLibraryFunction( EncodeString("My vulnerable data") );` suddenly becomes broken?
    This is broken from the beginning since the "My vulnerable data" is written as-is in the output binary.

  • Your code should probably have internal class CallerArgumentExpressionAttribute so it doesn't interfere with other projects referencing it that are .NET 6+.

  • @HA
    Thank you. Updated the code.

  • I agree with many of the posts that trying to jam more meaning into fewer characters makes for less readable code, which makes it harder to maintain the code moving forward. Net loss imho.

  • Thank you for the code sir. Keep it up

Add a Comment

As it will appear on the website

Not displayed

Your website