Why cannot I use Debug.Assert() with a method accepting dynamic and returning bool?

706 Views Asked by At

Here's my code:

class Program
{
    static void Main(string[] args)
    {
        dynamic param = null;
        System.Diagnostics.Debug.Assert(whatever(param));
    }

    static bool whatever(object param)
    {
        return true;
    }
}

When I run it I get RuntimeBinderException with the following message:

Cannot dynamically invoke method 'Assert' because it has a Conditional attribute

Yes, Assert() has ConditionalAttribute on it. Yet there's exactly one whatever() method that returns bool no matter what the method accepts.

What exactly does runtime complain about? Why cannot it use the bool and pass it into Assert()?

1

There are 1 best solutions below

2
On

The actual call in Main gets translated into a CallSite, because you're invoking the call with a dynamic param. The CallSite does all the preparation it needs in order to invoke this method at run-time. But, the problem is, that Assert has a Conditional attribute on it, which means it needs to pass information to the compiler pre-processor at compile-time

This blog post explains:

Conditional attributes can be placed on methods (and attributes in whidbey) to instruct the compiler to conditionally remove calls to the function if a symbol is not defined. This can be useful for debug-only functionality, like Debug.Assert, which has a Conditional("DEBUG") on it.

Conditional takes a string argument. If that string is defined (as determined by the compiler's preprocessor), then the compiler emits the method call. If the symbol is not defined, C# still compiles the method, but does not compile the calls.

And later, to strengthen our point:

The Conditional attribute is entirely handled by the compiler without any cooperation from the runtime. The method is still jitted normally, but the compiler just doesn't emit the calls if the symbol is not defined.

Now, we have a conflict. We can't pass parameters to the compiler pre-processor at run-time (to tell it if "DEBUG" is defined or not), only at compile-time, but the method will only be invoked at run-time, because that's when we'll infer the type of our dynamic value.

That's why the binder yells at run-time that this method can't actually be invoked, because that would be breaking the ConditionalAttribute.

Bonus:

This is what actually gets called:

private static void Main(string[] args)
{
    object param = null;
    if (Program.<Main>o__SiteContainer0.<>p__Site1 == null)
    {
        Program.<Main>o__SiteContainer0.<>p__Site1 = CallSite<Action<CallSite, Type, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "Assert", null, typeof(Program), new CSharpArgumentInfo[]
        {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
        }));
    }
    Action<CallSite, Type, object> arg_CB_0 = Program.<Main>o__SiteContainer0.<>p__Site1.Target;
    CallSite arg_CB_1 = Program.<Main>o__SiteContainer0.<>p__Site1;
    Type arg_CB_2 = typeof(Debug);
    if (Program.<Main>o__SiteContainer0.<>p__Site2 == null)
    {
        Program.<Main>o__SiteContainer0.<>p__Site2 = CallSite<Func<CallSite, Type, object, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.None, "whatever", null, typeof(Program), new CSharpArgumentInfo[]
        {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
        }));
    }
    arg_CB_0(arg_CB_1, arg_CB_2, Program.<Main>o__SiteContainer0.<>p__Site2.Target(Program.<Main>o__SiteContainer0.<>p__Site2, typeof(Program), param));