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

StackOverflow https://stackoverflow.com/questions/606104

Вопрос

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

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

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

Это было полезно?

Решение

Как уже упоминалось, это можно сделать с помощью 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));
    }
}

Другие советы

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

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

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

https://github.com/thiscode/DynamicSelectExtensions

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

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

var YourDynamicListOfFields = new List<string>(

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

)
var query = query.SelectPartially(YourDynamicListOfFields);

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

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

Я не верю, что вам удастся этого добиться.Хотя когда ты это делаешь 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 class, подумайте о его конструкторе и создайте.

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

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

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>.

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

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

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

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

Редактировать2:

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top