Cómo crear un árbol de expresiones LINQ para seleccionar un tipo anónimo

Resuelto Tom Deloford asked hace 15 años • 9 respuestas

Me gustaría generar la siguiente declaración de selección dinámicamente usando árboles de expresión:

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

He descubierto cómo generar

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

pero parece que no puedo encontrar un constructor/sobrecarga que me permita especificar múltiples propiedades en mi lambda seleccionada.

Tom Deloford avatar Mar 03 '09 18:03 Tom Deloford
Aceptado

Esto se puede hacer, como se mencionó, con la ayuda de Reflection Emit y una clase de ayuda que he incluido a continuación. El siguiente código es un trabajo en progreso, así que considérelo por lo que vale... "funciona en mi caja". La clase de método SelectDynamic debe incluirse en una clase de método de extensión estática.

Como era de esperar, no obtendrá ningún Intellisense ya que el tipo no se crea hasta el tiempo de ejecución. Funciona bien en controles de datos tardíos.

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));
    }
}
Ethan J. Brown avatar Apr 06 '2009 20:04 Ethan J. Brown

La respuesta aceptada es muy útil, pero necesitaba algo un poco más cercano a un tipo anónimo real.

Un tipo anónimo real tiene propiedades de solo lectura, un constructor para completar todos los valores, una implementación de Equals/GetHashCode para comparar los valores de cada propiedad y una implementación ToString que incluye el nombre/valor de cada propiedad. (Consulte https://msdn.microsoft.com/en-us/library/bb397696.aspx para obtener una descripción completa de los tipos anónimos).

Basado en esa definición de clases anónimas, puse una clase que genera tipos anónimos dinámicos en github en https://github.com/dotlattice/LatticeUtils/blob/master/LatticeUtils/AnonymousTypeUtils.cs . El proyecto también contiene algunas pruebas unitarias para garantizar que los tipos anónimos falsos se comporten como los reales.

Aquí hay un ejemplo muy básico de cómo usarlo:

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

Además, otra nota: descubrí que cuando se utiliza un tipo anónimo dinámico con Entity Framework, se debe llamar al constructor con el conjunto de parámetros "miembros". Por ejemplo:

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

Si utilizó una de las versiones de Expression.New que no incluye el parámetro "miembros", Entity Framework no la reconocerá como el constructor de un tipo anónimo. Entonces supongo que eso significa que la expresión del constructor de un tipo anónimo real incluiría esa información de "miembros".

dotlattice avatar Jan 25 '2015 19:01 dotlattice

Quizás un poco tarde pero puede ayudar a alguien.

Puede generar selección dinámica mediante llamada DynamicSelectGeneratoren selección de una entidad.

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

Y use este código:

var result = dbContextInstancs.EntityClass.Select(DynamicSelectGenerator<EntityClass>());
Ali avatar Jul 20 '2017 04:07 Ali

No creo que puedas lograr esto. Aunque cuando lo haces select new { c.Name, c.Population }parece que no estás creando una clase que en realidad sí lo estás. Si echa un vistazo a la salida compilada en Reflector o al IL sin formato, podrá ver esto.

Tendrás una clase que se vería así:

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

(Ok, lo limpié un poco, ya que una propiedad es en realidad solo un conjunto de métodos get_Name()y set_Name(name)de todos modos)

Lo que estás intentando hacer es una creación de clases dinámica adecuada, algo que no estará disponible hasta que salga .NET 4.0 (e incluso entonces no estoy muy seguro de si podrá lograr lo que deseas).

Su mejor solución sería definir las diferentes clases anónimas y luego realizar algún tipo de verificación lógica para determinar cuál crear, y para crearla puede usar el objeto System.Linq.Expressions.NewExpression.

Pero puede ser posible (al menos en teoría) hacerlo si se está volviendo muy estricto con el proveedor LINQ subyacente. Si está escribiendo su propio proveedor LINQ, puede detectar si la expresión actualmente analizada es una Selección, luego determina la CompilerGeneratedclase, refleja su constructor y crea.

Definitivamente no es una tarea sencilla, pero así sería como lo hacen LINQ to SQL, LINQ to XML, etc.

Aaron Powell avatar Mar 03 '2009 12:03 Aaron Powell

Puede utilizar IQueryable-Extensions aquí, que es una implementación de la solución descrita por "Ethan J. Brown":

https://github.com/thiscode/DynamicSelectExtensions

La extensión construye dinámicamente un tipo anónimo.

Entonces puedes hacer esto:

var YourDynamicListOfFields = new List<string>(

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

)
var query = query.SelectPartially(YourDynamicListOfFields);
thiscode avatar Jul 04 '2013 10:07 thiscode