Как создать LINQ Expression Tree для выбора анонимного типа - PullRequest
43 голосов
/ 03 марта 2009

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

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

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

Ответы [ 9 ]

68 голосов
/ 07 апреля 2009

Это можно сделать, как уже упоминалось, с помощью 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));
    }
}
8 голосов
/ 25 января 2015

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

Настоящий анонимный тип имеет свойства только для чтения, конструктор для заполнения всех значений, реализацию 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 не распознает его как конструктор анонимного типа. Поэтому я предполагаю, что это означает, что выражение конструктора реального анонимного типа будет включать в себя эту информацию "членов".

2 голосов
/ 20 июля 2017

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

Вы можете сгенерировать динамический выбор по вызову 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>());
2 голосов
/ 04 июля 2013

Вы можете использовать здесь IQueryable-Extensions, который является реализацией решения, описанного «Ethan J. Brown»:

https://github.com/thiscode/DynamicSelectExtensions

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

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

var YourDynamicListOfFields = new List<string>(

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

)
var query = query.SelectPartially(YourDynamicListOfFields);
1 голос
/ 04 марта 2009

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

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

1 голос
/ 03 марта 2009

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

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

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

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

Edit2:

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

1 голос
/ 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>.

1 голос
/ 03 марта 2009

Я не верю, что вы сможете достичь этого. Хотя, когда вы делаете 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 и т. Д.

0 голосов
/ 03 марта 2009

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

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

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

...