Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to assign a property value of an IQueryable<T>?

I'm using Entity Framework 4.1 Code First. In my entity, I have three date/time properties:

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 the database, I always have the From/To dates populated. I query against them using a simple where clause. But in the result set, I want to include the date I queried for. I need to persist this for some other business logic to work.

I'm working on an extension method to do this, but I'm running into problems:

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

This doesn't work. It works if I use an IEnumerable, but I want to keep it as IQueryable so everything runs on the database side, and this extention method can still be used in any part of another query. When it's IQueryable, I get a compile error of the following:

A lambda expression with a statement body cannot be converted to an expression tree

If this was SQL, I would just do something like this:

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

So the question is, how can I transform the expression tree I already have to include this extra property assignment? I have looked into IQueryable.Expression and IQueryable.Provider.CreateQuery - there's a solution in there somewhere. Maybe an assignment expression can be appended to the existing expression tree? I'm not familiar enough with the expression tree methods to figure this out. Any ideas?

Example Usage

To clarify, the goal is to be able to perform something like this:

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

And have the DateTime.Now persisited into the QueryDate of the resulting row, WITHOUT having more than one row returned from the database query. (With the IEnumerable solution, multiple rows are returned before FirstOrDefault picks the row we want.)

Another Idea

I could go ahead and map QueryDate like a real field, and set its DatabaseGeneratedOption to Computed. But then I would need some way to inject the "@QueryDate as QueryDate" into the SQL created by EF's select statements. Since it's computed, EF won't try to provide values during update or insert. So how could I go about injecting custom SQL into the select statements?

like image 435
Matt Johnson-Pint Avatar asked Apr 12 '11 18:04

Matt Johnson-Pint


People also ask

What inherits from IQueryable?

The IQueryable interface inherits the IEnumerable interface so that if it represents a query, the results of that query can be enumerated. Enumeration causes the expression tree associated with an IQueryable object to be executed.

What is the difference between IEnumerable T and IQueryable T >?

The main difference between IEnumerable and IQueryable in C# is that IQueryable queries out-of-memory data stores, while IEnumerable queries in-memory data.

How do you add a new record to an IQueryable variable?

The simple answer is that unless you add the record to the underlying datastore that the Iqueryable is querying, you can't add a new record into an IQueryable. So if you are using LinqToSql then you would have to add a row into the table that the IQueryable was querying in order to "add" a row into the IQueryable.

What is IQueryable return?

IQueryable is executed. // // Returns: // A System.Type that represents the type of the element(s) that are returned when. // the expression tree associated with this object is executed.


2 Answers

Ladislav is absolutely right. But since you obviously want the second part of your question to be answered, here is how you can use Assign. This won't work with EF, though.

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

Here is the same code after some tweaking. I got rid of the "Block" expression, that EF choked on in the code above, to demonstrate with absolute clarity that it's "Assign" expression that EF does not support. Note that Assign works in principle with generic Expression trees, it is EF provider that does not support 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) });
            
        }
    }
}
like image 130
Andrew Savinykh Avatar answered Nov 03 '22 10:11

Andrew Savinykh


No, I don't think there is a solution. It is true that you can modify expression tree but you will get exactly the same exception as you got with your linq query because that query actually is what you will build in expression tree. The problem is not in expression tree but in the mapping. EF can't map QueryData to the result. Moreover you are trying to do projection. Projection can't be done to mapped entity and anonymous type can't be returned from the method.

You can off course do the select you mentioned but simply you can't map it to your entity. You must create a new type for that:

var query = from x in context.MyData
            where x.FromDate <= queryDate && x.ToDate >= queryDate
            select new MyDateWrapper
                {
                   MyData = x,
                   QueryDate = queryDate
                };
like image 37
Ladislav Mrnka Avatar answered Nov 03 '22 10:11

Ladislav Mrnka