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

c# entity-framework expression-trees linq linq-to-entities

Pregunta

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 resuelto 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 varias propiedades en mi selección de lambda.

Respuesta aceptada

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 código a continuación es un trabajo en progreso, así que tómelo por lo que vale ... "funciona en mi caja". La clase de método SelectDynamic debe lanzarse 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 los controles de datos vinculados 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));
    }
}

Respuesta popular

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 un ToString de implementación 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).

Basándome 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 asegurarse de 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: encontré que al usar un tipo anónimo dinámico con Entity Framework, el constructor debe llamarse con el conjunto de parámetros "miembros". Por ejemplo:

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

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




Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué