Cómo asignar un valor de propiedad de un IQueryable ?

entity entity-framework expression-trees iqueryable linq

Pregunta

Estoy usando Entity Framework 4.1 Code First. En mi entidad, tengo tres propiedades de fecha / hora:

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
}

En la base de datos, siempre tengo las fechas Desde / Hasta rellenadas. Consulta contra ellos usando una simple cláusula where. Pero en el conjunto de resultados, quiero incluir la fecha que solicité. Necesito persistir esto para que funcione otra lógica empresarial.

Estoy trabajando en un método de extensión para hacer esto, pero tengo problemas:

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

Esto no funciona. Funciona si uso un IEnumerable, pero quiero mantenerlo como IQueryable para que todo se ejecute en el lado de la base de datos, y este método de extensión todavía se puede usar en cualquier parte de otra consulta. Cuando es IQueryable, recibo un error de compilación de lo siguiente:

Una expresión lambda con un cuerpo de declaración no se puede convertir en un árbol de expresión

Si esto fuera SQL, solo haría algo como esto:

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

Entonces, la pregunta es, ¿cómo puedo transformar el árbol de expresiones que ya tengo para incluir esta asignación de propiedad adicional? He examinado IQueryable.Expression y IQueryable.Provider.CreateQuery: hay una solución en alguna parte. ¿Tal vez una expresión de asignación se puede agregar al árbol de expresión existente? No estoy lo suficientemente familiarizado con los métodos del árbol de expresión para resolver esto. ¿Algunas ideas?

Ejemplo de uso

Para aclarar, el objetivo es poder realizar algo como esto:

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

Y haga que DateTime.Now se convierta en el QueryDate de la fila resultante, SIN tener más de una fila devuelta por la consulta de la base de datos. (Con la solución IEnumerable, se devuelven varias filas antes de que FirstOrDefault elija la fila que queremos).

Otra idea

Podría seguir adelante y asignar QueryDate como un campo real, y configurar su DatabaseGeneratedOption en Computed. Pero entonces necesitaría alguna forma de inyectar "@QueryDate as QueryDate" en el SQL creado por las declaraciones selectas de EF. Como se calcula, EF no intentará proporcionar valores durante la actualización o inserción. Entonces, ¿cómo podría inyectar SQL personalizado en las declaraciones seleccionadas?

Respuesta aceptada

Gracias por todos los comentarios valiosos. Parece que la respuesta es "no, no puedes hacerlo de esa manera".

Así que, me di cuenta de una solución. Esto es muy específico para mi implementación, pero hace el truco.

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 es que estoy usando un campo estático de subprocesos para almacenar en caché la fecha de la consulta para que esté disponible más adelante en el mismo subproceso. El hecho de que lo recupere en el programa de obtención de QueryDate es específico para mis necesidades.

Obviamente, esta no es una solución EF o LINQ para la pregunta original, pero logra el mismo efecto al eliminarla de ese mundo.


Respuesta popular

Ladislav tiene toda la razón. Pero como obviamente desea que se responda la segunda parte de su pregunta, aquí le explicamos cómo puede usar Asignar. Sin embargo, esto no funcionará 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;
        }
    }
}

Actualización 1

Aquí está el mismo código después de algunos ajustes. Me leí la expresión "Bloque", que EF se ahogó en el código anterior, para demostrar con absoluta claridad que es la expresión "Asignar" que EF no admite. Tenga en cuenta que Assign funciona en principio con árboles de expresión genéricos, es el proveedor EF que no admite 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) });

        }
    }
}


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow