26

I've read a lot about how ExpandoObject can be used to dynamically create objects from scratch by adding properties, but I haven't yet found how you do the same thing starting from a non-dynamic C# object that you already have.

For instance, I have this trivial class:

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string Telephone { get; set; }
}

I would like to convert this to ExpandoObject so that I can add or remove properties based on what it has already, rather than rebuilding the same thing from scratch. Is this possible?

Edit: the questions marked as duplicate are clearly NOT duplicates of this one.

Gigi
  • 28,163
  • 29
  • 106
  • 188
  • Maybe you could Create a Dictionary containing all properties from the Person-class and convert it to a dynamic object? https://stackoverflow.com/questions/15819720/dynamically-add-c-sharp-properties-at-runtime – Jompa234 Oct 26 '17 at 07:47
  • @PanagiotisKanavos `dynamic` "doesn't magically make your object expandable" https://stackoverflow.com/a/36558165 – Gigi Oct 26 '17 at 07:57
  • This is not practical. You could get somewhere if Person only had public fields, but properties are a no-go. You can't get the getter and setter invoked through ExpandoObject. It is really only a class that was made to work well with the dynamic binder, the part of the C# compiler that helps to translate statements like eo["foo"] = 42 into DLR calls. The kind of statement that is common in a dynamic language like Javascript or Python. Also evident from ExpandoObject having no public members other than the constructor. – Hans Passant Oct 26 '17 at 07:57
  • @PanagiotisKanavos what I stand to gain is irrelevant. I asked a direct question on whether this is possible, and don't see any reason to get into the philosophy of it. – Gigi Oct 26 '17 at 08:04
  • @HansPassant I don't need to have getter and setter invoked. As in a simple DTO, I just want the key (property name) and value to be copied into the ExpandoObject. – Gigi Oct 26 '17 at 08:06
  • @PanagiotisKanavos I can't make the assumption that I can inherit from DynamicObject. The class might not even be my own code. – Gigi Oct 26 '17 at 08:08
  • 2
    @PanagiotisKanavos with all due respect, please don't give me this "too broad" rubbish. My question couldn't be any more clear and can be answered very specifically. If you're confident about what you say, you can post an answer. But there's no reason to be pedantic about questions being asked, as this has become a big problem on Stack Overflow. – Gigi Oct 26 '17 at 08:16
  • Well, always best to let programmers shoot their own left foot. Use reflection to find the properties of Person, Type.GetProperties(). Create a `KeyValuePair` from PropertyInfo.Name and GetValue(). Cast the ExpandObject object to `IDictionary` so you can use its Add() method. – Hans Passant Oct 26 '17 at 08:17
  • Thanks @HansPassant. I am not a fan of dynamic and have a very specific use for this, but I'm curious to know any specific reasons why you think I'll regret this (other than the obvious consequences of dynamic typing). – Gigi Oct 26 '17 at 08:19
  • You got used to the automatic property syntax in C#, without yet having found the reason why it is useful. That tends to take a year or so after you've written the code, after which is will not be automatic anymore, ymmv. – Hans Passant Oct 26 '17 at 08:21
  • 1
    Correct, those others are not dublicates. And my searchresult suggested this one first. So welcome to the big downside of SO, with it's unreasonable downvotes. – oddbear Jan 25 '18 at 14:15

2 Answers2

39

It could be done like this:

var person = new Person { Id = 1, Name = "John Doe" };

var expando = new ExpandoObject();
var dictionary = (IDictionary<string, object>)expando;

foreach (var property in person.GetType().GetProperties())
    dictionary.Add(property.Name, property.GetValue(person));
Valerii
  • 2,147
  • 2
  • 13
  • 27
  • 9
    One thing I want to make clear about this solution, this is not making Person a expando object, it is copying the properties from Person in to a new expando object. If you do `expando.Id = 2` the value of `person.Id` will still be 1 – Scott Chamberlain Oct 26 '17 at 16:34
  • 1
    @Scott Chamberlain. Yes. You are right. – Valerii Oct 26 '17 at 16:36
  • 1
    My question did ask how to create an ExpandoObject based on the values of an existing object, so I think this is fine. – Gigi Oct 26 '17 at 18:40
  • 3
    Explanation: If you look at ExpandoObject, you can see it implements `IDictionary`. If you watch the object in runtime, you can see a `Results View`, which is the enumeration of that dictionary. So what is happening here is: You create an empty `ExpandoObject`, then take an up-casted instance of that object to manually manipulate its IDictionary Form. The `expando` object is being modified directly, even if it seems like you'd only be modifying a dictionary instance. In the end, you have manually added properties to the `expando` instance. – Suamere Feb 15 '18 at 21:39
9

You cannot "convert" a Person class into an expando object. However, you could create a wrapper DynamicObject that contains a Person and forwards all of the fields.

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace SandboxConsole
{
    public class ExpandoWrapper : DynamicObject
    {
        private readonly object _item;
        private readonly Dictionary<string, PropertyInfo> _lookup = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCulture);
        private readonly Dictionary<string, PropertyInfo> _ignoreCaseLookup = new Dictionary<string, PropertyInfo>(StringComparer.InvariantCultureIgnoreCase);

        private readonly Dictionary<string, Box> _lookupExtra = new Dictionary<string, Box>(StringComparer.InvariantCulture);
        private readonly Dictionary<string, Box> _ignoreCaseLookupExtra = new Dictionary<string, Box>(StringComparer.InvariantCultureIgnoreCase);

        private class Box
        {
            public Box(object item)
            {
                Item = item;
            }
            public object Item { get; }
        }

        public ExpandoWrapper(object item)
        {
            _item = item;
            var itemType = item.GetType();
            foreach (var propertyInfo in itemType.GetProperties())
            {
                _lookup.Add(propertyInfo.Name, propertyInfo);
                _ignoreCaseLookup.Add(propertyInfo.Name, propertyInfo);
            }
        }
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            result = null;
            PropertyInfo lookup;
            if (binder.IgnoreCase)
            {
                _ignoreCaseLookup.TryGetValue(binder.Name, out lookup);
            }
            else
            {
                _lookup.TryGetValue(binder.Name, out lookup);
            }

            if (lookup != null)
            {
                result = lookup.GetValue(_item);
                return true;
            }

            Box box;
            if (binder.IgnoreCase)
            {
                _ignoreCaseLookupExtra.TryGetValue(binder.Name, out box);
            }
            else
            {
                _lookupExtra.TryGetValue(binder.Name, out box);
            }

            if (box != null)
            {
                result = box.Item;
                return true;
            }

            return false;
        }

        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            PropertyInfo lookup;
            if (binder.IgnoreCase)
            {
                _ignoreCaseLookup.TryGetValue(binder.Name, out lookup);
            }
            else
            {
                _lookup.TryGetValue(binder.Name, out lookup);
            }

            if (lookup != null)
            {
                lookup.SetValue(_item, value);
                return true;
            }

            var box = new Box(value);
            _ignoreCaseLookupExtra[binder.Name] = box;
            _lookupExtra[binder.Name] = box;

            return true;
        }
    }
}

Example usage:

using System;

namespace SandboxConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            var person = new Person() {Id = 1};
            dynamic wrapper = new ExpandoWrapper(person);

            wrapper.Id = 2;
            wrapper.NewField = "Foo";

            Console.WriteLine(wrapper.Id);
            Console.WriteLine(person.Id);
            Console.WriteLine(wrapper.NewField);
        }
    }
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Address { get; set; }
        public string Telephone { get; set; }
    }
}
Pang
  • 9,564
  • 146
  • 81
  • 122
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431