3

I am using dynamic objects for my view models, as I find the overhead from using something like Automapper unnecessary and find this approach a lot more flexible and lightweight. I am using the builder from impromptu-interface like this:

private dynamic New = Builder.New();

private dynamic GetViewModel(Product p)
{
    var viewModel = New.Product( id : p.Id, name : p.Name );
    viewModel.AdditionalProperty = "some additional data";
    return viewModel;
}

There are a few scenarios where "expanding" the actual object would be better then remapping all the properties one by one, similar to how you would do in JavaScript using jQuery.extend()

private dynamic GetViewModel(Product p)
{
    var viewModel = //create base dynamic object, that has all the members of p.
    viewModel.AdditionalProperty = "some additional data";
    return viewModel;
}

This should be achievable using ExpandoObject combined with reflection and iterating through all the members, but I would like to know if there's a cleaner/neater solution.

Patrick
  • 1,717
  • 7
  • 21
  • 28
Can Gencer
  • 8,822
  • 5
  • 33
  • 52
  • 1
    I most likely wouldn't base a decision like this solely on performance, but are you sure `dynamic` is faster than using Automapper? – svick Oct 10 '11 at 01:01
  • The issue is not really about performance, just more lightweight and more flexible. There are never more than a hundred viewmodels created at a time or so. – Can Gencer Oct 10 '11 at 07:35

2 Answers2

5

I ended up implementing it like this:

public class ExpandedObject : DynamicObject
{
    private readonly IDictionary<string, object> expando = new ExpandoObject();

    public ExpandedObject(object o)
    {            
        foreach (var propertyInfo in o.GetType().GetProperties(BindingFlags.Public|BindingFlags.Instance))
        {
            this.expando[propertyInfo.Name] = Impromptu.InvokeGet(o, propertyInfo.Name);
        }
    }

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

    public override bool  TrySetMember(SetMemberBinder binder, object value)
    {
        this.expando[binder.Name] = value;
        return true;
    }
}

and the tests:

[TestFixture]
public class ExpandedObjectTest
{
    [Test]
    public void Can_add_new_properties_to_expanded_object()
    {
        dynamic expanded = new ExpandedObject(new object());
        var data = "some additional data";
        expanded.data = data;
        Assert.AreEqual(data, expanded.data);
    }

    [Test]
    public void Copies_existing_properties()
    {            
        var obj = new { id = 5 };            
        dynamic expanded = new ExpandedObject(obj);            
        Assert.AreEqual(obj.id, expanded.id);            
    }
}

This makes use of Impromptu.InvokeGet() instead of PropertyInfo.GetValue() because Impromptu.InvokeGet() uses the DLR and as such about 2.5x faster than using than reflection from my tests. Overall this works reasonably fast and the overhead for upto 10,000 objects is almost nonexistant.

I should note that this won't work to expand other ExpandoObject or similar, but this should not really be necessary anyway.

Can Gencer
  • 8,822
  • 5
  • 33
  • 52
1

You could create dynamic object that combines two or more objects:

class CombineDynamic : DynamicObject
{
    private readonly object[] m_objects;

    public CombineDynamic(params object[] objects)
    {
        m_objects = objects;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var callSite = CallSite<Func<CallSite, object, object>>.Create(binder);

        foreach (var o in m_objects)
        {
            try
            {
                result = callSite.Target(callSite, o);
                return true;
            }
            catch (RuntimeBinderException)
            {}
        }

        return base.TryGetMember(binder, out result);
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        // the binder from argument uses compile time type from call site,
        // which is object here; because of that, setting of properties that 
        // aren't of type object wouldn't work if we used that binder directly
        var fixedBinder = Binder.SetMember(
            CSharpBinderFlags.None, binder.Name, typeof(CombineDynamic),
            new[]
            {
                CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
                CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
            });

        var callSite =
            CallSite<Action<CallSite, object, object>>.Create(fixedBinder);

        foreach (var o in m_objects)
        {
            try
            {
                callSite.Target(callSite, o, value);
                return true;
            }
            catch (RuntimeBinderException)
            {}
        }

        return base.TrySetMember(binder, value);
    }
}

And use it like this:

dynamic viewModel = new CombineDynamic(product, new ExpandoObject());
viewModel.AdditionalProperty = "additional data";

When you get or set a property dynamically, it first tries to do that on the first object, then on the second etc., until it succeeds.

Doing it like this has (at least) one weird behavior: If, for example, Product had property Id of type int, the code viewModel.Id = "42"; would succeed. But it would set the property on the ExpandoObject. So if you tried to retrieve viewModel.Id after that, it would return the int from product.Id, which wasn't modified.

svick
  • 236,525
  • 50
  • 385
  • 514
  • this more working like a proxy though, not exactly what I want. But it should point me in the right direction :) – Can Gencer Oct 10 '11 at 17:30
  • Yeah, you could say it's a proxy. What don't you like about that? – svick Oct 10 '11 at 17:56
  • I would rather that it does a shallow copy into another object. But it should be easy to implement using reflection over public properties. – Can Gencer Oct 10 '11 at 18:24
  • Yeah, you could do it that way. But why? It seems unnecessarily complicated to me. – svick Oct 10 '11 at 18:26