Search code examples
entity-frameworkexpression-trees

Create an Expression Tree that generates a parametric query for Entity Framework


I'm trying to create a generic class to be used to compose queries for Entity Framework (5).

I got it to work, the only problem is that the value is injected as a constant of the query instead of as a parameter. This reduces the possibilities for EF to cache the query and reuse it later on.

This is what I got so far.

public class MinDateFilter<T> : IFilter<T> where T : class
{
    private readonly Expression<Func<T, bool>> _predicate;

    public MinDateCandidateFilter(Expression<Func<T, DateTime>> propertySelector, DateTime from)
    {
        from = from.Date.AddDays(-1);
        from = new DateTime(from.Year, from.Month, from.Day, 23, 59, 59, 999);

        Expression value = Expression.Constant(from, typeof(DateTime));
        //ParameterExpression variable = Expression.Variable(typeof(DateTime), "value");

        MemberExpression memberExpression = (MemberExpression)propertySelector.Body;
        ParameterExpression parameter = Expression.Parameter(typeof(T), "item");
        Expression exp = Expression.MakeMemberAccess(parameter, memberExpression.Member);

        Expression operation = Expression.GreaterThan(exp, value);
        //Expression operation = Expression.GreaterThan(exp, variable);
        _predicate = Expression.Lambda<Func<T, bool>>(operation, parameter);
    }

    public IQueryable<T> Filter(IQueryable<T> items)
    {
        return items.Where(_predicate);
    }
}

this class can be used in two ways:

by sub-classing it:

public class MinCreationDateCandidateFilter : MinDateFilter<Candidate>
{
    public MinCreationDateCandidateFilter(DateTime @from) : base(c => c.CreationDate, @from) {}
}

or simply by instantiating it:

var filter = new MinDateFilter<Entities.Transition>(t => t.Date, from.Value);

This is what I managed to achieve so far:

SELECT 
[Extent1].[Id] AS [Id]
-- Other fields
FROM [dbo].[Candidates] AS [Extent1]
WHERE [Extent1].[CreationDate] > convert(datetime2, '1982-12-09 23:59:59.9990000', 121)

instead of

SELECT 
[Extent1].[Id] AS [Id]
-- Other fields
FROM [dbo].[Candidates] AS [Extent1]
WHERE [Extent1].[CreationDate] > @p__linq__0

If I uncomment the two commented lines and I comment the two above, I get an error saying that the parameter "value" isn't bound.

I hope I gave all the useful details :)


Solution

  • When a parameter is passed as a ConstantExpression, like this:

    Expression.Constant(myString)
    

    ... it will produce a fixed, constant symbol on the resulting query:

    SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Bar] AS [Bar], 
    FROM [dbo].[Foo] AS [Extent1]
    WHERE [Extent1].[Bar] = "Some text"
    

    If we use a tool like Expression Tree Visualizer to analyze an expression like (f => f.Bar == myString)), we'll see that the parameter is actually a MemberExpression. So, in order to have a parameter instead of a string constant in the resulting query we have to pass something like a property of an object, or the more convenient anonymous type:

    Expression.Property(
        Expression.Constant(new { Value = myString }),
        "Value"
    )
    

    This way we're passing a property of the just-created object and the expression tree gets a MemberExpression that is reusable, resulting in a CommandText optimized for caching:

    SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Bar] AS [Bar], 
    FROM [dbo].[Foo] AS [Extent1]
    WHERE [Extent1].[Bar] = @p__linq__0