Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Expression<Func<TEntity, bool>> nesting that works with Entity Framework

I have read about 30 Stack Overflow questions, and about 20 blogs and cannot find my answer. That said, I am sure the answer I require is out there so if you know of it please point it out (please note the last paragraph/sentence about answers that do not fit my requirement). Thanks!

I am persisting instances of classes that have a similar shape to:

public class Data {
    public int Id { get; set; }
}

public class Container {
    public int Id { get; set; }
    public Data Data { get; set; }
}

Currently queries are written to find Containers via an abstraction layer that requires a Lambda Expression accepting a Container and returning bool (predicate). It will not accept IQueryable even though Entity Framework 5 is the ORM of choice.

My task is to present an API surface based around Lambda Expressions that accept the Data type. I cannot change the abstraction layer (I must pass a predicate that accepts a Container) so I am trying to convert an expression I receive as:

Expression<Func<Data , bool>>
to:
Expression<Func<Container , bool>>

I have added an extra method in my repository class like these:

public Container Find( Expression<Func<Data , bool>> predicate ) {

    IEnumerable<Container> result = QueryStrategy.Fetch( c => predicate.Compile().Invoke( c.Data ) );

    return result.FirstOrDefault();

}

This compliments the existing Find method:

public Container Find( Expression<Func<Container , bool>> predicate ) {

    IEnumerable<Container> result = QueryStrategy.Fetch( predicate );

    return result.FirstOrDefault();

}

When the former method is used it produces the following exception:

LINQ to Entities does not recognize the method 'Boolean Invoke(Container.Data)' method, and this method cannot be translated into a store expression.

I have tried all sorts of things with Expression classes I just can't see a way to map:

Expression<Func<Data , bool>>
to:
Expression<Func<Container , bool>>

Without using Invoke which is not supported by Entity Framework (but works fine with in memory enumerable data).

Can anybody help me get the above scenario working using Expressions?

I understand that I may be able to use the LinqKit library to solve this, but I really want to solve this problem without bringing in a third party library.

UPDATE: In my effort to present a simplified problem, I have implied (by originally using DbContext in the code sample) that the code would have access to IQueryable and/or be suitable for using LinqKit's AsExpandable(). In reality this is not the case - the repository class is not allowed to use IQueryable or any vendor specific extensions. I have modified the examples above and hopefully this makes things clearer.

This Issue Is Now Solved

like image 287
FantasticJamieBurns Avatar asked Dec 06 '12 00:12

FantasticJamieBurns


2 Answers

I have managed to solve the problem after 4 hours of reading/tinkering with Expressions (amazing technology).

I have put this class together (it is not polished or final but shows how it was achieved):

class ParameterRewriter<TTarget , TSource> : ExpressionVisitor {

    private ParameterExpression Source;
    private MemberExpression Target;

    public Expression<Func<TTarget , bool>> Rewrite( Expression<Func<TSource , bool>> predicate , Expression<Func<TTarget , TSource>> propertyNameExpression ) {

        var parameter = Expression.Parameter( typeof( TTarget ) );

        var propertyName = ( propertyNameExpression.Body as MemberExpression ).Member.Name;

        Source = predicate.Parameters.Single();
        Target = Expression.PropertyOrField( parameter , propertyName );

        var body = Visit( predicate.Body );

        return Expression.Lambda<Func<TTarget , bool>>(
            body ,
            parameter
        );

    }

    protected override Expression VisitParameter( ParameterExpression node ) {

        if ( node == Source ) {
            return Target;
        }

        return base.VisitParameter( node );

    }

}

And it can be used like this:

var parameterRewriter = new ParameterRewriter<Container , Data>();
Expression<Func<Data , bool>> dataPredicate = d => ( d.Id == 1 );
var containerPredicate = parameterRewriter.Rewrite( dataPredicate , c => c.Data );

In theory one should be able to traverse deeper relationships by examining propertyNameExpression but I have had enough for today.

I can now see that the SQL Entity Framework has generated for each flavour of query is identical:

========================

Expression<Func<Container , bool>> p = c => c.Data.Id == 1

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Data_Id] AS [Data_Id]
FROM [dbo].[Container] AS [Extent1]
WHERE 1 = [Extent1].[Data_Id]

========================

Expression<Func<Data , bool>> p = d => d.Id == 1

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Data_Id] AS [Data_Id]
FROM [dbo].[Container] AS [Extent1]
WHERE 1 = [Extent1].[Data_Id]

========================

like image 103
FantasticJamieBurns Avatar answered Oct 23 '22 12:10

FantasticJamieBurns


Join customers with filtered by predicated data, then select customer from result

public Container Find(Expression<Func<Data, bool>> predicate ) 
{
    return MyDbContext.Containers
                      .Join(MyDbContext.Containers.Select(c => c.Data)
                                                  .Where(predicate), 
                            c => c.Data.Id,
                            d => d.Id,
                            (c, d) => c)
                      .FirstOrDefault();
}
like image 1
Sergey Berezovskiy Avatar answered Oct 23 '22 11:10

Sergey Berezovskiy