Strange behaviour when using dynamic types as method parameters
Asked Answered
H

1

38

I have the following interfaces that are part of an existing project. I'd like to make it possible to call the Store(..) function with dynamic objects. But I don't want to change the Interface hierarchy (if at all possible).

public interface IActualInterface
{
    void Store(object entity);    
}
public interface IExtendedInterface : IActualInterface
{
    //Interface items not important
}        
public class Test : IExtendedInterface 
{
    public void Store(object entity)
    {
        Console.WriteLine("Storing: " + entity.ToString());
    }       
}

and the following code:

IExtendedInterface extendedInterfaceTest = new Test();
IActualInterface actualInterfaceTest = new Test();
Test directTest = new Test();

dynamic employee = new ExpandoObject();
employee.Name = "John Smith";
employee.Age = 33;
employee.Phones = new ExpandoObject();
employee.Phones.Home = "0111 123123";
employee.Phones.Office = "027 321123";
employee.Tags = new List<dynamic>() { 123.4D, 99.54D };

try
{
    extendedInterfaceTest .Store(employee);
}
catch (RuntimeBinderException rbEx)
{
    Console.WriteLine(rbEx.Message);
}

//Casting as (object) works okay as it's not resolved at runtime
extendedInterfaceTest.Store((object)employee);

//this works because IActualInterface implements 'Store'
actualInterfaceTest.Store(employee);
//this also works okay (directTest : IProxyTest)
directTest.Store(employee);

When I call extendedInterfaceTest.Store(employee), it raises a runtime binder exception. Why does the interface type make a difference when it's the same underlying type? I can call it on IActualInterface and Type, but not IExtendedInterface?

I understand that when calling a function with a dynamic parameter, the resolution happens at runtime, but why the different behaviours?

Houseyhousey answered 18/6, 2010 at 16:58 Comment(1)
Found this working on Raven didn't you?Chipman
C
87

What you need to remember is that dynamic resolution basically does the same process as static resolution, but at runtime. Anything that couldn't be resolved by the CLR won't be resolved by the DLR.

Let's take this small program, inspired by yours, and that doesn't use dynamic at all:

namespace ConsoleApplication38 {

    public interface IActualInterface {
        void Store(object entity);
    }
    public interface IExtendedInterface : IActualInterface {
    }
    public class TestInterface : IExtendedInterface {
        public void Store(object entity) {
        }
    }

    public abstract class ActualClass {
        public abstract void Store(object entity);
    }
    public abstract class ExtendedClass : ActualClass { 
    }
    public class TestClass : ExtendedClass {
        public override void Store(object entity) {
        }
    }

    class Program {

        static void TestInterfaces() {
            IActualInterface actualTest = new TestInterface();
            IExtendedInterface extendedTest = new TestInterface();
            TestInterface directTest = new TestInterface();
            
            actualTest.Store(null);
            extendedTest.Store(null);
            directTest.Store(null);
        }

        static void TestClasses() {
            ActualClass actualTest = new TestClass();
            ExtendedClass extendedTest = new TestClass();
            TestClass directTest = new TestClass();

            actualTest.Store(null);
            extendedTest.Store(null);
            directTest.Store(null);
        }

        static void Main(string[] args) {
            TestInterfaces();
            TestClasses();
        }
    }
}

Everything compiles fine. But what did the compiler really generate? Let's see using ILdasm.

For the interfaces:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestInterface::Store(object)

We can see here that the C# compiler always generates calls for the interface or class where the method is defined. IActualInterface has a method slot for Store so it's used for actualTest.Store. IExtendedInterface doesn't, so IActualInterface is used for the call. TestInterface defines a new method Store, using the newslot IL modifier, effectively assigning a new slot in the vtable for that method, so it's directly used since directTest is of type TestInterface.

For the classes:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

For the 3 different types, the same call is generated because the method slot is defined on ActualClass.

Let's now see what we get if we write the IL ourselves, using the type we want rather than letting the C# compiler choosing it for us. I've modified the IL to look like this:

For interfaces:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.IActualInterface::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.IExtendedInterface::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestInterface::Store(object)

For classes:

// actualTest.Store
IL_0015:  callvirt   instance void ConsoleApplication38.ActualClass::Store(object)

// extendedTest.Store
IL_001d:  callvirt   instance void ConsoleApplication38.ExtendedClass::Store(object)

// directTest.Store
IL_0025:  callvirt   instance void ConsoleApplication38.TestClass::Store(object)

The program compiles fine with ILasm. However it fails to pass peverify and crashes at runtime with the following error:

Unhandled Exception: System.MissingMethodException: Method not found: 'Void ConsoleApplication38.IExtendedInterface.Store(System.Object)'. at ConsoleApplication38.Program.TestInterfaces() at ConsoleApplication38.Program.Main(String[] args)

If you remove this invalid call, the derived classes calls work fine without any error. The CLR is able to resolve the base method from the derived type call. However interfaces have no true representation in runtime, and the CLR isn't able to resolve the method call from the extended interface.

In theory, the C# compiler could emit the call directly to the correct class specified in the runtime. It would avoid problems about middle classes calls as seen on Eric Lippert's blog. However as demonstrated, this is not possible for interfaces.

Let's get back to the DLR. It resolves the method exactly the same way as the CLR. We've seen that IExtendedInterface.Store couldn't be resolved by the CLR. The DLR cannot either! This is totally hidden by the fact that the C# compiler will emit the right call, so always be careful when using dynamic unless you perfectly know how it works in the CLR.

Clougher answered 18/6, 2010 at 18:40 Comment(2)
Thanks for the in-depth answer!Houseyhousey
In a later posting of this same issue, Eric Lippert admits that this is probably a bug. There is a Microsoft Connect report where Microsoft marked it as Won't Fix.Stringer

© 2022 - 2024 — McMap. All rights reserved.