Come creare LINQ Expression Tree per selezionare un tipo anonimo

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

Domanda

Vorrei generare la seguente istruzione select usando dinamicamente gli alberi di espressione:

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

Ho elaborato come generare

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

ma non riesco a trovare un costruttore / sovraccarico che mi consenta di specificare più proprietà nel mio lambda selezionato.

Risposta accettata

Questo può essere fatto, come detto, con l'aiuto di Reflection Emit e una classe di supporto che ho incluso di seguito. Il codice sotto è un work in progress, quindi prendilo per quello che vale ... 'funziona sulla mia scatola'. La classe del metodo SelectDynamic deve essere sottoposta a una classe di metodo di estensione statica.

Come previsto, non si ottiene alcun Intellisense poiché il tipo non viene creato fino al runtime. Funziona bene con i controlli dati ritardati.

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

Risposta popolare

La risposta accettata è molto utile, ma avevo bisogno di qualcosa di un po 'più vicino ad un vero tipo anonimo.

Un tipo anonimo reale ha proprietà di sola lettura, un costruttore per il riempimento di tutti i valori, un'implementazione di Equals / GetHashCode per confrontare i valori di ogni proprietà e un'implementazione di ToString che include il nome / valore di ciascuna proprietà. (Vedere https://msdn.microsoft.com/en-us/library/bb397696.aspx per una descrizione completa dei tipi anonimi.)

Sulla base di questa definizione di classi anonime, inserisco una classe che genera tipi dinamici anonimi su github in https://github.com/dotlattice/LatticeUtils/blob/master/LatticeUtils/AnonymousTypeUtils.cs . Il progetto contiene anche alcuni test unitari per assicurarsi che i tipi anonimi falsi si comportino come quelli veri.

Ecco un esempio molto semplice di come usarlo:

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

Inoltre, un'altra nota: ho scoperto che quando si utilizza un tipo anonimo dinamico con Entity Framework, il costruttore deve essere chiamato con il set di parametri "membri". Per esempio:

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

Se si è utilizzata una delle versioni di Expression.New che non include il parametro "members", Entity Framework non lo riconoscerà come costruttore di un tipo anonimo. Suppongo quindi che l'espressione di costruttore di un tipo di anonimo reale includa le informazioni "membri".



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché