开发者

Why is an ExpandoObject breaking code that otherwise works just fine?

Here's the setup: I have an Open Source project called Massive and I'm slinging around dynamics as a way of creating SQL on the fly, and dynamic result sets on the fly.

To do the database end of things I'm using System.Data.Common and the ProviderFactory stuff. Here's a sample that works just fine (it's static so you can run in a Console):

    static DbCommand CreateCommand(string sql) {
        return DbProviderFactories.GetFactory("System.Data.SqlClient")
                                  .CreateCommand();
    }
    static DbConnection OpenConnection() {
        return DbProviderFactories.GetFactory("System.Data.SqlClient")
                                  .CreateConnection();
    }
    public static dynamic DynamicWeirdness() {
        using (var conn = OpenConnection()) {
            var cmd = CreateCommand("SELECT * FROM Products");
            cmd.Connection = conn;
        }
        Console.WriteLine("It worked!");
        Console.Read();
        return null;
    }

The result of running this code is "It worked!"

Now, if I change the string argument to dynamic - specifically an ExpandoObject (pretend that there's a routine somewhere that crunches the Expando into SQL) - a weird error is thrown. Here's the code:

Why is an ExpandoObject breaking code that otherwise works just fine?

What worked before now fails with a message that makes no sense. A SqlConnection is a DbConnection - moreover if you mouseover the code in debug, you can see that the types are all SQL types. "conn" is a SqlConnection, "cmd" is a SqlCommand.

This error makes utterly no sense - but more importantly it's cause by the presence of an ExpandoObject that doesn't touch any of the implementation code. The differences between the two routines are: 1 - I've changed the argument in CreateCommand() to accept "dynamic" instead of string 2 - I've created an ExpandoObject and set a property.

It gets weirder.

If simply use a string ins开发者_Python百科tead of the ExpandoObject - it all works just fine!

    //THIS WORKS
    static DbCommand CreateCommand(dynamic item) {
        return DbProviderFactories.GetFactory("System.Data.SqlClient").CreateCommand();
    }
    static DbConnection OpenConnection() {
        return DbProviderFactories.GetFactory("System.Data.SqlClient").CreateConnection();
    }
    public static dynamic DynamicWeirdness() {
        dynamic ex = new ExpandoObject();
        ex.TableName = "Products";
        using (var conn = OpenConnection()) {
            //use a string instead of the Expando
            var cmd = CreateCommand("HI THERE");
            cmd.Connection = conn;
        }
        Console.WriteLine("It worked!");
        Console.Read();
        return null;
    }

If I swap out the argument for CreateCommand() to be my ExpandoObject ("ex") - it causes all of the code to be a "dynamic expression" which is evaluated at runtime.

It appears that the runtime evaluation of this code is different than compile-time evaluation... which makes no sense.

**EDIT: I should add here that if I hard-code everything to use SqlConnection and SqlCommand explicitly, it works :) - here's an image of what I mean:

Why is an ExpandoObject breaking code that otherwise works just fine?


When you pass the dynamic to CreateCommand, the compiler is treating its return type as a dynamic that it has to resolve at runtime. Unfortunately, you're hitting some oddities between that resolver and the C# language. Fortunately, it's easy to work around by removing your use of var forcing the compiler to do what you expect:

public static dynamic DynamicWeirdness() {
    dynamic ex = new ExpandoObject ();
    ex.Query = "SELECT * FROM Products";
    using (var conn = OpenConnection()) {
        DbCommand cmd = CreateCommand(ex); // <-- DON'T USE VAR
        cmd.Connection = conn;
    }
    Console.WriteLine("It worked!");
    Console.Read();
    return null;
}

This has been tested on Mono 2.10.5, but I'm sure it works with MS too.


It's acting as if you're trying to pass dynamics anonymous types across assemblies, which is not supported. Passing an ExpandoObject is supported though. The work-around I have used, when I need to pass across assemblies, and I have tested it successfully, is to cast the dynamic input variable as an ExpandoObject when you pass it in:

public static dynamic DynamicWeirdness()
{
    dynamic ex = new ExpandoObject();
    ex.TableName = "Products";
    using (var conn = OpenConnection()) {
        var cmd = CreateCommand((ExpandoObject)ex);
        cmd.Connection = conn;
    }
    Console.WriteLine("It worked!");
    Console.Read();
    return null;
}

EDIT: As pointed out in the comments, you CAN pass dynamics across assemblies, you CAN'T pass anonymous types across assemblies without first casting them.

The above solution is valid for the same reason as Frank Krueger states above.

When you pass the dynamic to CreateCommand, the compiler is treating its return type as a dynamic that it has to resolve at runtime.


Because you're using dynamic as the argument to CreateCommand(), the cmd variable is also dynamic, which means its type is resolved at runtime to be SqlCommand. By contrast, the conn variable is not dynamic and is compiled to be of type DbConnection.

Basically, SqlCommand.Connection is of type SqlConnection, so the conn variable, which is of type DbConnection, is an invalid value to set Connection to. You can fix this by either casting conn to an SqlConnection, or making the conn variable dynamic.

The reason it worked fine before was because cmd was actually a DbCommand variable (even so it pointed to the same object), and the DbCommand.Connection property is of type DbConnection. i.e. the SqlCommand class has a new definition of the Connection property.

Source issues annotated:

 public static dynamic DynamicWeirdness() {
    dynamic ex = new ExpandoObject();
    ex.TableName = "Products";
    using (var conn = OpenConnection()) { //'conn' is statically typed to 'DBConnection'
        var cmd = CreateCommand(ex); //because 'ex' is dynamic 'cmd' is dynamic
        cmd.Connection = conn; 
        /*
           'cmd.Connection = conn' is bound at runtime and
           the runtime signature of Connection takes a SqlConnection value. 
           You can't assign a statically defined DBConnection to a SqlConnection
           without cast. 
        */
    }
    Console.WriteLine("It will never get here!");
    Console.Read();
    return null;
}

Options for fixing source (pick only 1):

  1. Cast to statically declare conn as a SqlConnection: using (var conn = (SqlConnection) OpenConnection())

  2. Use runtime type of conn: using (dynamic conn = OpenConnection())

  3. Don't dynamic bind CreateCommand: var cmd = CreateCommand((object)ex);

  4. Statically define cmd: DBCommand cmd = CreateCommand(ex);


Looking at the exception being thrown, it seems that even though OpenConnection returns a static type (DbConnection) and CreateCommand returns a static type (DbCommand), because the parameter passed to DbConnection is of type dynamic it's essentially treating the following code as a dynamic binding site:

 var cmd = CreateCommand(ex);
    cmd.Connection = conn;

Because of this, the runtime-binder is trying to find the most specific binding possible, which would be to cast the connection to SqlConnection. Even though the instance is technically a SqlConnection, it's statically typed as DbConnection, so that's what the binder attempts to cast from. Since there's no direct cast from DbConnection to SqlConnection, it fails.

What seems to work, taken from this S.O. answer dealing with the underlying exception type, is to actually declare conn as dynamic, rather than using var, in which case the binder finds the SqlConnection -> SqlConnection setter and just works, like so:

public static dynamic DynamicWeirdness()
    {
        dynamic ex = new ExpandoObject();
        ex.TableName = "Products";
        using (dynamic conn = OpenConnection())
        {
            var cmd = CreateCommand(ex);
            cmd.Connection = conn;
        }
        Console.WriteLine("It worked!");
        Console.Read();
        return null;
    }

That being said, given the fact that you statically typed the return type of CreateCommand to DbConnection, one would have thought the binder would have done a better job of "doing the right thing" in this case, and this may well be a bug in the dynamic binder implementation in C#.


It appears that the runtime evaluation of this code is different than compile-time evaluation... which makes no sense.

That's what's going on. If any part of an invocation is dynamic, the entire invocation is dynamic. Passing a dynamic argument to a method causes the entire method to be invoked dynamically. And that makes the return type dynamic, and so on and so on. That's why it works when you pass a string, you're no longer invoking it dynamically.

I don't know specifically why the error occurs, but I guess implicit casts aren't handled automatically. I know there are some other cases of dynamic invocation that behave slightly differently than normal because we hit one of them when doing some of the dynamic POM (page object model) stuff in Orchard CMS. That's an extreme example though, Orchard plugs pretty deeply into dynamic invocation and may simply be doing things that it wasn't designed for.

As for "it makes no sense" -- agree that it is unexpected, and hopefully improved on in future revs. I bet there some some subtle reasons over my head that the language experts could explain on why it doesn't work just automatically.

This is one reason why I like to limit the dynamic parts of the code. If you're calling something that isn't dynamic with a dynamic value but you know what type you expect it to be, explicitly cast it to prevent the invocation from being dynamic. You get back into 'normal land', compile type checking, refactoring, etc. Just box in the dynamic use where you need it, and no more than that.


This question piqued my interest, and after a bit of back and forth on twitter I thought it might be worth writing my own take on the issue. After accepting Frank's answer, you mentioned on twitter that it worked, but didn't explain the 'weirdness'. Hopefully this can explain the weirdness, and exlpain why Frank's and Alexander's solutions work, as well as adding a bit of detail to Shane's initial answer.

The issue you've encountered is exactly as Shane first described. You are getting type mismatches based on a combination of compile-time type inference (due in part to the use of the var keyword), and runtime type resolution due to use of dynamic.

Firstly, compile-time type inference: C# is a statically or strongly typed language. Even dynamic is a static type, but one that bypasses static type checking (as discussed here). Take the following simple situation:

class A {}
class B : A {}
...
A a = new B();

In this situation, the static, or compile-time type of a is A, even though at runtime the actual object is of type B. The compiler will ensure that any use of a conforms only to what class A makes available, and any B specific functionality will require an explicit cast. Even at runtime, a is still considered to be statically A, despite the actual instance being a B.

If we change the initial declaration to var a = new B();, the C# compiler now infers the type of a. In this situation, the most specific type it can infer from the information is that a is of type B. Thus, a has a static or compile-time type of B, and the specific instance at runtime will also be of type B.

Type inference aims for the most specific type based on the information available. Take the following for example:

static A GetA()
{
    return new B();
}
...
var a = GetA();

Type inference will now infer a to be of type A as that is the information available to the compiler at the callsite. Consequently, a has a static or compile-time type of A and the compiler ensures that all usage of a conforms to A. Once again, even at runtime, a has a static type of A even though the actual instance is of type B.

Secondly, dynamic and runtime-evaluation: As stated in the previous article I linked to, dynamic is still a static type, but the C# compiler does not perform any static type checking on any statement or expression that has a type dynamic. For instance, dynamic a = GetA(); has a static or compile-time type of dynamic, and consequently no compile-time static type checks are performed on a. At run time, this will be a B, and can be used in any situation that accepts a static type of dynamic (i.e. all situations). If it is used in a situation that doesn't accept a B then a run-time error is thrown. However, if an operation involves a conversion from dynamic to another type, that expression is not dynamic. For instance:

dynamic a = GetA();
var b = a; // whole expression is dynamic
var b2 = (B)a; // whole expression is not dynamic, and b2 has static type of B

This is situation is obvious, but it becomes less so in more complex examples.

static A GetADynamic(dynamic item)
{
    return new B();
}
...
dynamic test = "Test";
var a = GetADynamic(test); // whole expression is dynamic
var a2 = GetADynamic((string)test); // whole expression is not dynamic, and a2 has a static type of `A`

The second statement here is not dynamic, due to type-casting test to string (even though the parameter type is dynamic). Consequently, the compiler can infer the type of a2 from the return type of GetADynamic and a2 has a static or compile-time type of A.

Using this information, it's possible to create a trivial replica of the error you were receiving:

class A
{
    public C Test { get; set; }
}

class B : A
{
    public new D Test { get; set; }
}

class C {}

class D : C {}
...
static A GetA()
{
    return new B();
}

static C GetC()
{
    return new D();
}

static void DynamicWeirdness()
{
    dynamic a = GetA();
    var c = GetC();
    a.Test = c;
}

In this example, we get the same run-time exception at line a.Test = c;. a has the static type of dynamic and at run-time will be an instance of B. c is not dynamic. The compiler infers its type to be C using the information available (return type of GetC). Thus, c has a static compile-time type of C, and even though at run-time it will be an instance of D, all uses have to conform to its static type of C. Consequently, we get a run-time error on the third line. The run-time binder evaluates a to be a B, and consequently Test is of type D. However, the static type of c is C and not D, so even though c is actually an instance of D, it can't be assigned without first casting (casting its static type C to D).

Moving onto your specific code and problem (finally!!):

public static dynamic DynamicWeirdness()
{
    dynamic ex = new ExpandoObject();
    ex.TableName = "Products";
    using (var conn = OpenConnection())
    {
        var cmd = CreateCommand(ex);
        cmd.Connection = conn;
    }
    Console.WriteLine("It worked!");
    Console.Read();
    return null;
}

ex has the static type of dynamic and consequently all expressions involving it are also dynamic and thus bypass static type checking at compile time. However, nothing in this line using (var conn = OpenConnection()) is dynamic, and consequently all typing is inferred at compile-time. Therefore, conn has a static compile-time type of DbConnection even though at runtime it will be an instance of SqlConnection. All usages of conn will assume it is a DbConnection unless it is cast to change its static type. var cmd = CreateCommand(ex); uses ex, which is dynamic, and consequently the whole expression is dynamic. This means that cmd is evaluated at run-time, and its static type is dynamic. The run-time then evaluates this line cmd.Connection = conn;. cmd is evaluated to be a SqlCommand and thus Connection expects SqlConnection. However, the static type of conn is still DbConnection, so the runtime throws an error as it can't assign an object with static type DbConnection to a field requiring SqlConnection without first casting the static type to SqlConnection.

This not only explains why you get the error, but also why the proposed solutions work. Alexander's solution fixed the problem by changing the line var cmd = CreateCommand(ex); to var cmd = CreateCommand((ExpandoObject)ex);. However, this isn't due to passing dynamic across assemblies. Instead, it fits into the situation described above (and in the MSDN article): explicitly casting ex to ExpandoObject means the expression is no longer evaluated as dynamic. Consequently, the compiler is able to infer the type of cmd based on the return type of CreateCommand, and cmd now has a static type of DbCommand (instead of dynamic). The Connection property of DbCommand expects a DbConnection, not a SqlConnection, and so conn is assigned without error.

Frank's solution works for essentially the same reason. var cmd = CreateCommand(ex); is a dynamic expression. 'DbCommand cmd = CreateCommand(ex);requires a conversion fromdynamicand consequently falls into the category of expressions involvingdynamicthat are not themselves dynamic. As the static or compile-time type ofcmdis now explicitlyDbCommand, the assignment toConnection` works.

Finally, addressing your comments on my gist. Changing using (var conn = OpenConnection()) to using (dynamic conn = OpenConnection()) works because conn is now dyanmic. This means it has a static or compile-time type of dynamic and thus bypasses static type checking. Upon assignment at line cmd.Connection = conn the run-time is now evaluating both 'cmd' and 'conn', and their static types are not coming into play (because they are dynamic). Because they are instances of SqlCommand and SqlConnection respectively, it all works.

As for the statement 'the entire block is a dynamic expression - given that then there is no compile time type': as your method DynamicWeirdness returns dynamic, any code that calls it is going to result in dynamic (unless it performs an explicit conversion, as discussed). However, that doesn't mean that all code inside the method is treated as dynamic - only those statements that explicitly involve dynamic types, as discussed. If the entire block was dynamic, you presumably wouldn't be able to get any compile errors, which is not the case. The following, for instance, doesn't compile, demonstrating that the entire block is not dynamic and static / compile-time types do matter:

public static dynamic DynamicWeirdness()
{
    dynamic ex = new ExpandoObject();
    ex.TableName = "Products";
    using (var conn = OpenConnection())
    {
        conn.ThisMethodDoesntExist();
        var cmd = CreateCommand(ex);
        cmd.Connection = conn;
    }
    Console.WriteLine("It worked!");
    Console.Read();
    return null;
}

Finally, in regards to your comments about the debug display / console output of the objects: that isn't surprising and doesn't contradict anything here. GetType() and the debugger both output the type of the instance of the object, not the static type of the variable itself.


You don't need to use the Factory to create the command. Just use conn.CreateCommand(); it will be the correct type and the connection will already be set.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜