Развернуть статически типизированный объект в динамический объект

#c# #dynamic #impromptu-interface

#c# #динамический #импровизированный интерфейс

Вопрос:

Я использую dynamic объекты для своих моделей представления, поскольку нахожу ненужными накладные расходы, связанные с использованием чего-то вроде Automapper, и нахожу этот подход намного более гибким и легким. Я использую конструктор из импровизированного интерфейса следующим образом:

 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;
}
  

Существует несколько сценариев, в которых «расширение» фактического объекта было бы лучше, чем переназначение всех свойств одно за другим, аналогично тому, как вы это делаете в JavaScript с помощью 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;
}
  

Это должно быть достижимо в ExpandoObject сочетании с отражением и перебором всех элементов, но я хотел бы знать, есть ли более чистое / аккуратное решение.

Комментарии:

1. Я, скорее всего, не стал бы основывать подобное решение исключительно на производительности, но вы уверены dynamic , что это быстрее, чем использование Automapper?

2. Проблема на самом деле не в производительности, а в более легком и гибком. Одновременно никогда не создается более сотни viewmodels или около того.

Ответ №1:

В итоге я реализовал его следующим образом:

 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;
    }
}
  

и тесты:

 [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);            
    }
}
  

Это использует Impromptu.InvokeGet() вместо PropertyInfo.GetValue() , потому Impromptu.InvokeGet() что использует DLR и, как таковой, примерно в 2,5 раза быстрее, чем использование отражения от моих тестов. В целом это работает достаточно быстро, и накладные расходы для 10 000 объектов практически отсутствуют.

Я должен отметить, что это не сработает для расширения other ExpandoObject или подобного, но в любом случае это не должно быть необходимо.

Ответ №2:

Вы можете создать динамический объект, который объединяет два или более объектов:

 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);
    }
}
  

И использовать его следующим образом:

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

Когда вы получаете или устанавливаете свойство динамически, оно сначала пытается сделать это для первого объекта, затем для второго и т.д., пока не завершится успешно.

Выполнение этого подобным образом имеет (по крайней мере) одно странное поведение: если бы, например, Product было свойство Id type int , код viewModel.Id = "42"; был бы успешным. Но это установило бы свойство на ExpandoObject . Поэтому, если вы попытаетесь получить viewModel.Id после этого, он вернет int from product.Id , который не был изменен.

Комментарии:

1. хотя это больше работает как прокси, не совсем то, что я хочу. Но это должно указать мне правильное направление 🙂

2. Да, можно сказать, что это прокси. Что вам в этом не нравится?

3. Я бы предпочел, чтобы он выполнял неглубокую копию в другой объект. Но это должно быть легко реализовать, используя отражение над общедоступными свойствами.

4. Да, вы могли бы сделать это таким образом. Но почему? Мне это кажется излишне сложным.