Events
Mar 17, 11 PM - Mar 21, 11 PM
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register nowThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.
There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.
You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.
Champion issue: https://github.com/dotnet/csharplang/issues/287
Allow developers to capture the expressions passed to a method, to enable better error messages in diagnostic/testing APIs and reduce keystrokes.
When an assertion or argument validation fails, the developer wants to know as much as possible about where and why it failed. However, today's diagnostic APIs do not fully facilitate this. Consider the following method:
T Single<T>(this T[] array)
{
Debug.Assert(array != null);
Debug.Assert(array.Length == 1);
return array[0];
}
When one of the asserts fail, only the filename, line number, and method name will be provided in the stack trace. The developer will not be able to tell which assert failed from this information-- they will have to open the file and navigate to the provided line number to see what went wrong.
This is also the reason testing frameworks have to provide a variety of assert methods. With xUnit, Assert.True
and Assert.False
are not frequently used because they do not provide enough context about what failed.
While the situation is a bit better for argument validation because the names of invalid arguments are shown to the developer, the developer must pass these names to exceptions manually. If the above example were rewritten to use traditional argument validation instead of Debug.Assert
, it would look like
T Single<T>(this T[] array)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
if (array.Length != 1)
{
throw new ArgumentException("Array must contain a single element.", nameof(array));
}
return array[0];
}
Notice that nameof(array)
must be passed to each exception, although it's already clear from context which argument is invalid.
In the above examples, including the string "array != null"
or "array.Length == 1"
in the assert message would help the developer determine what failed. Enter CallerArgumentExpression
: it's an attribute the framework can use to obtain the string associated with a particular method argument. We would add it to Debug.Assert
like so
public static class Debug
{
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}
The source code in the above example would stay the same. However, the code the compiler actually emits would correspond to
T Single<T>(this T[] array)
{
Debug.Assert(array != null, "array != null");
Debug.Assert(array.Length == 1, "array.Length == 1");
return array[0];
}
The compiler specially recognizes the attribute on Debug.Assert
. It passes the string associated with the argument referred to in the attribute's constructor (in this case, condition
) at the call site. When either assert fails, the developer will be shown the condition that was false and will know which one failed.
For argument validation, the attribute cannot be used directly, but can be made use of through a helper class:
public static class Verify
{
public static void Argument(bool condition, string message, [CallerArgumentExpression("condition")] string conditionExpression = null)
{
if (!condition) throw new ArgumentException(message: message, paramName: conditionExpression);
}
public static void InRange(int argument, int low, int high,
[CallerArgumentExpression("argument")] string argumentExpression = null,
[CallerArgumentExpression("low")] string lowExpression = null,
[CallerArgumentExpression("high")] string highExpression = null)
{
if (argument < low)
{
throw new ArgumentOutOfRangeException(paramName: argumentExpression,
message: $"{argumentExpression} ({argument}) cannot be less than {lowExpression} ({low}).");
}
if (argument > high)
{
throw new ArgumentOutOfRangeException(paramName: argumentExpression,
message: $"{argumentExpression} ({argument}) cannot be greater than {highExpression} ({high}).");
}
}
public static void NotNull<T>(T argument, [CallerArgumentExpression("argument")] string argumentExpression = null)
where T : class
{
if (argument == null) throw new ArgumentNullException(paramName: argumentExpression);
}
}
static T Single<T>(this T[] array)
{
Verify.NotNull(array); // paramName: "array"
Verify.Argument(array.Length == 1, "Array must contain a single element."); // paramName: "array.Length == 1"
return array[0];
}
static T ElementAt<T>(this T[] array, int index)
{
Verify.NotNull(array); // paramName: "array"
// paramName: "index"
// message: "index (-1) cannot be less than 0 (0).", or
// "index (6) cannot be greater than array.Length - 1 (5)."
Verify.InRange(index, 0, array.Length - 1);
return array[index];
}
A proposal to add such a helper class to the framework is underway at https://github.com/dotnet/corefx/issues/17068. If this language feature was implemented, the proposal could be updated to take advantage of this feature.
The this
parameter in an extension method may be referenced by CallerArgumentExpression
. For example:
public static void ShouldBe<T>(this T @this, T expected, [CallerArgumentExpression("this")] string thisExpression = null) {}
contestant.Points.ShouldBe(1337); // thisExpression: "contestant.Points"
thisExpression
will receive the expression corresponding to the object before the dot. If it's called with static method syntax, e.g. Ext.ShouldBe(contestant.Points, 1337)
, it will behave as if first parameter wasn't marked this
.
There should always be an expression corresponding to the this
parameter. Even if an instance of a class calls an extension method on itself, e.g. this.Single()
from inside a collection type, the this
is mandated by the compiler so "this"
will get passed. If this rule is changed in the future, we can consider passing null
or the empty string.
Caller*
attributes, such as CallerMemberName
, this attribute may only be used on parameters with default values.CallerArgumentExpression
are permitted, as shown above.System.Runtime.CompilerServices
.null
or a string that is not a parameter name (e.g. "notAParameterName"
) is provided, the compiler will pass in an empty string.CallerArgumentExpressionAttribute
is applied to must have a standard conversion from string
. This means no user-defined conversions from string
are allowed, and in practice means the type of such a parameter must be string
, object
, or an interface implemented by string
.People who know how to use decompilers will be able to see some of the source code at call sites for methods marked with this attribute. This may be undesirable/unexpected for closed-source software.
Although this is not a flaw in the feature itself, a source of concern may be that there exists a Debug.Assert
API today that only takes a bool
. Even if the overload taking a message had its second parameter marked with this attribute and made optional, the compiler would still pick the no-message one in overload resolution. Therefore, the no-message overload would have to be removed to take advantage of this feature, which would be a binary (although not source) breaking change.
[assembly: EnableCallerArgumentExpression]
attribute they put in AssemblyInfo.cs
.
// Assembly1
void Foo(string bar); // V1
void Foo(string bar, string barExpression = "not provided"); // V2
void Foo(string bar, [CallerArgumentExpression("bar")] string barExpression = "not provided"); // V3
// Assembly2
Foo(a); // V1: Compiles to Foo(a), V2, V3: Compiles to Foo(a, "not provided")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
// Assembly3
[assembly: EnableCallerArgumentExpression]
Foo(a); // V1: Compiles to Foo(a), V2: Compiles to Foo(a, "not provided"), V3: Compiles to Foo(a, "a")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
Debug.Assert
, an alternative solution would be to add a CallerInfo
struct to the framework that contains all the necessary information about the caller.struct CallerInfo
{
public string MemberName { get; set; }
public string TypeName { get; set; }
public string Namespace { get; set; }
public string FullTypeName { get; set; }
public string FilePath { get; set; }
public int LineNumber { get; set; }
public int ColumnNumber { get; set; }
public Type Type { get; set; }
public MethodBase Method { get; set; }
public string[] ArgumentExpressions { get; set; }
}
[Flags]
enum CallerInfoOptions
{
MemberName = 1, TypeName = 2, ...
}
public static class Debug
{
public static void Assert(bool condition,
// If a flag is not set here, the corresponding CallerInfo member is not populated by the caller, so it's
// pay-for-play friendly.
[CallerInfo(CallerInfoOptions.FilePath | CallerInfoOptions.Method | CallerInfoOptions.ArgumentExpressions)] CallerInfo callerInfo = default(CallerInfo))
{
string filePath = callerInfo.FilePath;
MethodBase method = callerInfo.Method;
string conditionExpression = callerInfo.ArgumentExpressions[0];
//...
}
}
class Bar
{
void Foo()
{
Debug.Assert(false);
// Translates to:
var callerInfo = new CallerInfo();
callerInfo.FilePath = @"C:\Bar.cs";
callerInfo.Method = MethodBase.GetCurrentMethod();
callerInfo.ArgumentExpressions = new string[] { "false" };
Debug.Assert(false, callerInfo);
}
}
This was originally proposed at https://github.com/dotnet/csharplang/issues/87.
There are a few disadvantages of this approach:
Despite being pay-for-play friendly by allowing you to specify which properties you need, it could still hurt perf significantly by allocating an array for the expressions/calling MethodBase.GetCurrentMethod
even when the assert passes.
Additionally, while passing a new flag to the CallerInfo
attribute won't be a breaking change, Debug.Assert
won't be guaranteed to actually receive that new parameter from call sites that compiled against an old version of the method.
TBD
N/A
C# feature specifications feedback
C# feature specifications is an open source project. Select a link to provide feedback:
Events
Mar 17, 11 PM - Mar 21, 11 PM
Join the meetup series to build scalable AI solutions based on real-world use cases with fellow developers and experts.
Register now