Come assegnare un valore di proprietà di un IQueryable ?

entity entity-framework expression-trees iqueryable linq

Domanda

Sto usando il codice di Entity Framework 4.1 First. Nella mia entità, ho tre proprietà data / ora:

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
}

Nel database, ho sempre le date Da / A popolate. Esigo contro di loro usando una semplice clausola where. Ma nel set di risultati, voglio includere la data per cui ho richiesto. Devo insistere affinché funzioni un'altra logica aziendale.

Sto lavorando su un metodo di estensione per farlo, ma sto riscontrando problemi:

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

Questo non funziona. Funziona se utilizzo un oggetto IEnumerable, ma desidero mantenerlo come IQueryable, quindi tutto viene eseguito sul lato del database e questo metodo di estensione può ancora essere utilizzato in qualsiasi parte di un'altra query. Quando è IQueryable, ottengo un errore di compilazione di quanto segue:

Un'espressione lambda con un corpo di istruzione non può essere convertita in un albero di espressioni

Se si trattasse di SQL, vorrei fare qualcosa del genere:

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

Quindi la domanda è: come posso trasformare l'albero delle espressioni che devo già includere questo assegnamento di proprietà extra? Ho esaminato IQueryable.Expression e IQueryable.Provider.CreateQuery - c'è una soluzione da qualche parte. Forse un'espressione di assegnazione può essere aggiunta all'albero dell'espressione esistente? Non sono abbastanza familiare con i metodi dell'albero delle espressioni per capirlo. Qualche idea?

Esempio di utilizzo

Per chiarire, l'obiettivo è essere in grado di eseguire qualcosa del genere:

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

E avere il DateTime.Ora conservato nel QueryDate della riga risultante, senza avere più di una riga restituita dalla query del database. (Con la soluzione IEnumerable, vengono restituite più righe prima che FirstOrDefault scelga la riga che vogliamo.)

Un'altra idea

Potrei andare avanti e mappare QueryDate come un campo reale, e impostare DatabaseGeneratedOption su Calculated. Ma poi avrei bisogno di un modo per iniettare "@QueryDate come QueryDate" nell'SQL creato dalle istruzioni select di EF. Dal momento che è calcolato, EF non tenterà di fornire valori durante l'aggiornamento o l'inserimento. Quindi, come posso inserire l'SQL personalizzato nelle istruzioni selezionate?

Risposta accettata

Grazie per tutto il prezioso feedback. Sembra che la risposta sia "no - non puoi farlo in quel modo".

Quindi - ho trovato una soluzione alternativa. Questo è molto specifico per la mia implementazione, ma fa il trucco.

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 magia è che sto usando un campo statico di thread per memorizzare la data della query in modo che sia disponibile più tardi nella stessa discussione. Il fatto di averlo ripreso nel getter di QueryDate è specifico per le mie esigenze.

Ovviamente questa non è una soluzione EF o LINQ alla domanda originale, ma ha lo stesso effetto rimuovendola da quel mondo.


Risposta popolare

Ladislav ha assolutamente ragione. Ma dal momento che ovviamente vuoi che la seconda parte della tua domanda abbia una risposta, ecco come puoi usare Assegna. Questo però non funzionerà con EF.

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

Aggiornamento 1

Ecco lo stesso codice dopo alcuni ritocchi. Ho letto l'espressione "Block", che EF ha soffocato nel codice sopra, per dimostrare con assoluta chiarezza che è l'espressione "Assegna" che EF non supporta. Nota che Assegna funziona in linea di massima con gli alberi di espressione generici, è il provider EF che non supporta Assegna.

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

        }
    }
}


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é