How can I use collection initializer syntax with ExpandoObject?

徘徊边缘 提交于 2019-12-03 05:17:35

I've had the need for a simple ExpandoObject initializer several times before and typically use the following two extension methods to accomplish something like initializer syntax:

public static KeyValuePair<string, object> WithValue(this string key, object value)
{
    return new KeyValuePair<string, object>(key, value);
}

public static ExpandoObject Init(
    this ExpandoObject expando, params KeyValuePair<string, object>[] values)
{
    foreach(KeyValuePair<string, object> kvp in values)
    {
        ((IDictionary<string, Object>)expando)[kvp.Key] = kvp.Value;
    }
    return expando;
}

Then you can write the following:

dynamic foo = new ExpandoObject().Init(
    "A".WithValue(true),
    "B".WithValue("Bar"));

In general I've found that having an extension method to build KeyValuePair<string, object> instances from a string key comes in handy. You can obviously change the name to something like Is so that you can write "Key".Is("Value") if you need the syntax to be even more terse.

The language specification (7.5.10.3 on Collection Initializers) is a bit vague on this point as far as I can tell. It says

For each specified element in order, the collection initializer invokes an Add method on the target object with the expression list of the element initializer as argument list, applying normal overload resolution for each invocation. Thus, the collection object must contain an applicable Add method for each element initializer.

Unfortunately the text doesn't go into details about what an applicable Add method is, but it seems that explicitly implemented interface methods don't fit the bill as they are essentially considered private (see 13.4.1):

It is not possible to access an explicit interface member implementation through its fully qualified name in a method invocation, property access, or indexer access. An explicit interface member implementation can only be accessed through an interface instance, and is in that case referenced simply by its member name.

...

Explicit interface member implementations have different accessibility characteristics than other members. Because explicit interface member implementations are never accessible through their fully qualified name in a method invocation or a property access, they are in a sense private. However, since they can be accessed through an interface instance, they are in a sense also public.

The opensource framework Dynamitey has an alternative syntax for building ExpandoObject instances inline.

    dynamic obj = Builder.New<ExpandoObject>(
        foo:"hello",
        bar: 42 ,
        baz: new object()
    );

    int value = obj.bar;

It also has a dictionary based dynamic prototype object Dynamitey.DynamicObjects.Dictionary such that

    dynamic obj = new Dynamitey.DynamicObjects.Dictionary()
    {
        { "foo", "hello" },
        { "bar", 42 },
        { "baz", new object() }
    };

    int value = obj.bar;

works too.

First of all, you are spot on. The IDictionary<string,object> has been implemented explicitly.

You do not even need casting. This works:

IDictionary<string,object> exdict = new ExpandoObject() 

Now the reason collection syntax does not work is because that is an implementation in the Dictionary<T,T> constructor and not part of the interface hence it will not work for expando.

Wrong statement above. You are right, it uses add function:

static void Main(string[] args)
{
Dictionary<string,object> dictionary = new Dictionary<string, object>()
                                                {
                                                    {"Ali", "Ostad"}
                                                };
}

Gets compiled to

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       27 (0x1b)
  .maxstack  3
  .locals init ([0] class [mscorlib]System.Collections.Generic.Dictionary`2<string,object> dictionary,
           [1] class [mscorlib]System.Collections.Generic.Dictionary`2<string,object> '<>g__initLocal0')
  IL_0000:  nop
  IL_0001:  newobj     instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,object>::.ctor()
  IL_0006:  stloc.1
  IL_0007:  ldloc.1
  IL_0008:  ldstr      "Ali"
  IL_000d:  ldstr      "Ostad"
  IL_0012:  callvirt   instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string,object>::Add(!0,
                                                                                                                 !1)
  IL_0017:  nop
  IL_0018:  ldloc.1
  IL_0019:  stloc.0
  IL_001a:  ret
} // end of method Program::Main

UPDATE

The main reason is Add has been implemented as protected (with no modifier which becomes protected).

Since Add is not visible on ExpandoObject, it cannot be called as above.

orad

Using the idea from @samedave, and adding reflection, I got this:

void Main()
{
    dynamic foo = new ExpandoObject().Init(new
    {
        A = true,
        B = "Bar"
    });

    Console.WriteLine(foo.A);
    Console.WriteLine(foo.B);
}

public static class ExtensionMethods
{
    public static ExpandoObject Init(this ExpandoObject expando, dynamic obj)
    {
        var expandoDic = (IDictionary<string, object>)expando;
        foreach (System.Reflection.PropertyInfo fi in obj.GetType().GetProperties())
        {
           expandoDic[fi.Name] = fi.GetValue(obj, null);
        }
        return expando;
    }
}

But it would be nicer to be able to just do it like:

dynamic foo = new ExpandoObject
{
    A = true,
    B = "Bar"
});

Please vote for this feature in Visual Studio UserVoice.

Update:

Using Rick Strahl's Expando implementation you should be able to do something like this:

dynamic baz = new Expando(new
{
    A = false,
    B = "Bar2"
});

Console.WriteLine(baz.A);
Console.WriteLine(baz.B);

It's a shame that adding dynamic properties (whose name is known only at run-time) to ExpandoObject is not as easy as it should have been. All the casting to dictionary is plain ugly. Never mind you could always write a custom DynamicObject that implements Add which helps you with neat object initializer like syntax.

A rough example:

public sealed class Expando : DynamicObject, IDictionary<string, object>
{
    readonly Dictionary<string, object> _properties = new Dictionary<string, object>();

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        return _properties.TryGetValue(binder.Name, out result);
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (binder.Name == "Add")
        {
            var del = value as Delegate;
            if (del != null && del.Method.ReturnType == typeof(void))
            {
                var parameters = del.Method.GetParameters();
                if (parameters.Count() == 2 && parameters.First().ParameterType == typeof(string))
                    throw new RuntimeBinderException("Method signature cannot be 'void Add(string, ?)'");
            }
        }
        _properties[binder.Name] = value;
        return true;
    }



    object IDictionary<string, object>.this[string key]
    {
        get
        {
            return _properties[key];
        }
        set
        {
            _properties[key] = value;
        }
    }

    int ICollection<KeyValuePair<string, object>>.Count
    {
        get { return _properties.Count; }
    }

    bool ICollection<KeyValuePair<string, object>>.IsReadOnly
    {
        get { return false; }
    }

    ICollection<string> IDictionary<string, object>.Keys
    {
        get { return _properties.Keys; }
    }

    ICollection<object> IDictionary<string, object>.Values
    {
        get { return _properties.Values; }
    }



    public void Add(string key, object value)
    {
        _properties.Add(key, value);
    }

    bool IDictionary<string, object>.ContainsKey(string key)
    {
        return _properties.ContainsKey(key);
    }

    bool IDictionary<string, object>.Remove(string key)
    {
        return _properties.Remove(key);
    }

    bool IDictionary<string, object>.TryGetValue(string key, out object value)
    {
        return _properties.TryGetValue(key, out value);
    }

    void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
    {
        ((ICollection<KeyValuePair<string, object>>)_properties).Add(item);
    }

    void ICollection<KeyValuePair<string, object>>.Clear()
    {
        _properties.Clear();
    }

    bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
    {
        return ((ICollection<KeyValuePair<string, object>>)_properties).Contains(item);
    }

    void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
    {
        ((ICollection<KeyValuePair<string, object>>)_properties).CopyTo(array, arrayIndex);
    }

    bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
    {
        return ((ICollection<KeyValuePair<string, object>>)_properties).Remove(item);
    }

    IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
    {
        return _properties.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)_properties).GetEnumerator();
    }
}

And you could call like you wanted:

dynamic obj = new Expando()
{
    { "foo", "hello" },
    { "bar", 42 },
    { "baz", new object() }
};

int value = obj.bar;

The caveat with this approach is that you cannot add Add "method" with the same signature as Dictionary.Add to your expando object since it is already a valid member of the Expando class (which was required for the collection initializer syntax). The code throws an exception if you do

obj.Add = 1; // runs
obj.Add = new Action<string, object>(....); // throws, same signature
obj.Add = new Action<string, int>(....); // throws, same signature for expando class
obj.Add = new Action<string, object, object>(....); // runs, different signature
obj.Add = new Func<string, object, int>(....); // runs, different signature

If property names needn't be truly dynamic then another alternative is to have a ToDynamic extension method so that you can initialize in-line.

public static dynamic ToDynamic(this object item)
{
    var expando = new ExpandoObject() as IDictionary<string, object>;
    foreach (var propertyInfo in item.GetType().GetProperties())
        expando[propertyInfo.Name] = propertyInfo.GetValue(item, null);

    return expando;
}

So you can call:

var obj = new { foo = "hello", bar = 42, baz = new object() }.ToDynamic();

int value = obj.bar;

There are a hundred ways you can design an API for this, another one such (mentioned in orad's answer) is:

dynamic obj = new Expando(new { foo = "hello", bar = 42, baz = new object() });

Will be trivial to implement.


Side note: there is always anonymous types if you know the property names statically and you dont want to add further after initialization.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!