Comment attribuer une valeur de propriété d'un IQueryable ?

entity entity-framework expression-trees iqueryable linq

Question

J'utilise Entity Framework 4.1 Code First. Dans mon entité, j'ai trois propriétés de date / heure:

public class MyEntity
{
    [Key]
    public Id { get; set; }

    public DateTime FromDate { get; set; }

    public DateTime ToDate { get; set; }

    [NotMapped]
    public DateTime? QueryDate { get; set; }

    // and some other fields, of course
}

Dans la base de données, j'ai toujours les dates De / À. Je leur demande en utilisant une simple clause where. Mais dans le jeu de résultats, je souhaite inclure la date à laquelle j'ai demandé. Je dois persister dans cette logique pour qu'une autre logique métier fonctionne.

Je travaille sur une méthode d'extension pour faire cela, mais je rencontre des problèmes:

public static IQueryable<T> WhereDateInRange<T>(this IQueryable<T> queryable, DateTime queryDate) where T : MyEntity
{
    // this part works fine
    var newQueryable = queryable.Where(e => e.FromDate <= queryDate &&
                                            e.ToDate >= queryDate);

    // in theory, this is what I want to do
    newQueryable = newQueryable.Select(e =>
                                           {
                                               e.QueryDate = queryDate;
                                               return e;
                                           });
    return newQueryable;
}

Ça ne marche pas. Cela fonctionne si j'utilise un IEnumerable, mais je veux le conserver en tant que IQueryable afin que tout fonctionne du côté base de données et cette méthode d'extension peut toujours être utilisée dans n'importe quelle partie d'une autre requête. Quand c'est IQueryable, j'obtiens une erreur de compilation de ce qui suit:

Une expression lambda avec un corps d'instruction ne peut pas être convertie en un arbre d'expression

S'il s'agissait de SQL, je ferais simplement quelque chose comme ceci:

SELECT *, @QueryDate as QueryDate
FROM MyEntities
WHERE @QueryDate BETWEEN FromDate AND ToDate

La question est donc de savoir comment puis-je transformer l’arbre d’expression dont j’ai déjà besoin pour inclure cette attribution de propriété supplémentaire? J'ai examiné IQueryable.Expression et IQueryable.Provider.CreateQuery - il existe une solution quelque part. Peut-être qu'une expression d'affectation peut être ajoutée à l'arbre d'expression existant? Je ne connais pas suffisamment les méthodes de l'arbre d'expression pour comprendre cela. Des idées?

Exemple d'utilisation

Pour clarifier, l'objectif est de pouvoir effectuer quelque chose comme ceci:

var entity = dataContext.Set<MyEntity>()
                        .WhereDateInRange(DateTime.Now)
                        .FirstOrDefault();

Et ayez DateTime.Now persisited dans la QueryDate de la ligne résultante, SANS avoir plus d'une ligne renvoyée de la requête de base de données. (Avec la solution IEnumerable, plusieurs lignes sont renvoyées avant que FirstOrDefault ne sélectionne la ligne souhaitée.)

Une autre idée

Je pouvais aller de l'avant et mapper QueryDate comme un champ réel et définir sa propriété DatabaseGeneratedOption sur Calculée. Mais il me faudrait alors un moyen d'injecter "@QueryDate as QueryDate" dans le code SQL créé par les instructions select d'EF. Etant donné qu'il est calculé, EF n'essaiera pas de fournir des valeurs lors de la mise à jour ou de l'insertion. Alors, comment puis-je injecter du SQL personnalisé dans les instructions select?

Réponse acceptée

Merci pour tous vos précieux commentaires. On dirait que la réponse est "non - vous ne pouvez pas le faire de cette façon".

Donc - j'ai trouvé une solution de contournement. Ceci est très spécifique à mon implémentation, mais cela fait l'affaire.

public class MyEntity
{       
    private DateTime? _queryDate;

    [ThreadStatic]
    internal static DateTime TempQueryDate;

    [NotMapped]
    public DateTime? QueryDate
    {
        get
        {
            if (_queryDate == null)
                _queryDate = TempQueryDate;
            return _queryDate;
        }
    }

    ...       
}

public static IQueryable<T> WhereDateInRange<T>(this IQueryable<T> queryable, DateTime queryDate) where T : MyEntity
{
    MyEntity.TempQueryDate = queryDate;

    return queryable.Where(e => e.FromDate <= queryDate && e.ToDate >= queryDate);
}

La magie est que j'utilise un champ statique de thread pour mettre en cache la date de la requête afin qu'elle soit disponible ultérieurement dans le même thread. Le fait que je le récupère dans le getter de QueryDate est spécifique à mes besoins.

Évidemment, ce n'est pas une solution EF ou LINQ à la question initiale, mais cela produit le même effet en la supprimant de ce monde.


Réponse populaire

Ladislav a absolument raison. Mais comme vous souhaitez de toute évidence répondre à la deuxième partie de votre question, voici comment utiliser Assign. Cela ne fonctionnera pas avec EF, cependant.

using System;
using System.Linq;
using System.Linq.Expressions;

namespace SO5639951
{
    static class Program
    {
        static void Main()
        {
            AdventureWorks2008Entities c = new AdventureWorks2008Entities();
            var data = c.Addresses.Select(p => p);

            ParameterExpression value = Expression.Parameter(typeof(Address), "value");
            ParameterExpression result = Expression.Parameter(typeof(Address), "result");
            BlockExpression block = Expression.Block(
                new[] { result },
                Expression.Assign(Expression.Property(value, "AddressLine1"), Expression.Constant("X")),
                Expression.Assign(result, value)
                );

            LambdaExpression lambdaExpression = Expression.Lambda<Func<Address, Address>>(block, value);

            MethodCallExpression methodCallExpression = 
                Expression.Call(
                    typeof(Queryable), 
                    "Select", 
                    new[]{ typeof(Address),typeof(Address) } , 
                    new[] { data.Expression, Expression.Quote(lambdaExpression) });

            var data2 = data.Provider.CreateQuery<Address>(methodCallExpression);

            string result1 = data.ToList()[0].AddressLine1;
            string result2 = data2.ToList()[0].AddressLine1;
        }
    }
}

Mise à jour 1

Voici le même code après quelques ajustements. On m'a lu l'expression "Block", sur laquelle EF s'est étouffé dans le code ci-dessus, pour démontrer avec une clarté absolue que c'est une expression "Assign" que EF ne prend pas en charge. Notez que Assign fonctionne en principe avec les arbres d’expression génériques, c’est le fournisseur EF qui ne prend pas en charge Assign.

using System;
using System.Linq;
using System.Linq.Expressions;

namespace SO5639951
{
    static class Program
    {
        static void Main()
        {
            AdventureWorks2008Entities c = new AdventureWorks2008Entities();

            IQueryable<Address> originalData = c.Addresses.AsQueryable();

            Type anonType = new { a = new Address(), b = "" }.GetType();

            ParameterExpression assignParameter = Expression.Parameter(typeof(Address), "value");
            var assignExpression = Expression.New(
                anonType.GetConstructor(new[] { typeof(Address), typeof(string) }),
                assignParameter,
                Expression.Assign(Expression.Property(assignParameter, "AddressLine1"), Expression.Constant("X")));
            LambdaExpression lambdaAssignExpression = Expression.Lambda(assignExpression, assignParameter);

            var assignData = originalData.Provider.CreateQuery(CreateSelectMethodCall(originalData, lambdaAssignExpression));
            ParameterExpression selectParameter = Expression.Parameter(anonType, "value");
            var selectExpression = Expression.Property(selectParameter, "a");
            LambdaExpression lambdaSelectExpression = Expression.Lambda(selectExpression, selectParameter);

            IQueryable<Address> finalData = assignData.Provider.CreateQuery<Address>(CreateSelectMethodCall(assignData, lambdaSelectExpression));

            string result = finalData.ToList()[0].AddressLine1;
        }        

        static MethodCallExpression CreateSelectMethodCall(IQueryable query, LambdaExpression expression)
        {
            Type[] typeArgs = new[] { query.ElementType, expression.Body.Type };
            return Expression.Call(
                typeof(Queryable),
                "Select",
                typeArgs,
                new[] { query.Expression, Expression.Quote(expression) });

        }
    }
}


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