0

I was looking at the question Use 'dynamic' throw a RuntimeBinderException. I face a similar problem:

Basically, I want to create a "HTML helper" in ASP.NET MVC that uses dynamic arguments, akin to the htmlArguments parameter for many of the existing helpers (more code below):

public BootstrapCell(Action<string> emitContentAction, dynamic args)

View:

@using (grid.Cell(ViewContext.Writer.Write, new {Position = 4}))
{
     <p>zomg!</p>
}

However in the naive approach, i get RuntimeBinderException thrown at me, declaring that 'object' does not contain a definition for 'Position', even though when debugging and hovering over the _args variable, it clearly does have a Position property.

The caller and the callee are in separate assemblies. Why is that problem happening?

(The solution to that has been shown in the same question: Manually create an ExpandoObject to hold the args.)

Implementation:

public class Cell
{
    private readonly string _tagName;
    private dynamic _args;
    private Action<string> EmitContentAction;

    public BootstrapCell(Action<string> emitContentAction, dynamic args) : DisposableBaseClass
    {
        _args = args;
        EmitContentAction = emitContentAction;
        OnContextEnter();
    }

    protected void OnContextEnter()
    {
        var sb = new StringBuilder("<");
        sb.Append(_tagName);

        if (_args.Position > 0)
        {
            sb.Append(" class=\"offset");
            sb.Append(args.Position);
            sb.Append("\"");
        }

        sb.Append(">");

        EmitContentAction(sb.ToString());
    }
}

[Edited to make clearer that my problem arises when "obviously" the Position property is set. I am aware that if the property never was defined in the first place, an exception must be raised.]

Community
  • 1
  • 1
Cornelius
  • 830
  • 11
  • 31

1 Answers1

3

That code is fatally flawed.

It does work, as long as you specify that property:

void Bar()
{
    Foo(new {Position = 0});
}

void Foo(dynamic args)
{
    Console.WriteLine(args.Position);
}

That will output 0, it will not throw a RuntimeBinderException.

But the purpose of such code is the possibility for the caller to specify only the properties needed and omit the rest.
You are trying to check for this omission via if(args.Position != null). But that doesn't work, it already requires Position to exist.

When you have a look at the routing API of ASP.NET that also supports those anonymous configuration objects you will notice that the type of the parameter is object and not dynamic.
Using object instead of dynamic will enable your API to be used across assembly boundaries.

So how does it work?

Just like in the linked answer, you need to manually create a dictionary of the properties. Whether you use a plain old Dictionary<string, object> or an ExpandoObject is a matter of preference.
Using ExpandoObject will make your code a bit simpler to read and write, but it is not required.


About the actual exception you are getting:
Please note that it tells you it can't find the Position property on object. If it would be an anonymous type that was missing the Position property the exception message wouldn't refer to object but to an anonymous type. Something like this:

'<>f__AnonymousType0' does not contain a definition for 'Position'

Daniel Hilgarth
  • 171,043
  • 40
  • 335
  • 443
  • Yes, I am aware the "Postion" property has to exist. And as said, it "did exist" in so far as that having args.ToString() in the watch window showed me "Position = 4, Size = 6", but the code still threw the exception. – Cornelius Feb 06 '13 at 11:41
  • @ClearsTheScreen: It shouldn't do that, it certainly doesn't do it for me. Please try the code I posted - does it throw? If not, try to make it throw by changing it to your code - one change at a time. – Daniel Hilgarth Feb 06 '13 at 11:44
  • I thought I did just that, but I obviously should not debug in the evening. Thank you for the nudge in the right direcion. +1 :) – Cornelius Feb 06 '13 at 11:50
  • @ClearsTheScreen: BTW: Your exception tells me that the code you showed here isn't the one that throws the exception. The call would have to look like this to produce the exception: `grid.Cell(ViewContext.Writer.Write, new object())`. I am saying that, because the exception wouldn't refer to `object` but to an anonymous type otherwise: `'<>f__AnonymousType0' does not contain a definition for 'Position'` – Daniel Hilgarth Feb 06 '13 at 11:52
  • That is precisely the problem. I called across assembly boundaries. In retrospect: I was stupid. And I never realized that it should not show "object"; thank you for that pointer. – Cornelius Feb 06 '13 at 11:54
  • I would like to give credit for you; would you want to rephrase your answer for me to accept? – Cornelius Feb 06 '13 at 11:55
  • @ClearsTheScreen: Thanks for the update. I was wondering how that exception came to be. Because I couldn't believe that you simply wrote `new object()` somewhere and forgot about it ;-) – Daniel Hilgarth Feb 06 '13 at 11:56
  • @ClearsTheScreen: In what way do you want me to rephrase it? I added the part with the exception to the answer. – Daniel Hilgarth Feb 06 '13 at 11:57
  • 1
    My problem was simply that I used `dynamic` objects across assembly boundaries. So the answer to "why does this happen" is "because you aren't in the same assembly". Your answer led me to find that problem, so I want to credit you; but what you wrote is not actually the solution to the problem I had. – Cornelius Feb 06 '13 at 12:06
  • @ClearsTheScreen: I couldn't have possibly known that you are using this code across assembly boundaries, so I can't change my answer to this. Furthermore, I didn't even know about that problem :) So you might want to accept your own answer. But: I told you that other APIs use `object` instead of `dynamic` - and that's the solution to the problem. – Daniel Hilgarth Feb 06 '13 at 12:08