{"id":5037,"date":"2023-09-11T12:17:55","date_gmt":"2023-09-11T17:17:55","guid":{"rendered":"https:\/\/neosmart.net\/blog\/?p=5037"},"modified":"2023-09-11T13:05:56","modified_gmt":"2023-09-11T18:05:56","slug":"callerargumentexpression-and-extension-methods-dont-mix","status":"publish","type":"post","link":"https:\/\/neosmart.net\/blog\/callerargumentexpression-and-extension-methods-dont-mix\/","title":{"rendered":"CallerArgumentExpression and extension methods don&#8217;t mix"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" class=\"alignright size-thumbnail wp-image-5053 colorbox-5037\" src=\"https:\/\/neosmart.net\/blog\/wp-content\/uploads\/2023\/09\/c-sharp-150x150.jpg\" alt=\"\" width=\"150\" height=\"150\" \/>This post is for the C# developers out there and takes a look at the interesting conjunction of <code>[CallerArgumentExpression]<\/code> and static extension methods \u2013 a mix that at first seems too convenient to pass up.<\/p>\n<p>A quick recap: <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/csharp\/language-reference\/proposals\/csharp-10.0\/caller-argument-expression\" rel=\"follow\"><code>[CallerArgumentExpression]<\/code> landed<\/a> 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 <code>string<\/code> method parameter with <code>[CallerArgumentExpression(\"argName\")]<\/code> where <code>argName<\/code> is the name of the method argument you want stringified, and the compiler does the rest.<\/p>\n<p><!--more--><\/p>\n<p>Here&#8217;s a quick demo of how <code>[CallerArgumentExpression]<\/code> works:<\/p>\n<pre><code class=\"language-csharp\">using System;\r\nusing System.Runtime.CompilerServices;\r\n\r\npublic class Program\r\n{\r\n    static string Stringify(object obj,\r\n        [CallerArgumentExpression(\"obj\")] string expr = \"\")\r\n    {\r\n        return expr;\r\n    }\r\n\r\n    public static class Foo\r\n    {\r\n        public string Bar = \"bar\";\r\n    }\r\n\r\n    public static void Main()\r\n    {\r\n        var expr = Stringify(Foo.Bar);\r\n        Console.WriteLine(expr); \/\/ prints \"Foo.Bar\"\r\n        expr = Stringify(Foo.Bar + Foo.Bar);\r\n        Console.WriteLine(expr); \/\/ prints \"Foo.Bar + Foo.Bar\"\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>And you can try it online yourself <a href=\"https:\/\/dotnetfiddle.net\/BQPJCs\" rel=\"nofollow\">in this .NET Fiddle<\/a>.<\/p>\n<p>It&#8217;s really cool and it opens the door to a lot of possibilities (though I&#8217;m still stuck trying to figure some of them out, such as reliably setting\/clearing model binding errors that involve array expressions).<\/p>\n<p>As mentioned, this shipped with C# 10. And of course, C# 8 shipped &#8220;the big one:&#8221; <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/csharp\/language-reference\/proposals\/csharp-8.0\/nullable-reference-types\" rel=\"follow\">nullable reference types<\/a>. Since then, the following pattern has become familiar in many a codebase while devs figure out where variables actually can or can&#8217;t be null:<\/p>\n<pre><code class=\"language-csharp\">using System;\r\nusing System.Diagnostics.CodeAnalysis;\r\nusing System.Runtime.CompilerServices;\r\n\r\nstatic class Extensions\r\n{\r\n    public static T ThrowIfNull&lt;T&gt;([NotNull] T? value, string expr)\r\n    {\r\n        if (value is null) {\r\n            throw new ArgumentNullException(expr);\r\n        }\r\n        return value;\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>This does exactly what you think it does: it verifies that a value isn&#8217;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-<code>null<\/code>. To make it useful, it&#8217;s common enough to extend it with more caller attribute magic:<\/p>\n<pre><code class=\"language-csharp\">using System;\r\nusing System.Diagnostics.CodeAnalysis;\r\nusing System.Runtime.CompilerServices;\r\n\r\nstatic class Extensions\r\n{\r\n    public static T ThrowIfNull&lt;T&gt;(\r\n        [NotNull] T? value,\r\n        string expr,\r\n        [CallerMemberName] string callerName = \"\",\r\n        [CallerFilePath] string filePath = \"\",\r\n        [CallerLineNumber] int lineNumber = 0)\r\n    {\r\n        if (value is null) {\r\n            throw new InvalidOperationException(\r\n                $\"{expr} unexpectedly null in {callerName} \"\r\n                + $\"at {filePath}:{lineNumber}\");\r\n        }\r\n        return value;\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>Now we get useful exceptions that we&#8217;ll hopefully log and revisit to help us find any places in our codebase where we are assuming a value can&#8217;t be <code>null<\/code> but it turns out that, in fact, it can be.<\/p>\n<p>But what about if we try to add our new best buddy <code>[CallerArgumentExpression]<\/code> here, to get rid of the need to manually specify the text of the argument via <code>argName<\/code> in our <code>ThrowIfNull()<\/code>?<\/p>\n<pre><code class=\"language-csharp\">using System;\r\nusing System.Diagnostics.CodeAnalysis;\r\nusing System.Runtime.CompilerServices;\r\n\r\nstatic class Extensions\r\n{\r\n    public static T ThrowIfNull&lt;T&gt;(\r\n        [NotNull] T? value,\r\n        [CallerArgumentExpression(\"value\")] string expr = \"\",\r\n        [CallerMemberName] string callerName = \"\",\r\n        [CallerFilePath] string filePath = \"\",\r\n        [CallerLineNumber] int lineNumber = 0)\r\n    {\r\n        if (value is null) {\r\n            throw new InvalidOperationException(\r\n                $\"{expr} unexpectedly null in {callerName} \"\r\n                + $\" at {filePath}:{lineNumber}\");\r\n        }\r\n        return value;\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>At first blush, this works great. Use it with a single variable directly, as in <code>foo.ThrowIfNull()<\/code>, and everything will work swimmingly and it&#8217;ll do exactly what it says on the tin. But try using it in a more-complicated setting, say like <code>foo?.bar?.ThrowIfNull()<\/code>, and you&#8217;ll see what I mean: here, <code>argName<\/code> will only capture the last token in the chain and you&#8217;ll see that <code>argName<\/code> is only <code>bar<\/code> and not <code>foo.bar<\/code>!<\/p>\n<p>It&#8217;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 <code>bar<\/code> itself \u2013 after all, <code>T<\/code> here would have been <code>bar.GetType()<\/code>, so it&#8217;s not a huge stretch of the imagination to guess that <code>expr<\/code> might only span <code>bar<\/code> as well.<sup id=\"rf1-5037\"><a href=\"#fn1-5037\" title=\"Except expr is actually not bar but rather .bar!\" rel=\"footnote\">1<\/a><\/sup><\/p>\n<p>Indeed, when you look at what the code compiles to, you&#8217;ll see why. For the following code fragment:<\/p>\n<pre><code class=\"language-csharp\">public class Foo {\r\n    public string? Bar;\r\n}\r\n\r\npublic class C {\r\n    public void M(Foo? foo) {\r\n        foo?.Bar.ThrowIfNull();\r\n    }\r\n}\r\n<\/code><\/pre>\n<p><a href=\"https:\/\/sharplab.io\/#v2:EYLgZgpghgLgrgJwgZwLQGED2AbbEDGMAlpgHYAyRMECU2yAPgAIBMAjALABQTADAARM2AOgAiRKAHNSmZMXzJhWACYQAgqToBPZEWQBubn0EiASnFLEAthCWYrAByJ4EAZRoA3IvhSGuRgGZBFn4AMUxMfgBvbn44wSChXgB+fgAhKAQ\/AF9uQOD+dGjY+KZEgBZ+AFkACnDMVLAIgEpirniO\/iaG4QyEYQAVAAsETAB3AEkwADk4XBrmvw7crhX8oQA2AoBRAA9qUl0yZDaOgG0mAHYQfmnMGFncAF0SuLKTLYH+YdHJmbnsAAeAYAPhqZzuDwBT34MCGem+qQ8dDgEAANPwzug6C41AhJHAbJY9g4kMgjqQagAiZHYVFU5owpKpCC7Un8AC8\/FIANaMXanTiRDA\/BqtNR\/ARPNwfNegvicN+3IgY34eIJRKhuD2PgcxDINVZpMWcuWptKl344ogS3iK2yQA==\" rel=\"nofollow\">We get<\/a><\/p>\n<pre><code class=\"language-csharp\">public class Foo\r\n{\r\n    [System.Runtime.CompilerServices.Nullable(2)]\r\n    public string Bar;\r\n}\r\n\r\npublic class C\r\n{\r\n    [System.Runtime.CompilerServices.NullableContext(2)]\r\n    public void M(Foo foo)\r\n    {\r\n        if (foo != null)\r\n        {\r\n            Extensions.ThrowIfNull(foo.Bar, \".Bar\");\r\n        }\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>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&#8217;t have any other choice.<\/p>\n<p>When we change <code>ThrowIfNull()<\/code> from an extension method to a regular static method though, we get the result we <em>really<\/em> wanted:<\/p>\n<pre><code class=\"language-csharp\">public static class Utils\r\n{\r\n    public static T ThrowIfNull&lt;T&gt;(\r\n        [NotNull] T? value,\r\n        [CallerArgumentExpression(\"value\")] string? expr = null) \r\n    {\r\n        if (value is null) {\r\n            throw new ArgumentNullException(expr);\r\n        }\r\n        return value;\r\n    }\r\n}\r\n\r\npublic class Foo\r\n{\r\n    public string? Bar;\r\n}\r\n\r\npublic class C\r\n{\r\n    public void M(Foo? foo)\r\n    {\r\n        Utils.ThrowIfNull(foo?.Bar);\r\n    }\r\n}\r\n<\/code><\/pre>\n<p><a href=\"https:\/\/sharplab.io\/#v2:EYLgZgpghgLgrgJwgZwLQGED2AbbEDGMAlpgHYAyRMECU2yAPgAIBMAjALABQTADAARM2AOgAiRKAHNSmZMXzJhWACYQAgqToBPZEWQBubn0EiASnFLEAthCWYrAByJ4EAZRoA3IvhSGuRgGYTADZBFn4AVWJ6fgBvbn5E\/gBtJgB2EH4AOUwYLLhcAF0EpKYgoVCAFX5KgAsETAB3AEkwfNwAHkqAPgAKZJy8guxCmoB+fg86OAgAGhT0Ohc1BEk4G0sAUQAPByRkXTJegCIp7BnjgEpRoV4JiF2EfgBeflJhy7iSpKSiMH5emcZvw9G8Pl8uD8oUkYPUmm8II1+Cs1hshrgdj4HMQjg89pc\/NDEgBfb5Q9KTaYQQlJUlcOmBML8ABimEwEJ+ZRMd34ACEoAg\/AyeOVwugOaVygAWfgAWV6rMwEzAbM+8UhRKizkUdQaLTaw16KqVwn5CAJZLpxKAA=\" rel=\"nofollow\">Desugaring to<\/a>:<\/p>\n<pre><code class=\"language-csharp\">public class C\r\n{\r\n    [System.Runtime.CompilerServices.NullableContext(2)]\r\n    public void M(Foo foo)\r\n    {\r\n        Utils.ThrowIfNull((foo != null) ? foo.Bar : null, \"foo?.Bar\");\r\n    }\r\n}\r\n<\/code><\/pre>\n<p>Liked this post? Follow me on twitter <a href=\"https:\/\/twitter.com\/mqudsi\" rel=\"follow\">@mqudsi<\/a> and like this tweet for more .NET awesomeness!<\/p>\n<blockquote class=\"twitter-tweet\" data-width=\"550\" data-dnt=\"true\">\n<p lang=\"en\" dir=\"ltr\">Do you know how to effectively use [CallerArgumentExpression] to supercharge C# <a href=\"https:\/\/twitter.com\/dotnet?ref_src=twsrc%5Etfw\" rel=\"follow\">@dotnet<\/a> codebase? Here&#39;s the number one mistake to look out for. <a href=\"https:\/\/t.co\/sReLDSS3iw\" rel=\"follow\">https:\/\/t.co\/sReLDSS3iw<\/a><\/p>\n<p>&mdash; Mahmoud Al-Qudsi (@mqudsi) <a href=\"https:\/\/twitter.com\/mqudsi\/status\/1701285293372965228?ref_src=twsrc%5Etfw\" rel=\"follow\">September 11, 2023<\/a><\/p><\/blockquote>\n<p><script async src=\"https:\/\/platform.twitter.com\/widgets.js\" charset=\"utf-8\"><\/script><\/p>\n<div class=\"sendy_widget\" style='margin-bottom: 0.5em;'>\n<p><em>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.<\/em><\/p>\n<iframe tabIndex=-1 onfocus=\"sendy_no_focus\" src=\"https:\/\/neosmart.net\/sendy\/subscription?f=BUopX8f2VyLSOb892VIx6W4BB8V5K2ReYGLVwsfKUZLXCc892Ffz8rIgRyIGoE22cZVr&title=Join+the+dotnet+mailing+list\" style=\"height: 300px; width: 100%;\"><\/iframe>\n<\/div>\n<script type=\"text\/javascript\">function sendy_no_focus(e) { e.preventDefault(); }<\/script>\n<hr class=\"footnotes\"><ol class=\"footnotes\"><li id=\"fn1-5037\"><p>Except <code>expr<\/code> is actually not <code>bar<\/code> but rather <code>.bar<\/code>!&nbsp;<a href=\"#rf1-5037\" class=\"backlink\" title=\"Jump back to footnote 1 in the text.\">&#8617;<\/a><\/p><\/li><\/ol>","protected":false},"excerpt":{"rendered":"<p>This post is for the C# developers out there and takes a look at the interesting conjunction of [CallerArgumentExpression] and static extension methods \u2013 a mix that at first seems too convenient to pass up. A quick recap: [CallerArgumentExpression] landed &hellip; <a href=\"https:\/\/neosmart.net\/blog\/callerargumentexpression-and-extension-methods-dont-mix\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":505,"featured_media":5053,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[999],"tags":[91,325,1027,955,1003,11,1026],"class_list":["post-5037","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-net","tag-c","tag-callerargumentexpression","tag-dot-net","tag-dotnet","tag-programming","tag-stringification"],"aioseo_notices":[],"jetpack_featured_media_url":"https:\/\/neosmart.net\/blog\/wp-content\/uploads\/2023\/09\/c-sharp.jpg","jetpack_shortlink":"https:\/\/wp.me\/p4xDa-1jf","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts\/5037","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/users\/505"}],"replies":[{"embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/comments?post=5037"}],"version-history":[{"count":17,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts\/5037\/revisions"}],"predecessor-version":[{"id":5056,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts\/5037\/revisions\/5056"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/media\/5053"}],"wp:attachment":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/media?parent=5037"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/categories?post=5037"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/tags?post=5037"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}