Разверните статически типизированный объект в динамический объект
Я использую 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
в сочетании с размышлениями и повторением всех участников, но я хотел бы знать, есть ли более чистое / аккуратное решение.
2 ответа
В итоге я реализовал это так:
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 раза быстрее, чем использование отражения от моих тестов. В целом, это работает достаточно быстро, и накладные расходы для 10000 объектов практически не существуют.
Я должен отметить, что это не будет работать, чтобы расширить другие ExpandoObject
или аналогичные, но это не должно быть необходимым в любом случае.
Вы можете создать динамический объект, который объединяет два или более объектов:
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
типа int
, код viewModel.Id = "42";
будет успешным. Но это установит свойство на ExpandoObject
, Так что, если вы пытались получить viewModel.Id
после этого он вернет int
от product.Id
, который не был изменен.