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

前端 未结 7 943
感动是毒
感动是毒 2021-02-01 02:12

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.

<
相关标签:
7条回答
  • 2021-02-01 02:38

    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);

    0 讨论(0)
  • 2021-02-01 02:43

    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.

    0 讨论(0)
  • 2021-02-01 02:47

    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 讨论(0)
  • 2021-02-01 02:48

    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#.

    0 讨论(0)
  • 2021-02-01 02:53

    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.

    0 讨论(0)
  • 2021-02-01 02:53

    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.

    0 讨论(0)
提交回复
热议问题