Matthew Watson
Matthew Watson

Reputation: 109792

How can I use CallerArgumentExpression with Visual Studio 2022 and .net Standard 2.0 or .net 4.8?

With Visual Studio 2022 and .net 6.0 we have the new CallerArgumentExpression attribute that we can use to "capture the expressions passed to a method, to enable better error messages in diagnostic/testing APIs and reduce keystrokes".

For example, we can write a class to check for null method arguments like so:

public static class Contract
{
    public static T RequiresArgNotNull<T>(T? item, [CallerArgumentExpression("item")] string? expression = default, string? message = null)
        where T : class
    {
        if (item == null)
            throw new ArgumentNullException(
                expression ?? "<unknown>",
                message ?? (expression != null ? "Requires " + expression + " != null" : "RequiresArgNotNull() failed."));

        return item;
    }
}

Which can be used like so:

using static ClassLibrary1.Contract; // To allow just putting RequiresArgNotNull()

...

static void test(string theString)
{
    RequiresArgNotNull(theString); // Note that we do NOT need to pass the parameter
                                   // name as a separate string.
    Console.WriteLine(theString);
}

If theString is null, an exception will be thrown which looks like this:

System.ArgumentNullException: Requires theString != null
Parameter name: theString

I would like to be able to use this feature with .net 4.8 and/or .net Standard 2.0. Is this possible?

Upvotes: 21

Views: 5117

Answers (1)

Matthew Watson
Matthew Watson

Reputation: 109792

If you are using Visual Studio 2022, you can use CallerArgumentExpression with .net 4.8 and/or .net Standard 2.0 by defining a local internal implementation of CallerArgumentExpressionAttribute like so:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    internal sealed class CallerArgumentExpressionAttribute : Attribute
    {
        public CallerArgumentExpressionAttribute(string parameterName)
        {
            ParameterName = parameterName;
        }

        public string ParameterName { get; }
    }
}

Note that you must use the System.Runtime.CompilerServices namespace in order for this to work correctly. By making this implementation internal you can guarantee that it won't conflict with any system-defined implementation.

This will compile as a .net Standard 2.0 target, so it can be also used by assemblies that target .net 4.8, .net Core 3.1 and so on.

Also note that you still require Visual Studio 2022 and its SDK for this to work - if you try to use Visual Studio 2019 it will compile OK but the argument will be null.

You can include the above class in the assembly that uses CallerArgumentExpression and it will work as expected.


Sample .net Standard 2.0 class library providing Contract.RequiresArgNotNull():

(This sample assembly is using namespace ClassLibrary1 for brevity.)

Project:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8</LangVersion>
  </PropertyGroup>

</Project>

CallerArgumentExpression.cs:

namespace System.Runtime.CompilerServices
{
    #if !NET6_0_OR_GREATER

    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    internal sealed class CallerArgumentExpressionAttribute : Attribute
    {
        public CallerArgumentExpressionAttribute(string parameterName)
        {
            ParameterName = parameterName;
        }

        public string ParameterName { get; }
    }

    #endif
}

Contract.cs:

using System;
using System.Runtime.CompilerServices;

#nullable enable

namespace ClassLibrary1
{
    public static class Contract
    {
        public static T RequiresArgNotNull<T>(T? item, [CallerArgumentExpression("item")] string? expression = default, string? message = null)
            where T : class
        {
            if (item == null)
                throw new ArgumentNullException(
                    expression ?? "<unknown>",
                    message ?? (expression != null ? "Requires " + expression + " != null" : "RequiresArgNotNull() failed."));

            return item;
        }
    }
}

Sample console application demonstrating the use of RequiresArgNotNull():

using System;
using static ClassLibrary1.Contract;

#nullable enable

namespace Demo
{
    class Program
    {
        static void Main()
        {
            try
            {
                test(null!);
            }

            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());   
            }
        }

        static void test(string theString)
        {
            RequiresArgNotNull(theString);
            Console.WriteLine(theString);
        }
    }
}

This will output the following exception message (for my particular build):

System.ArgumentNullException: Requires theString != null
Parameter name: theString
   at ClassLibrary1.Contract.RequiresArgNotNull[T](T item, String expression, String message) in E:\Test\cs9\ConsoleApp1\ClassLibrary1\Contract.cs:line 18
   at Demo.Program.test(String theString) in E:\Test\cs9\ConsoleApp1\ConsoleApp1\Program.cs:line 25
   at Demo.Program.Main() in E:\Test\cs9\ConsoleApp1\ConsoleApp1\Program.cs:line 14

Upvotes: 33

Related Questions