Reputation: 109792
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
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