web-dev-qa-db-fra.com

Comment créer un arbre d'expression LINQ pour sélectionner un type anonyme

Je voudrais générer dynamiquement l'instruction select suivante à l'aide d'arbres d'expression:

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

J'ai trouvé comment générer

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

mais je n'arrive pas à trouver un constructeur/surcharge qui me permettra de spécifier plusieurs propriétés dans mon lambda sélectionné.

45
Tom Deloford

Cela peut être fait, comme mentionné, à l'aide de Reflection Emit et d'une classe d'assistance que j'ai incluse ci-dessous. Le code ci-dessous est un travail en cours, alors prenez-le pour ce qu'il vaut ... 'ça marche sur ma boîte'. La classe de méthode SelectDynamic doit être lancée dans une classe de méthode d'extension statique.

Comme prévu, vous n'obtiendrez aucun Intellisense car le type n'est pas créé avant l'exécution. Fonctionne bien sur les contrôles de données à liaison tardive.

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));
    }
}
70
Ethan J. Brown

La réponse acceptée est très utile, mais j'avais besoin de quelque chose d'un peu plus proche d'un vrai type anonyme.

Un véritable type anonyme a des propriétés en lecture seule, un constructeur pour remplir toutes les valeurs, une implémentation de Equals/GetHashCode pour comparer les valeurs de chaque propriété et une implémentation ToString qui inclut le nom/la valeur de chaque propriété. (Voir https://msdn.Microsoft.com/en-us/library/bb397696.aspx pour une description complète des types anonymes.)

Sur la base de cette définition des classes anonymes, j'ai mis une classe qui génère des types anonymes dynamiques sur github à https://github.com/dotlattice/LatticeUtils/blob/master/LatticeUtils/AnonymousTypeUtils.cs . Le projet contient également des tests unitaires pour s'assurer que les faux types anonymes se comportent comme de vrais.

Voici un exemple très simple de son utilisation:

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

En outre, une autre note: j'ai constaté que lors de l'utilisation d'un type anonyme dynamique avec Entity Framework, le constructeur doit être appelé avec le jeu de paramètres "members". Par exemple:

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

Si vous avez utilisé l'une des versions d'Expression.New qui n'inclut pas le paramètre "members", Entity Framework ne le reconnaîtrait pas comme le constructeur d'un type anonyme. Je suppose donc que cela signifie que l'expression d'un constructeur de type anonyme réel inclurait ces informations de "membres".

9
dotlattice

Peut-être un peu tard mais peut aider quelqu'un.

Vous pouvez générer une sélection dynamique en appelant DynamicSelectGenerator dans select depuis une entité.

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

Et utiliser par ce code:

var result = dbContextInstancs.EntityClass.Select(DynamicSelectGenerator<EntityClass>());
3
Ali

Vous pouvez utiliser les extensions IQueryable ici, qui est une implémentation de la solution décrite par "Ethan J. Brown":

https://github.com/thiscode/DynamicSelectExtensions

L'extension crée dynamiquement un type anonyme.

Ensuite, vous pouvez le faire:

var YourDynamicListOfFields = new List<string>(

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

)
var query = query.SelectPartially(YourDynamicListOfFields);
2
thiscode

Vous pouvez utiliser une classe de paramètres au lieu de travailler avec un type anonyme. Dans votre exemple, vous pouvez créer une classe de paramètres comme celle-ci:

public struct ParamClass {
    public string Name { get; set; };
    public int Population { get; set; };
}

… Et mettez-le dans votre sélection comme ceci:

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

Ce que vous sortez est quelque chose du type IQueryable<ParamClass>.

1
Spoike

Je pense que la plupart des choses ont déjà été répondues - comme l'a dit Slace, vous avez besoin d'une classe qui serait renvoyée par la méthode Select. Une fois que vous avez la classe, vous pouvez utiliser le System.Linq.Expressions.NewExpression méthode pour créer l'expression.

Si vous voulez vraiment le faire, vous pouvez également générer une classe au moment de l'exécution. C'est un peu plus de travail, car cela ne peut pas être fait en utilisant des arbres d'expression LINQ, mais c'est possible. Vous pouvez utiliser System.Reflection.Emit namespace pour le faire - je viens de faire une recherche rapide et voici un article qui explique ceci:

1
Tomas Petricek

Cela compile, je sais pas si ça marche cependant ...

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

En supposant que p est ce que vous transformez, et l'instruction select renvoie un type anon, en utilisant la déclaration de fonction de lambda.

Edit: Je ne sais pas non plus comment vous pourriez générer cela dynamiquement. Mais au moins, il vous montre comment utiliser le lambda sélectionné pour renvoyer un type anon avec plusieurs valeurs

Edit2:

Vous devez également garder à l'esprit que le compilateur c # génère en fait des classes statiques de type anon. Le type anon a donc un type après le moment de la compilation. Donc, si vous générez ces requêtes au moment de l'exécution (ce que je suppose que vous êtes), vous devrez peut-être construire un type en utilisant les différentes méthodes de réflexion (je crois que vous pouvez les utiliser pour créer des types à la volée) charger les types créés dans le contexte d'exécution et utilisez-les dans votre sortie générée.

1
Sekhat

Je ne pense pas que vous pourrez y parvenir. Bien que lorsque vous faites select new { c.Name, c.Population }, Il semble que vous ne créez pas une classe que vous êtes réellement. Si vous regardez la sortie compilée dans Reflector ou l'IL brut, vous pourrez le voir.

Vous aurez une classe qui ressemblerait à ceci:

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

(Ok, je l'ai nettoyé, car une propriété est en fait juste une méthode get_Name() et set_Name(name) définie de toute façon)

Ce que vous essayez de faire est une création de classe dynamique appropriée, quelque chose qui ne sera pas disponible jusqu'à la sortie de .NET 4.0 (et même alors, je ne suis pas vraiment sûr de pouvoir atteindre ce que vous voulez).

Votre meilleure solution serait de définir les différentes classes anonymes puis d'avoir une sorte de vérification logique pour déterminer laquelle créer et la créer vous pouvez utiliser l'objet System.Linq.Expressions.NewExpression.

Mais, il peut être (en théorie du moins) possible de le faire, si vous êtes vraiment dur avec le fournisseur LINQ sous-jacent. Si vous êtes en train d'écrire votre propre fournisseur LINQ, vous pouvez détecter si l'expression actuellement analysée est un Select, puis vous déterminez la classe CompilerGenerated, réfléchissez pour son constructeur et créez.

Ce n'est pas une tâche simple, mais ce serait la façon dont LINQ to SQL, LINQ to XML, etc.

1
Aaron Powell

Vous pouvez utiliser l'API Dynamic Expression qui vous permet de créer dynamiquement votre instruction select comme ceci:

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

Vous avez besoin du fichier Dynamics.cs de LINQ et des exemples de langue pour Visual Studio pour que cela fonctionne, les deux sont liés en bas de cette page . Vous pouvez également voir un exemple de travail montrant cela en action sur la même URL.

0
Adrian Grigore