So weisen Sie einen Eigenschaftswert eines IQueryable zu ?

entity entity-framework expression-trees iqueryable linq

Frage

Ich verwende Entity Framework 4.1 Code First. In meiner Entität habe ich drei Eigenschaften für Datum und Uhrzeit:

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
}

In der Datenbank habe ich immer die Von / Bis-Daten gefüllt. Ich frage sie mit einer einfachen Where-Klausel ab. Aber in der Ergebnismenge möchte ich das Datum einfügen, nach dem ich gefragt habe. Ich muss dies beibehalten, damit eine andere Geschäftslogik funktioniert.

Ich arbeite an einer Erweiterungsmethode, aber ich habe Probleme:

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

Dies funktioniert nicht. Es funktioniert, wenn ich ein IEnumerable verwende, aber ich möchte es als IQueryable behalten, so dass alles auf der Datenbankseite läuft, und diese Erweiterungsmethode kann immer noch in irgendeinem Teil einer anderen Abfrage verwendet werden. Wenn es IQueryable ist, erhalte ich einen Kompilierungsfehler der folgenden:

Ein Lambda-Ausdruck mit einem Anweisungskörper kann nicht in einen Ausdrucksbaum konvertiert werden

Wenn dies SQL wäre, würde ich einfach so etwas tun:

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

Die Frage ist also, wie kann ich den Ausdrucksbaum, den ich bereits habe, transformieren, um diese zusätzliche Eigenschaftszuweisung zu enthalten? Ich habe IQueryable.Expression und IQueryable.Provider.CreateQuery untersucht - da ist irgendwo eine Lösung drin. Vielleicht kann ein Zuweisungsausdruck an den vorhandenen Ausdrucksbaum angefügt werden? Ich kenne die Ausdrucksbaum-Methoden nicht genug, um das herauszufinden. Irgendwelche Ideen?

Beispiel Verwendung

Um es klarzustellen, ist das Ziel, etwas in der Art zu tun zu können:

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

Und das DateTime.Now persistiert in das QueryDate der resultierenden Zeile, OHNE dass mehr als eine Zeile von der Datenbankabfrage zurückgegeben wird. (Bei der IEnumerable-Lösung werden mehrere Zeilen zurückgegeben, bevor FirstOrDefault die gewünschte Zeile auswählt.)

Eine andere Idee

Ich könnte weitermachen und QueryDate wie ein echtes Feld zuordnen und DatabaseGeneratedOption auf Compute setzen. Aber dann würde ich einen Weg brauchen, um das "@QueryDate als QueryDate" in das SQL einzufügen, das von den Select-Anweisungen von EF erzeugt wird. Da EF berechnet wird, versucht EF beim Aktualisieren oder Einfügen keine Werte anzugeben. Wie also könnte ich benutzerdefinierte SQL in die SELECT-Anweisungen einfügen?

Akzeptierte Antwort

Vielen Dank für das wertvolle Feedback. Es klingt wie die Antwort ist "Nein - Sie können es nicht so machen".

Also - ich habe einen Workaround gefunden. Dies ist sehr spezifisch für meine Implementierung, aber es macht den Trick.

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

Die Magie besteht darin, dass ich ein statisches Thread-Feld verwende, um das Abfragedatum zwischenzuspeichern, damit es später im selben Thread verfügbar ist. Die Tatsache, dass ich es in den Getter des QueryDates zurückbekomme, ist spezifisch für meine Bedürfnisse.

Offensichtlich ist dies keine EF- oder LINQ-Lösung für die ursprüngliche Frage, aber sie bewirkt den gleichen Effekt, indem sie sie aus dieser Welt entfernt.


Beliebte Antwort

Ladislav hat absolut Recht. Aber da Sie offensichtlich den zweiten Teil Ihrer Frage beantworten möchten, können Sie hier Assign verwenden. Dies funktioniert jedoch nicht mit 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;
        }
    }
}

Update 1

Hier ist der gleiche Code nach einigen Optimierungen. Ich habe den "Block" -Ausdruck gelesen, den EF im obigen Code erstickt hat, um mit absoluter Klarheit zu demonstrieren, dass es "Assign" -Ausdruck ist, den EF nicht unterstützt. Beachten Sie, dass Assign grundsätzlich mit generischen Ausdrucksbäumen arbeitet. EF-Provider unterstützen Assign nicht.

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

        }
    }
}


Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow