Casting Dynamic Object and Passing Into UnitOfWork and Repository Pattern. Throws Exception
Asked Answered
H

2

7

This is a very specific problem. Not quite sure how to even word it. Basically I am implementing the unit of work and repository pattern, I have a dynamic object that I convert to an int, but if I use var it will throw an exception when trying to call the method.

I tried to remove all the trivial variables to this problem that I can. For some reason I only see it happen with these two design patterns. The exception I get is Additional information: 'BlackMagic.ITacoRepo' does not contain a definition for 'DoStuff'

Here is the code:

class BlackMagic
{
    static void Main(string[] args)
    {
        dynamic obj = new ExpandoObject();
        obj.I = 69;

        UnitOfWork uow = new UnitOfWork();

        int i1 = Convert.ToInt32(obj.I);
        var i2 = Convert.ToInt32(obj.I);

        if(i1.Equals(i2))
        {
            uow.TacoRepo.DoStuff(i1); // Works fine
            uow.TacoRepo.DoStuff(i2); // Throws Exception
        }
    }
}

class UnitOfWork
{
    public ITacoRepo TacoRepo { get; set; }

    public UnitOfWork()
    {
        TacoRepo = new TacoRepo();
    }
}

class Repo<T> : IRepo<T> where T : class
{
    public void DoStuff(int i)
    {
    }
}

interface IRepo<T> where T : class
{
    void DoStuff(int i);
}

class TacoRepo : Repo<Taco>, ITacoRepo
{
}

interface ITacoRepo : IRepo<Taco>
{
}

class Taco
{
}

EDIT: The main question I am trying to find an answer for, is why would the exception get thrown by calling DoStuff inside the unit of work (while using the repo) but not get thrown if DoStuff existed in the BlackMagic class.

Headmaster answered 21/10, 2016 at 19:46 Comment(27)
Can you toss in a breakpoint before DoStuff is called and see what actual runtime type i2 is?Philander
@EdPlunkett it tells me it is an Int32Headmaster
Yep, just tried it. The value in i2 is System.Int32. But I think it's behaving as if i2 is a dynamic reference to something else: If you pass a dynamic argument to a method, overload resolution is done at runtime. uow.TacoRepo.DoStuff((int)i2); works fine .Philander
This is happening because the runtime uses reflection in order to attempt to determine the type of i2. However, since i2 came from a ExpandoObject it's declaring and reflected type are null, and as a result it does not match the int requirement of DoStuff. Strange indeed, perhaps you should just avoid using dynamic here in that fashion.Titanium
Yeah, what he said!Philander
@TravisJ I gotcha. So one thing that I'm curious about. In the actual code that this problem comes from, I actually using the Newtonsoft Json library to convert a Json string to a dynamic object like: dynamic obj = JObject.Parse(strJson) so is it still the same reason for not working?Headmaster
To be honest, it is an interesting phenomenon and the reason I only posted that as a comment was in the hopes of someone who could expand on it or at least explain perhaps why the runtime didn't know that Convert.ToInt was an int type when using var there. As for your question with regards to parsing a json string into a dynamic object, I think that requires the same type of reflection as a model binder, and with dynamic that option doesn't seem to exist. Unsure though off the top of my head what the connection between this example shown and your issue with parse is wrt dynamic.Titanium
@TravisJ What is interesting too that I just noticed, is hovering over i2 in the debugger it says that it is a dynamic objectHeadmaster
var i2 = Int32.Parse(obj.I.ToString()) throws the same exception. var i2 = Int32.Parse($"{obj.I}"); doesn't. @TravisJ the declaring and reflected types of i1 are null as well here.Philander
Hmmmm.Philander
@EdPlunkett yeah, so like I said to Travis, in your example, if you hover over your former example, you'll see the debugger will say it is a dynamic object. If you hover over the latter it says it is an int. But at runtime GetType says they are both Int32... which is odd to meHeadmaster
If I understand Eric Lippert in my link above, it's all the compiler messing with you. GetType() is runtime. By the compiler's logic, var i2 means you're happy for i2 to be dynamic. What's weirding me is that according to my understanding of dynamic and overload resolution, it ought to be happy with DoStuff(int) -- so clearly my "understanding" isn't.Philander
@EdPlunkett I dont have a super high level of understanding of the compiler. But wouldn't when you have var i2 assigned the Convert.ToInt32() method, wouldn't that tell the compiler that i2 will be an int?Headmaster
I'd have forgiven you for expecting it to take the hint there, yeah. I'm dumbfounded myself. But it wants an explicit cast: var i2 = (int)Convert.ToInt32(obj.I); makes i2 non-dynamic. In fact, var i2 = (int)obj.I; makes i2 non-dynamic. Maybe more gobsmacked than dumbfounded, if you want to be pedantic.Philander
Here's another thing: If I give UnitOfWork a method public void Foo(int x) { }, I can call uow.Foo(i2) with dynamic i2. I don't understand how that method differs from ITacoRepo.DoStuff(int).Philander
@EdPlunkett When I do this: var i2 = Convert.ToInt32((object)obj.I); no more exception. I guess I don't fully understand why the parameters you pass in have any effect on what is returned. I mean Convert.ToInt32 always returns an int so I don't understand why it is dynamic insteadHeadmaster
@EdPlunkett Yeah it is really weird because I tested this problem in many different ways. But it only seemed to happen when I used the unit of work and repository pattern. I was not able to replicate it in any other way.Headmaster
While I am not sure of the why, I can at least tell you that the runtime binder is the who in the type determinations.Titanium
My impression is the compiler is looking at the parameter to that particular call to Convert.ToInt32(), and saying "OK, this is a dynamic parameter. And we're assigning the Int32 value this call returns to something declared var, so but a dynamic came in, so we'll go and make that var dynamic too." It's changing the meaning of that var declaration. The return value from Convert.ToInt32() isn't changing -- the compiler is changing what happens to it after return. Dump the MSIL and see what you get.Philander
@EdPlunkett I actually don't have any tools on this machine right now to get itHeadmaster
One thing I do want to note though, if I take DoStuff out of the Repo, and drop it into the BlackMagic class, the problem doesn't happen anymore. But for some reason using these design patterns, it throws the exception.Headmaster
Possible duplicate of Why does a method invocation expression have type dynamic even when there is only one possible return type?Titanium
So I voted to close this as a duplicate. Let me explain why. The reason that int is not being used as the type is because the rule for using implicit conversion on the return type of the method call (in this case ToInt) has several conditions, one of which is violated in this situation, notably that "The primary-expression has compile-time type dynamic." As a result the implicit conversion is not used, and the best accessible type (object) is used as a result. There is no definition for DoStuff(object) and as a result the runtime exception occurs.Titanium
@TravisJ But why is i2 an acceptable argument for other methods that take int? (Never mind, just saw Sunshine's answer. But this is not a dupe of that other question -- it's a combination of that one, and the surprising runtime binding behavior)Philander
@TravisJ I agree with Ed on this. You make a good point about the dynamic object conversion, but the error doesn't get thrown if I take DoStuff out of the Repo and put it in the main `BlackMagic' class then pass i2 inHeadmaster
@TravisJ I do agree that the link you posted does answer our discussion about the dynamic object conversion. But I am not sure if it completely answers the main question of: Why is the exception thrown. I think that Sunshine's answer is on the right path, but not sure if it completely answers it.Headmaster
it's weird but can you check what is the type of i2 at runtime if it turn out to be anything other than Int32 then that's the reason - note that the compiler doesn't check dynamic type at compile timeMindamindanao
A
3

This is one of the bugs I reported to Microsoft more than 5 years ago, soon after the dynamic was introduced. As far as I know, it is considered of a very low priority on their list, and might never be fixed.

Here are simple repro steps:

using System.Collections;

class C
{
    static void Main()
    {
        object[] array = { };
        IList list = new ArrayList();
        list.CopyTo(array, 0); // Works okay
        dynamic index = 0;
        list.CopyTo(array, index); // Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'System.Collections.IList' does not contain a definition for 'CopyTo'
    }
}

Here is an explanation of the problem. When a function member (a method or an indexer) is invoked on an expression whose static type is an interface type, and at least one of the arguments to the invocation is of the type dynamic (which means the complete member lookup -- type inference -- overload resolution process is postponed until runtime, and becomes a responsibility of the runtime binder rather than the compiler; only a partial set of checks is performed by the compiler based on incomplete type information), and the member being invoked is inherited by the interface from one of its base interfaces (rather than declared in the interface itself), then the runtime binder fails to properly traverse the tree of the base interfaces to find the inherited member, and throws an exception at runtime, reporting that the required member is not found. Note that it is only the runtime binder's fault -- the compiler properly accepted the invocation (but would reject it, if, for example, you made a typo in the method name).

A possible workaround: cast the expression you invoke a member on to the base interface that actually declares the member you are trying to invoke. For example, the program from the repro steps above could be fixed as follows:

using System.Collections;

class C
{
    static void Main()
    {
        object[] array = { };
        IList list = new ArrayList();
        list.CopyTo(array, 0); // Works okay
        dynamic index = 0;
        ((ICollection) list).CopyTo(array, index); // Works okay
    }
}

Or, if possible, get rid of the dynamic dispatch completely by casting the argument(s) of type dynamic to the type specified in the invoked member's signature.

using System.Collections;

class C
{
    static void Main()
    {
        object[] array = { };
        IList list = new ArrayList();
        list.CopyTo(array, 0); // Works okay
        dynamic index = 0;
        list.CopyTo(array, (int) index); // Works okay
    }
}

Unfortunately, both workarounds might be not helpful if you really want overload resolution to happen at runtime, and among the possible candidates there are both members declared by the interface, and members inherited by it. You would probably need to invent some ad hoc solution in that case, or significantly refactor your program.

Almanac answered 28/10, 2016 at 3:27 Comment(4)
So essentially you're saying, that since the interface ITacoRepo doesn't explicitly implement the interface IRepo<Taco>'s DoStuff method, it doesn't know where else to look? That it is simply just looking at ITacoRepo? So technically a problem in interfaces implementing interfaces when passing dynamics in?Headmaster
Interfaces cannot implement other interfaces, they can only inherit from them. They can re-declare inherited abstract members, but that does not constitute implementations. Of course, a variable of an interface type at runtime (if not null) will contain a reference to an object of some concrete type that has implementations of all members of the interface, both declared (or re-declared) in it, and inherited. But overload resolution must occur based on the statically known type of the receiver, that is, on the interface type. The runtime binder fails to properly collect inherited candidates.Almanac
At runtime, though, when you call GetType on i2, it tells you that it is an Int32. Why would it still cause the same problem? I see why it would if you kept it as a dynamic, but if the problem happens at runtime, I don't understand why it wouldnt accept an intHeadmaster
The variable i2 is declared as var that means its compile-time type is inferred from its initializer. Its initializer contains a sub-expression obj of type dynamic, so the type of the initializer is also dynamic, and so is the type of i2 (exactly as if you explicitly declared it as dynamic). It means that any method invocation using i2 as an argument has to be resolved dynamically at runtime, using the runtime type of the object it refers to for applicability checks and overload resolution. The bug in the runtime binder prevents it from finding an applicable inherited method.Almanac
A
2

It looks like the RuntimeBinder doesn't traverse the inheritance hierarchy so it only looks in the immediate interface ITacoRepo for a definition of DoStuff.

If you make the the UnitOfWork use IRepo<Taco> instead of ITacoRepo, it is able to find the method definition.

Afterthought answered 21/10, 2016 at 20:53 Comment(5)
Why would this be true of the var i2 and not of the int i1?Titanium
@TravisJ Because RuntimeBinder isn't involved in the call with i1. That's all compile time.Philander
So then why if I created a local IRepo<Taco> r = new TacoRepo() and then call r.DoStuff(i2) would I not get the error?Headmaster
@THEStephenStanton What happens if you define r as ITacoRepo, like the definition in UnitOfWork? I think the problem is the type you're using, not where the variable is.Euromarket
@KendallFrey So you raise an interesting point that I tried (if I understand you right). If I have just have TacoRepo and ITacoRepo, and I move the signature DoStuff to ITacoRepo, then implement it in 'TacoRepo', and just do 'ITacoRepo r = new TacoRepo()' then do 'r.DoStuff(i2)', there is no exception. Which is why it is so weird that it only happens when I use this design patternHeadmaster

© 2022 - 2024 — McMap. All rights reserved.