CallerArgumentExpression and extension methods don’t mix

How to effectively use [CallerArgumentExpression] to super-power your C# codebase

This post is for the C# developers out there and takes a look at the interesting conjunction of [CallerArgumentExpression] and static extension methods – a mix that at first seems too convenient to pass up.

A quick recap: [CallerArgumentExpression] landed as part of the C# 10.0 language update and helps to reduce the (often brittle!) boilerplate involved in, among other uses, creating useful error messages capturing the names of variables or the text of expressions. You tag an optional string method parameter with [CallerArgumentExpression("argName")] where argName is the name of the method argument you want stringified, and the compiler does the rest.

Here’s a quick demo of how [CallerArgumentExpression] works:

using System;
using System.Runtime.CompilerServices;

public class Program
{
    static string Stringify(object obj,
        [CallerArgumentExpression("obj")] string expr = "")
    {
        return expr;
    }

    public static class Foo
    {
        public string Bar = "bar";
    }

    public static void Main()
    {
        var expr = Stringify(Foo.Bar);
        Console.WriteLine(expr); // prints "Foo.Bar"
        expr = Stringify(Foo.Bar + Foo.Bar);
        Console.WriteLine(expr); // prints "Foo.Bar + Foo.Bar"
    }
}

And you can try it online yourself in this .NET Fiddle.

It’s really cool and it opens the door to a lot of possibilities (though I’m still stuck trying to figure some of them out, such as reliably setting/clearing model binding errors that involve array expressions).

As mentioned, this shipped with C# 10. And of course, C# 8 shipped “the big one:” nullable reference types. Since then, the following pattern has become familiar in many a codebase while devs figure out where variables actually can or can’t be null:

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

static class Extensions
{
    public static T ThrowIfNull<T>([NotNull] T? value, string expr)
    {
        if (value is null) {
            throw new ArgumentNullException(expr);
        }
        return value;
    }
}

This does exactly what you think it does: it verifies that a value isn’t null or throws an exception if it is. And it lets the compiler know that downstream of this call, the passed-in value is non-null. To make it useful, it’s common enough to extend it with more caller attribute magic:

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

static class Extensions
{
    public static T ThrowIfNull<T>(
        [NotNull] T? value,
        string expr,
        [CallerMemberName] string callerName = "",
        [CallerFilePath] string filePath = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        if (value is null) {
            throw new InvalidOperationException(
                $"{expr} unexpectedly null in {callerName} "
                + $"at {filePath}:{lineNumber}");
        }
        return value;
    }
}

Now we get useful exceptions that we’ll hopefully log and revisit to help us find any places in our codebase where we are assuming a value can’t be null but it turns out that, in fact, it can be.

But what about if we try to add our new best buddy [CallerArgumentExpression] here, to get rid of the need to manually specify the text of the argument via argName in our ThrowIfNull()?

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

static class Extensions
{
    public static T ThrowIfNull<T>(
        [NotNull] T? value,
        [CallerArgumentExpression("value")] string expr = "",
        [CallerMemberName] string callerName = "",
        [CallerFilePath] string filePath = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        if (value is null) {
            throw new InvalidOperationException(
                $"{expr} unexpectedly null in {callerName} "
                + $" at {filePath}:{lineNumber}");
        }
        return value;
    }
}

At first blush, this works great. Use it with a single variable directly, as in foo.ThrowIfNull(), and everything will work swimmingly and it’ll do exactly what it says on the tin. But try using it in a more-complicated setting, say like foo?.bar?.ThrowIfNull(), and you’ll see what I mean: here, argName will only capture the last token in the chain and you’ll see that argName is only bar and not foo.bar!

It’s actually not particularly surprising behavior. Even without knowing what Roslyn desugars the above code to, you could logically think of it as being an expression (conditionally) invoked on/with the final variable bar itself – after all, T here would have been bar.GetType(), so it’s not a huge stretch of the imagination to guess that expr might only span bar as well.1

Indeed, when you look at what the code compiles to, you’ll see why. For the following code fragment:

public class Foo {
    public string? Bar;
}

public class C {
    public void M(Foo? foo) {
        foo?.Bar.ThrowIfNull();
    }
}

We get

public class Foo
{
    [System.Runtime.CompilerServices.Nullable(2)]
    public string Bar;
}

public class C
{
    [System.Runtime.CompilerServices.NullableContext(2)]
    public void M(Foo foo)
    {
        if (foo != null)
        {
            Extensions.ThrowIfNull(foo.Bar, ".Bar");
        }
    }
}

Which, while still helpful, is not exactly what we want. Although as C# developers we are somewhat allergic to calling static helper utilities directly instead of cleverly turning them into their more ergonomic extension method counterparts, in this we don’t have any other choice.

When we change ThrowIfNull() from an extension method to a regular static method though, we get the result we really wanted:

public static class Utils
{
    public static T ThrowIfNull<T>(
        [NotNull] T? value,
        [CallerArgumentExpression("value")] string? expr = null) 
    {
        if (value is null) {
            throw new ArgumentNullException(expr);
        }
        return value;
    }
}

public class Foo
{
    public string? Bar;
}

public class C
{
    public void M(Foo? foo)
    {
        Utils.ThrowIfNull(foo?.Bar);
    }
}

Desugaring to:

public class C
{
    [System.Runtime.CompilerServices.NullableContext(2)]
    public void M(Foo foo)
    {
        Utils.ThrowIfNull((foo != null) ? foo.Bar : null, "foo?.Bar");
    }
}

Liked this post? Follow me on twitter @mqudsi and like this tweet for more .NET awesomeness!

If you would like to receive a notification the next time we release a nuget package for .NET or release resources for .NET Core and ASP.NET Core, you can subscribe below. Note that you'll only get notifications relevant to .NET programming and development by NeoSmart Technologies. If you want to receive email updates for all NeoSmart Technologies posts and releases, please sign up in the sidebar to the right instead.


  1. Except expr is actually not bar but rather .bar

  • Similar Posts

    Craving more? Here are some posts a vector similarity search turns up as being relevant or similar from our catalog you might also enjoy.
    1. Modern C++ isn't memory safe, either
    2. System.Threading.Thread, Universal Windows Platform, and the fragmentation of .NET Standard
    3. My Thoughts on Sutter's "C++ and Beyond 2011"
  • Leave a Reply

    Your email address will not be published. Required fields are marked *