Использование LINQ для динамического отображения (или построения проекций)

Я знаю, что могу сопоставить два типа объектов с помощью LINQ, используя проекцию так:

var destModel = from m in sourceModel
               select new DestModelType {A = m.A, C = m.C, E = m.E}

куда

class SourceModelType
{
    string A {get; set;}
    string B {get; set;}
    string C {get; set;}
    string D {get; set;}
    string E {get; set;}
}

class DestModelType
{
    string A {get; set;}
    string C {get; set;}
    string E {get; set;}
}

Но что, если я хочу сделать что-то вроде универсального для этого, где я не знаю конкретно два типа, с которыми имею дело. Таким образом, он будет ходить по типу «Dest» и соответствовать соответствующим типам «Source». Возможно ли это? Кроме того, чтобы добиться отложенного выполнения, я бы хотел, чтобы он просто возвращал IQueryable.

Например:

public IQueryable<TDest> ProjectionMap<TSource, TDest>(IQueryable<TSource> sourceModel)
{
   // dynamically build the LINQ projection based on the properties in TDest

   // return the IQueryable containing the constructed projection
}

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


person CodeGrue    schedule 28.05.2010    source источник


Ответы (1)


Вы должны сгенерировать дерево выражений, но простое, так что это не так сложно...

void Main()
{
    var source = new[]
    {
        new SourceModelType { A = "hello", B = "world", C = "foo", D = "bar", E = "Baz" },
        new SourceModelType { A = "The", B = "answer", C = "is", D = "42", E = "!" }
    };

    var dest = ProjectionMap<SourceModelType, DestModelType>(source.AsQueryable());
    dest.Dump();
}

public static IQueryable<TDest> ProjectionMap<TSource, TDest>(IQueryable<TSource> sourceModel)
    where TDest : new()
{
    var sourceProperties = typeof(TSource).GetProperties().Where(p => p.CanRead);
    var destProperties =   typeof(TDest).GetProperties().Where(p => p.CanWrite);
    var propertyMap = from d in destProperties
                      join s in sourceProperties on new { d.Name, d.PropertyType } equals new { s.Name, s.PropertyType }
                      select new { Source = s, Dest = d };
    var itemParam = Expression.Parameter(typeof(TSource), "item");
    var memberBindings = propertyMap.Select(p => (MemberBinding)Expression.Bind(p.Dest, Expression.Property(itemParam, p.Source)));
    var newExpression = Expression.New(typeof(TDest));
    var memberInitExpression = Expression.MemberInit(newExpression, memberBindings);
    var projection = Expression.Lambda<Func<TSource, TDest>>(memberInitExpression, itemParam);
    projection.Dump();
    return sourceModel.Select(projection);
}

(проверено в LinqPad, поэтому Dumps)

Сгенерированное выражение проекции выглядит так:

item => new DestModelType() {A = item.A, C = item.C, E = item.E}
person Thomas Levesque    schedule 28.05.2010
comment
Спасибо за это решение. Я глубоко понимаю, как это работает. Если бы я хотел, чтобы он углублялся в сложные объекты, мне пришлось бы изменить propertyMap, верно? - person CodeGrue; 28.05.2010
comment
Если вы хотите понять, как строится выражение, я предлагаю вам использовать LinqPad; это позволяет вам легко проверять каждый узел выражения. Что касается вашего вопроса, я не уверен, что понимаю, что вы имеете в виду... если вы знаете только типы источника и назначения, вы не можете сделать ничего более сложного, чем копирование свойств с тем же именем. - person Thomas Levesque; 28.05.2010
comment
Что, если вы хотите включить сложные объекты, чтобы item => new DestModelType() {A = item.A.X, C = item.C, E = item.E}. Это может быть через атрибут свойства, указывающий, что сопоставлять. - person CodeGrue; 28.05.2010
comment
Я думаю, что это будет намного сложнее, но все же выполнимо... вам придется изменить propertyMap и memberBindings в соответствии с вашей собственной логикой. - person Thomas Levesque; 28.05.2010