Nécessité de créer un arbre d'expression pour la valeur de date maximale

.net c# entity-framework expression-trees linq

Question

J'essaie de construire un arbre d'expression pour cette requête linq afin de pouvoir transmettre une entité générique:

this.EntityCollection.Select((ent) => ent.TimeStamp).Max()

Je veux créer une classe qui prend une entité générique et trouve le maximum de sa propriété TimeStamp.

J'essayais quelque chose comme ci-dessous, mais il se plaint:

ParameterExpression param = Expression.Parameter(typeof(TE), "ent");

MemberExpression prop = Expression.
    Property(param, typeof(TE).GetProperty("TimeStamp").GetGetMethod());

Expression<Func<TE, DateTime>> lambda = Expression.Lambda<Func<TE, DateTime>>(
    prop, new ParameterExpression[] { param });

DateTime maxdate = this.EntityCollection.Select(lambda).Max();

Lorsque je compile, l'erreur suivante apparaît sur la dernière ligne de code:

La résolution de la surcharge a échoué car aucune sélection 'accessible' ne peut être appelée avec ces arguments:

Qu'est-ce que je fais mal?

Réponse acceptée

(Selon les commentaires ...)

Le problème est que vous essayez d'utiliser un mélange de LINQ to Objects (qui utilise IEnumerable<T> et des délégués) et de LINQ basé sur la requête (qui utilise IQueryable<T> et des arbres d'expression). Vous ne pouvez pas transmettre à Enumerable<T> un arbre d’expression.

Trois options:

  • Convertissez d'abord la collection en un IQueryable<T> :

    DateTime maxdate = this.EntityCollection.AsQueryable().Select(lambda).Max();
    
  • Convertissez d'abord l'arbre des expressions en délégué:

    DateTime maxdate = this.EntityCollection.Select(lambda.Compile()).Max();
    
  • Changez votre méthode pour accepter un IQueryable<T> au lieu d'un IEnumerable<T>


Réponse populaire

Personnellement, je préfère la surcharge d' Expression.Property qui prend une instance de PropertyInfo .

En faisant cela, vous pourriez faire ceci:

ParameterExpression param = Expression.Parameter(typeof(TE), "ent");
MemberExpression prop = Expression.
    Property(param, typeof(TE).GetProperty("TimeStamp"));
Expression<Func<TE, DateTime>> lambda = Expression.Lambda<Func<TE, DateTime>>(
    prop, new ParameterExpression[] { param });
DateTime maxdate = this.EntityCollection.Select(lambda).Max();

C'est juste beaucoup plus propre.

Il est possible que l'appel à Type.GetProperty ne renvoie rien et cela vous donne l'erreur. N'oubliez pas que le nom de la propriété passé en tant que paramètre doit être public, sinon vous devez utiliser la surcharge de GetProperty qui vous permet de spécifier des valeurs à partir de l'énumération BindingFlags pour indiquer que vous souhaitez inclure des propriétés non publiques.

Cependant, je pense qu'il existe une meilleure alternative. Vous devriez définir une interface comme ceci:

public interface IHaveTimestamp
{
    DateTime TimeStamp { get; set; }
}

Cela vous permet ensuite de définir votre méthode d'extension de la manière suivante:

public static DateTime? MaxTimeStamp(IEnumerable<T> entities) 
    where T : IHaveTimeStamp
{
    // Return the max.
    return entities.Select(e => (DateTime?) e.TimeStamp).Max();
}

Remarque: DateTime? est utilisé à la place de DateTime dans le cas où vous avez une séquence vide. En outre, vous pouvez créer une surcharge prenant un IQueryable<T> si vous souhaitez que l'exécution se produise sur un serveur.

Le principal avantage que vous obtenez ici est que vous bénéficiez d'une vérification au moment de la compilation pour déterminer où les appels sont valides. C'est beaucoup mieux que d'avoir une exception levée à l'exécution.

En outre, il ne serait pas difficile à mettre en œuvre; vous utilisez Entity Framework qui crée des fichiers de classe partiels ; à cause de cela, il est facile d'ajouter un autre fichier de classe partiel pour chaque type qui a ceci:

public partial class MyEntity : IHaveTimeStamp
{ }

Votre code d'origine indique que vous avez déjà la propriété TimeStamp sur chacune des entités pour lesquelles vous souhaitez utiliser cette méthode d'extension. De ce fait, vous n'avez rien à faire pour implémenter l'interface, elle est déjà implicitement implémentée pour vous car la propriété TimeStamp doit être publique.

Si ce n'est pas public, vous pouvez changer votre définition pour qu'elle soit facilement la suivante:

public partial class MyEntity : IHaveTimeStamp
{ 
    IHaveTimeStamp.TimeStamp
    { 
        get { return this.TimeStamp; } 
        set { this.TimeStamp = value; } 
    }
}

Quoi qu'il en soit, il s'agit d'un simple travail de copier-coller avec quelques modifications du nom de la classe à chaque fois.



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow