Как создать дерево выражений LINQ для выбора анонимного типа

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

var v = from c in Countries
        where c.City == "London"
        select new {c.Name, c.Population};

Я придумал, как создавать

var v = from c in Countries
        where c.City == "London"
        select new {c.Name};

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


person Tom Deloford    schedule 03.03.2009    source источник


Ответы (9)


Как уже упоминалось, это можно сделать с помощью Reflection Emit и вспомогательного класса, который я включил ниже. Код ниже находится в стадии разработки, так что принимайте его как следует ... «он работает на моем ящике». Класс метода SelectDynamic должен быть помещен в класс метода статического расширения.

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

public static IQueryable SelectDynamic(this IQueryable source, IEnumerable<string> fieldNames)
{
    Dictionary<string, PropertyInfo> sourceProperties = fieldNames.ToDictionary(name => name, name => source.ElementType.GetProperty(name));
    Type dynamicType = LinqRuntimeTypeBuilder.GetDynamicType(sourceProperties.Values);

    ParameterExpression sourceItem = Expression.Parameter(source.ElementType, "t");
    IEnumerable<MemberBinding> bindings = dynamicType.GetFields().Select(p => Expression.Bind(p, Expression.Property(sourceItem, sourceProperties[p.Name]))).OfType<MemberBinding>();

    Expression selector = Expression.Lambda(Expression.MemberInit(
        Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), bindings), sourceItem);

    return source.Provider.CreateQuery(Expression.Call(typeof(Queryable), "Select", new Type[] { source.ElementType, dynamicType },
                 Expression.Constant(source), selector));
}



public static class LinqRuntimeTypeBuilder
{
    private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    private static AssemblyName assemblyName = new AssemblyName() { Name = "DynamicLinqTypes" };
    private static ModuleBuilder moduleBuilder = null;
    private static Dictionary<string, Type> builtTypes = new Dictionary<string, Type>();

    static LinqRuntimeTypeBuilder()
    {
        moduleBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run).DefineDynamicModule(assemblyName.Name);
    }

    private static string GetTypeKey(Dictionary<string, Type> fields)
    {
        //TODO: optimize the type caching -- if fields are simply reordered, that doesn't mean that they're actually different types, so this needs to be smarter
        string key = string.Empty;
        foreach (var field in fields)
            key += field.Key + ";" + field.Value.Name + ";";

        return key;
    }

    public static Type GetDynamicType(Dictionary<string, Type> fields)
    {
        if (null == fields)
            throw new ArgumentNullException("fields");
        if (0 == fields.Count)
            throw new ArgumentOutOfRangeException("fields", "fields must have at least 1 field definition");

        try
        {
            Monitor.Enter(builtTypes);
            string className = GetTypeKey(fields);

            if (builtTypes.ContainsKey(className))
                return builtTypes[className];

            TypeBuilder typeBuilder = moduleBuilder.DefineType(className, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable);

            foreach (var field in fields)                    
                typeBuilder.DefineField(field.Key, field.Value, FieldAttributes.Public);

            builtTypes[className] = typeBuilder.CreateType();

            return builtTypes[className];
        }
        catch (Exception ex)
        {
            log.Error(ex);
        }
        finally
        {
            Monitor.Exit(builtTypes);
        }

        return null;
    }


    private static string GetTypeKey(IEnumerable<PropertyInfo> fields)
    {
        return GetTypeKey(fields.ToDictionary(f => f.Name, f => f.PropertyType));
    }

    public static Type GetDynamicType(IEnumerable<PropertyInfo> fields)
    {
        return GetDynamicType(fields.ToDictionary(f => f.Name, f => f.PropertyType));
    }
}
person Ethan J. Brown    schedule 06.04.2009
comment
замечательно, не знал, что создать шрифт во время выполнения было так просто! спасибо! - person mCasamento; 05.06.2012
comment
Хорошо, но не удается сериализовать интерфейс System.Linq.IQueryable - person Davut Gürbüz; 08.02.2013
comment
Вы можете поместить OrderBy в свой // TODO для оптимизации, и все готово. - person Akash Kava; 20.03.2013
comment
@Ethan J. Brown, не могли бы вы рассказать мне, как изменить свой код, если исходный код - IEnumerable, а не IQueryable? Спасибо! - person Lei Yang; 28.12.2013
comment
Я использовал это (ну, подобное) и получаю Unable to create a constant value of type ошибку. Я исправил это, заменив Expression.Constant(source) на source.Expression в последней строке. Надеюсь, это кому-то поможет :) - person Connell; 24.01.2014
comment
@Ethan J. Brown, Спасибо за отличный ответ. Как я могу использовать этот код с вложенными типами, например x = ›new {a = 1, b = new {ba = 1, bb = 2}}; - person mesut; 23.12.2015
comment
Мне нравится этот построитель типов, но вызов .GetProperties() для нового типа возвращает пустой массив. Требуется ли для их получения более сложное решение этого вопроса? stackoverflow.com/a/3862241/701346 - person oscilatingcretin; 15.11.2017

Принятый ответ очень полезен, но мне нужно было что-то немного ближе к настоящему анонимному типу.

Настоящий анонимный тип имеет свойства только для чтения, конструктор для заполнения всех значений, реализацию Equals / GetHashCode для сравнения значений каждого свойства и реализацию ToString, которая включает имя / значение каждого свойства. (См. https://msdn.microsoft.com/en-us/library/bb397696.aspx для полного описания анонимных типов.)

Основываясь на этом определении анонимных классов, я помещаю класс, который генерирует динамические анонимные типы в github, по адресу https://github.com/dotlattice/LatticeUtils/blob/master/LatticeUtils/AnonymousTypeUtils.cs. Проект также содержит несколько модульных тестов, чтобы убедиться, что поддельные анонимные типы ведут себя как настоящие.

Вот очень простой пример того, как его использовать:

AnonymousTypeUtils.CreateObject(new Dictionary<string, object>
{
    { "a", 1 },
    { "b", 2 }
});

Также еще одно примечание: я обнаружил, что при использовании динамического анонимного типа с Entity Framework конструктор должен вызываться с набором параметров «members». Например:

Expression.New(
    constructor: anonymousType.GetConstructors().Single(), 
    arguments: propertyExpressions,
    members: anonymousType.GetProperties().Cast<MemberInfo>().ToArray()
); 

Если вы использовали одну из версий Expression.New, которая не включает параметр «members», Entity Framework не распознала бы ее как конструктор анонимного типа. Итак, я предполагаю, что это означает, что выражение конструктора реального анонимного типа будет включать эту информацию о «членах».

person dotlattice    schedule 25.01.2015

Возможно, немного поздно, но может кому-то помочь.

Вы можете сгенерировать динамический выбор с помощью вызова DynamicSelectGenerator при выборе из объекта.

public static Func<T, T> DynamicSelectGenerator<T>()
            {
                // get Properties of the T
                var fields = typeof(T).GetProperties().Select(propertyInfo => propertyInfo.Name).ToArray();

            // input parameter "o"
            var xParameter = Expression.Parameter(typeof(T), "o");

            // new statement "new Data()"
            var xNew = Expression.New(typeof(T));

            // create initializers
            var bindings = fields.Select(o => o.Trim())
                .Select(o =>
                {

                    // property "Field1"
                    var mi = typeof(T).GetProperty(o);

                    // original value "o.Field1"
                    var xOriginal = Expression.Property(xParameter, mi);

                    // set value "Field1 = o.Field1"
                    return Expression.Bind(mi, xOriginal);
                }
            );

            // initialization "new Data { Field1 = o.Field1, Field2 = o.Field2 }"
            var xInit = Expression.MemberInit(xNew, bindings);

            // expression "o => new Data { Field1 = o.Field1, Field2 = o.Field2 }"
            var lambda = Expression.Lambda<Func<T, T>>(xInit, xParameter);

            // compile to Func<Data, Data>
            return lambda.Compile();
        }

И используйте этот код:

var result = dbContextInstancs.EntityClass.Select(DynamicSelectGenerator<EntityClass>());
person Ali    schedule 20.07.2017
comment
Expression.New (typeof (T)) Он не будет работать, если T является одним из типов сопоставленных сущностей. - person Mabakay; 05.03.2020

Здесь вы можете использовать IQueryable-Extensions, которые являются реализацией решения, описанного «Итаном Дж. Брауном»:

https://github.com/thiscode/DynamicSelectExtensions

Расширение динамически создает анонимный тип.

Тогда вы можете сделать это:

var YourDynamicListOfFields = new List<string>(

    "field1",
    "field2",
    [...]

)
var query = query.SelectPartially(YourDynamicListOfFields);
person thiscode    schedule 04.07.2013

Я не верю, что вам удастся этого добиться. Хотя когда вы делаете select new { c.Name, c.Population }, кажется, что вы не создаете класс, которым вы на самом деле являетесь. Если вы посмотрите на скомпилированный вывод в Reflector или на необработанный IL, вы сможете это увидеть.

У вас будет класс, который будет выглядеть примерно так:

[CompilerGenerated]
private class <>c__Class {
  public string Name { get; set; }
  public int Population { get; set; }
}

(Хорошо, я немного подчистил, так как на самом деле свойство - это просто метод get_Name() и set_Name(name), установленный в любом случае)

Что вы пытаетесь сделать, так это создание правильного динамического класса, чего не будет до выхода .NET 4.0 (и даже тогда я не совсем уверен, сможет ли он достичь того, чего вы хотите).

Лучшим решением было бы определить различные анонимные классы, а затем провести какую-то логическую проверку, чтобы определить, какой из них создать, и для его создания вы можете использовать объект System.Linq.Expressions.NewExpression.

Но это может быть (по крайней мере теоретически) возможным, если вы действительно серьезно относитесь к базовому поставщику LINQ. Если вы пишете своего собственного поставщика LINQ, вы можете определить, является ли анализируемое в данный момент выражение Select, затем вы определяете класс CompilerGenerated, отражаете его конструктор и создаете.

Вызывающе непростая задача, но это будет то, как LINQ to SQL, LINQ to XML и т. Д. Все это делают.

person Aaron Powell    schedule 03.03.2009
comment
Хорошее резюме. Жалко, что нет возможности сгенерировать выражение для генерации нового типа. Хотя, я могу представить, что это открывает большую банку червей. :) - person Inferis; 03.03.2009
comment
Я проверю, как работают расширения в System.Linq.Dynamic, я предполагаю, что должен быть способ сделать это, если этот класс может это сделать. - person Tom Deloford; 03.03.2009

Вы можете использовать класс параметров вместо работы с анонимным типом. В вашем примере вы можете создать такой класс параметров:

public struct ParamClass {
    public string Name { get; set; };
    public int Population { get; set; };
}

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

var v = from c in Countries
        where c.City == "London"
        select new ParamClass {c.Name, c.Population};

Вы получаете что-то типа IQueryable<ParamClass>.

person Spoike    schedule 03.03.2009

Это компилируется, я не знаю, работает ли это, однако ...

myEnumerable.Select((p) => { return new { Name = p.Name, Description = p.Description }; });

Предполагая, что p - это то, что вы преобразовываете, а оператор select возвращает анонимный тип, используя объявление функции лямбда.

Изменить: я также не знаю, как вы это создадите динамически. Но, по крайней мере, он показывает вам, как использовать лямбда-выражение select для возврата типа anon с несколькими значениями.

Edit2:

Вы также должны помнить, что компилятор C # на самом деле генерирует статические классы анонимного типа. Таким образом, у типа anon действительно есть тип после времени компиляции. Поэтому, если вы генерируете эти запросы во время выполнения (что, как я предполагаю, так и есть), вам, возможно, придется создать тип, используя различные методы отражения (я считаю, что вы можете использовать их для создания типов на лету), загрузите созданные типы в контекст выполнения и используйте их в созданном вами выходе.

person Sekhat    schedule 03.03.2009

Я думаю, что на большинство вопросов уже дан ответ - как сказал Слэйс, вам нужен какой-то класс, который будет возвращен из метода Select. Когда у вас есть класс, вы можете использовать метод System.Linq.Expressions.NewExpression для создания выражения.

Если вы действительно хотите это сделать, вы также можете сгенерировать класс во время выполнения. Это немного больше работы, потому что это невозможно сделать с использованием деревьев выражений LINQ, но это возможно. Для этого вы можете использовать пространство имен System.Reflection.Emit - я только что провел быстрый поиск, и вот статья, в которой это объясняется:

person Tomas Petricek    schedule 04.03.2009

Вы можете использовать API динамических выражений, который позволяет динамически строить оператор выбора следующим образом:

 Select("new(<property1>,<property2>,...)");

Для этого вам понадобится файл Dynamics.cs из LINQ и образцы языка для Visual Studio, ссылки на оба файла находятся в нижней части эта страница. Вы также можете увидеть рабочий пример, показывающий это в действии, по тому же URL-адресу.

person Adrian Grigore    schedule 03.03.2009
comment
Я считаю, что это будет работать только с LINQ to SQL, но не с другим поставщиком LINQ, хотя - person Aaron Powell; 03.03.2009
comment
Я считаю, что фреймворк работает только с IQueryable, а не с IEnumerable. - person Adrian Grigore; 03.03.2009
comment
Я пробовал ваш код, потому что он дает ошибку, как реализовать приведенный выше код в структуре сущностей с помощью dbcontext? - person Thulasiram; 20.06.2012