Codementor Events

.NET: Limit the scope of your `dynamic`

Published Aug 02, 2019
.NET: Limit the scope of your `dynamic`

Check out the original version of this article on my developer blog.

Dynamic is contagious!

The dynamic keyword in C# and the CLR's dynamic dispatch feature can be a great tool for interfacing with external platforms easily and expressively. In my team we use it in our Data Access Layer to parse the result of SQL queries (which we perform using Dapper).

While reviewing DAL code I recently came across code like this (simplified):

dynamic result = connection.Query<dynamic>("SELECT SomeStringColumn, SomeEnumColumn ....");
string someString = result.SomeStringColumn.ToUpper();
if(Enum.TryParse<MyEnum>(result.SomeEnumColumn, false, out MyEnum parsed)){
            // do something with `parsed`
}
...

This demonstrates a good use case for dynamic, and is good code in general in my opinion. But it has some deficiencies. Let's just look at the 2nd line to demonstrate what I meant by dynamic is contagious.

We have a variable result with the type dynamic. (It's not strictly a type from the CLR's point of view, but more on that later.) The main thing to think about is that any expression that contains a dynamic variable (except for a type cast) will also be dynamic.

Consider this code:

public string A(){
  string staticString = "Some String";
  return staticString.ToUpper();
}

This is perfectly statically typed, and produces very efficient IL similar to this:

.method public hidebysig 
        instance string A () cil managed 
    {
        // Method begins at RVA 0x2076
        // Code size 11 (0xb)
        .maxstack 8

        IL_0000: ldstr "Some String"
        IL_0005: callvirt instance string [System.Private.CoreLib]System.String::ToUpper()
        IL_000a: ret
    } // end of method C::A

You can see how the ToUpper call is bound statically at compile time, it is right there in the IL itself.

Now if we change our code so that our input variable is dynamic:

public string B(){
  dynamic dynamicString = "Some String from dynamic";
  return dynamicString.ToUpper();
}

This means that the type of dynamicString is not known until runtime. Which then implies that we have no idea (in compile time) about what that .ToUpper() call will do exactly, it depends on the actual type of dynamicString after all. That means the compiler can no longer produce a single callvirt instruction, it has to emit code that dynamically figures out the right method to call and call it. (Similar to logic you are doing by hand when using Reflection.)

Involving dynamic dispatch to perform that single ToUpper call suddenly changes the IL representation of our innocent method to look like this: (Don't try to comprehend all of this, feel free to scroll trough it, I just wanted to illustrate to you the sheer amount of crap that gets compiled)

 .class nested private auto ansi abstract sealed beforefieldinit '<>o__3'
        extends [System.Private.CoreLib]System.Object
    {
        .custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
            01 00 00 00
        )
        // Fields
        .field public static class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> '<>p__0'
        .field public static class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>> '<>p__1'

    } // end of class <>o__3

.method public hidebysig 
        instance string B () cil managed 
    {
        // Method begins at RVA 0x2084
        // Code size 146 (0x92)
        .maxstack 11
        .locals init (
            [0] object
        )

        IL_0000: ldstr "Some String from dynamic"
        IL_0005: stloc.0
        IL_0006: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>> C/'<>o__3'::'<>p__1'
        IL_000b: brtrue.s IL_0031

        IL_000d: ldc.i4.0
        IL_000e: ldtoken [System.Private.CoreLib]System.String
        IL_0013: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
        IL_0018: ldtoken C
        IL_001d: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
        IL_0022: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::Convert(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, class [System.Private.CoreLib]System.Type, class [System.Private.CoreLib]System.Type)
        IL_0027: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
        IL_002c: stsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>> C/'<>o__3'::'<>p__1'

        IL_0031: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>> C/'<>o__3'::'<>p__1'
        IL_0036: ldfld !0 class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>>::Target
        IL_003b: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>> C/'<>o__3'::'<>p__1'
        IL_0040: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> C/'<>o__3'::'<>p__0'
        IL_0045: brtrue.s IL_0077

        IL_0047: ldc.i4.0
        IL_0048: ldstr "ToUpper"
        IL_004d: ldnull
        IL_004e: ldtoken C
        IL_0053: call class [System.Private.CoreLib]System.Type [System.Private.CoreLib]System.Type::GetTypeFromHandle(valuetype [System.Private.CoreLib]System.RuntimeTypeHandle)
        IL_0058: ldc.i4.1
        IL_0059: newarr [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo
        IL_005e: dup
        IL_005f: ldc.i4.0
        IL_0060: ldc.i4.0
        IL_0061: ldnull
        IL_0062: call class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo::Create(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags, string)
        IL_0067: stelem.ref
        IL_0068: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.Binder::InvokeMember(valuetype [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags, string, class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<class [System.Private.CoreLib]System.Type>, class [System.Private.CoreLib]System.Type, class [System.Private.CoreLib]System.Collections.Generic.IEnumerable`1<class [Microsoft.CSharp]Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo>)
        IL_006d: call class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<!0> class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>>::Create(class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSiteBinder)
        IL_0072: stsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> C/'<>o__3'::'<>p__0'

        IL_0077: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> C/'<>o__3'::'<>p__0'
        IL_007c: ldfld !0 class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>>::Target
        IL_0081: ldsfld class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite`1<class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>> C/'<>o__3'::'<>p__0'
        IL_0086: ldloc.0
        IL_0087: callvirt instance !2 class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, object>::Invoke(!0, !1)
        IL_008c: callvirt instance !2 class [System.Private.CoreLib]System.Func`3<class [System.Linq.Expressions]System.Runtime.CompilerServices.CallSite, object, string>::Invoke(!0, !1)
        IL_0091: ret
    } // end of method C::B

OK, enough scary IL, back to fixing our original code

Let's go back to this assignment: string someString = result.SomeStringColumn.ToUpper();. This code is equivalent to string someString = (string)((result.SomeStringColumn).ToUpper()); in its execution path.

If you remember result is dynamic, which means result.SomeStringColumn is a dynamic-dispatch call, which in turn means the type of this expression is still dynamic. Even though we know it really is a string we are executing the .ToUpper() call on a dynamic expression, making the entire expression dynamic. Finally we assign the result of this expression to a string variable, which implies a type cast. From then on, someString is a regular string variable.

Problems

There are multiple problems with using more dynamic operations than intended.

1. Performance

As we saw above, dynamic dispatch is expensive. I didn't do very thorough benchmarking on this one, but from a simple test it looks like that in this .ToUpper() example the dynamic code performs about 50% slower.

2. Loss of compile-time checking

It is really easy to introduce typos with dynamic code. Think what happened if I were to write return dynamicString.T0Upper();? It compiles just as fine! The compiler does not know anything about the operation until runtime, so everything that is syntactically correct will compile, and that can lead to problems later on.

3. No extension methods (and this can catch you)

One important thing to know about the runtime binder is that it will not bind to extension methods. Here is a SO question that goes into detail on that. (By the way, you know you asked a very good question when Jon Skeet and Eric Lippert both decide to answer it... 😉 Both answers are worth the read.)

The problem is that - for reasons above - it will not give you any warning. If you accidentally try to call an extension method on a dynamic expression you will find out the hard way (a.k.a. RuntimeBinderException).

4. Refactoring?

You are calling a method on some of your own types dynamically. You then decide to rename the method. Even though you use your IDE's Refactor/Rename function, it will not find the dynamic usage, and will not update that usage. (Which you will also not find out until it throws in runtime.) You get the idea.

Takeaway: Always make the cast to a concrete type as early as possible

All the problems above can be mitigated by casting the source dynamic to a string (or whatever type it is) either explicitly:

string someString = ((string)result.SomeStringColumn).ToUpper();

or implicitly:

string someStringColumnValue = result.SomeStringColumn;
string someString = someStringColumnValue.ToUpper();

Just be careful not to accidentally use var for your variable. var here would mean dynamic, which leads back to the problems.

Feedback

If you enjoyed this article, feel free to check out my Developer Blog where I share more stories like this.

If you have any feedback or wish to learn more about a topic with me in a 1:1 mentoring session, feel free to contact me anytime!

Discover and read more posts from Marcell Toth
get started
post commentsBe the first to share your opinion
Show more replies