Queries don't work with generic interface-only filters

Posts   
 
    
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 05-Aug-2014 07:08:36   

Example against Northwind

    public static IQueryable<T> FilterByDiscontinued<T>(this IQueryable<T> products, bool? discontinued) where T : EntityBase2, IProduct
    {
      if (discontinued.HasValue)
        return discontinued.Value ? products.Where(r => r.Discontinued) : products.Where(r => r.Discontinued);
      return products;
    }

Test()
{
      var metaData = new LinqMetaData(new DataAccessAdapter());
      metaData.Product.FilterByDiscontinued(true).ToEntityCollection2();
}

This runs fine but if I remove EntityBase2 so type constraint becomes

where T : IProduct

It blows up with

The binary expression '(Convert(Entity(ProductEntity)).Discontinued == True)' can't be converted to a predicate expression.

at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleBinaryExpressionSeparateOperands(BinaryExpression expressionToHandle, Expression leftSide, Expression rightSide)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleBinaryExpressionBooleanOperator(BinaryExpression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.CoerceToFilterExpression(Expression toCoerce)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleWhereExpression(WhereExpression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.GenericExpressionHandler.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.HandleExpressionTree(Expression expression)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.Execute(Expression expression)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.System.Linq.IQueryProvider.Execute(Expression expression)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProQuery`1.System.Collections.IEnumerable.GetEnumerator()

LLBL V4.2.14.0725

Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 05-Aug-2014 14:13:38   

This is because IProduct isn't something it can convert to an entity. The entity is required to produce the fields for the predicate. The IProduct interface makes the code compile, but isn't used for query production as our framework doesn't support querying on interfaces.

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 06-Aug-2014 00:55:40   

Otis wrote:

This is because IProduct isn't something it can convert to an entity.

but EntityBase2, IProduct can I take it? I've also tried this with a POCO that implements IProduct and that works the same. e.g.

  public class PocoBase  {  }

  public class ProductViewDto : PocoBase, IProduct
  {
    public Category? CategoryId { get; set; }
...
}

public static IQueryable<T> FilterByDiscontinuedP<T>(this IQueryable<T> products, bool? discontinued) where T : PocoBase, IProduct
    {
      if (discontinued.HasValue)
        return discontinued.Value ? products.Where(r => r.Discontinued) : products.Where(r => r.Discontinued);
      return products;
    }

Works with PocoBase as a type constraint but not without it

Otis wrote:

Our framework doesn't support querying on interfaces.

Do you mind expanding on that? Because, in my example, at first glance, it looks like you have all you need to make it work, as a concrete type is passed into the filter method.

Full code here: http://rapiddevbookcode.codeplex.com/SourceControl/latest#LLBL%20Pro%20v4.2/Northwind/DAL/DTO/ProductViewDto.cs

Jeremy Thomas
daelmo avatar
daelmo
Support Team
Posts: 8245
Joined: 28-Nov-2005
# Posted on: 06-Aug-2014 07:37:25   

Hi Tom,

Isn't that example the same as the first one? I mean, it didn't work if you remove the concrete type. The thing is that the framework cannot query on interfaces.

David Elizondo | LLBLGen Support Team
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 06-Aug-2014 09:58:21   

Additionally, the query with PocoBase works because you have to fetch it with an entity query somewhere, so it gets projected into PocoBase instances and it uses that to find the fields to build the filter. The interface can't provide that information as it's not a concrete implementation.

I know it sounds weird, but the projection is with a concrete type, so it has a direct line between concrete property in pocobase and entity field. If it just has an interface it doesn't have that.

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 06-Aug-2014 14:17:21   

Annoyed with myself for not trying this sooner but this also works

    public static IQueryable<T> FilterByDiscontinued<T>(this IQueryable<T> products, bool? discontinued) where T : class, IProduct
    {
      if (discontinued.HasValue)
        return discontinued.Value ? products.Where(r => r.Discontinued) : products.Where(r => r.Discontinued);
      return products;
    }

which gives me the result I'm after, a filter I can use with both IQueryable<ProductViewDto> and IQueryable<ProductEntity>

But I'm still curious about

  • What 'Querying on interfaces' means
  • Why the framework doesn't support itIOW, can you give me a quick(hopefully one sentence) definition of what sort of queries aren't supported.
Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 06-Aug-2014 15:14:14   

Query on interface is that the type given to return (e.g. IProduct) is an interface, not a concrete type. In Linq every method returns an IQueryable<T>, and if T is an interface, the code can't proceed as it can't determine which concrete type T is at runtime, as all it has is a type.

We don't support this in the core framework, so the linq provider can't deal with it either. The main thing is that say you have 4 entities all implementing a given interface IProduct. If the sequence in the linq query is about IProduct, the code doesn't know what type to return, as 4 concrete types implement the interface. Some ORMs support querying on interface, so when you query IProduct instances, you'll get all instances of all types implementing IProduct matching the query. That is something we don't support, so you have to specify concrete types.

I have to check why 'class' works, but I guess the type to work with (T) is then no longer just an interface but a concrete type and the query can continue.

Hopefully I explained things properly simple_smile

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 07-Aug-2014 05:57:36   

Otis wrote:

Query on interface is that the type given to return (e.g. IProduct) is an interface, not a concrete type. In Linq every method returns an IQueryable<T>, and if T is an interface, the code can't proceed as it can't determine which concrete type T is at runtime, as all it has is a type.

We don't support this in the core framework, so the linq provider can't deal with it either.

I have to check why 'class' works, but I guess the type to work with (T) is then no longer just an interface but a concrete type and the query can continue.

Hopefully I explained things properly simple_smile

Yeah, you're explanation of what it means for an ORM to support querying on interface made things a lot clearer. Thankssimple_smile

But I would contend whether, to execute the examples I've given, an ORM needs to implement querying on interface. since the concrete type is present in the query. i.e. Product.FilterByDiscontinued(true).GetType() is SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProQuery`1[[Northwind.DAL.EntityClasses.ProductEntity]] if the filter was non-generic

    public static IQueryable<IProduct> FilterByDiscontinuedI(this IQueryable<IProduct> products, bool? discontinued)
    {
      if (discontinued.HasValue)
        return discontinued.Value ? products.Where(r => r.Discontinued) : products.Where(r => r.Discontinued);
      return products;
    }

then I wouldn't expect it to work without underlying querying on interface support.

I await to hear back on your investigation of why 'class' works with interest, as it seems to contradict your opening statement.

Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 07-Aug-2014 09:30:30   

The thing to remember is that in linq expressions, there's no such thing as an instance to reflect on, there are types only, and that's it. The binary expression which fails when only IProduct is used, is due to the fact it's not evaluated properly before that.

When only IProduct is used as where filter, the expression to handle there is Convert(Entity(ProductEntity).Discontinued == true)

When I use 'class' as well, the expression to handle is: EntityField(LPLA_1.Discontinued AS Discontinued)==true)

So it goes wrong earlier. Looking into why this is ...

(edit) When "class, IProduct" is used, the full expression is: Entity(ProductEntity).Where(r => Entity(ProductEntity).Discontinued) when only IProduct is used, it's Entity(ProductEntity).Where(r => Convert(Entity(ProductEntity)).Discontinued)

The Convert is a cast to IProduct. That one isn't stripped off, so it's not handled properly (because, how?). The Convert expression isn't stripped off, because it's not expected. It's not correct to simply remove them everywhere, they have to be removed at places where they don't mean anything. This sadly means that we have to anticipate on these Convert expressions whenever they pop up.

So that's the reason. And in case you wonder: no it's not easy to add a reliable way to anticipate on this particular convert so I'm not going to spend time on fixing this, as there's a workaround (and specifying 'class' in the generic filter is better so it knows no structs with the interface will use the method so the convert won't be introduced in the expression tree).

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 07-Aug-2014 10:25:31   

Otis wrote:

So that's the reason. And in case you wonder: no it's not easy to add a reliable way to anticipate on this particular convert so I'm not going to spend time on fixing this, as there's a workaround

Thanks for the full explanation, your answer is pretty much what i expected: it could be handled but not easily.

One last thing: In my last example, with the non-generic IProduct filter, it throws a null exception, can that error be handled more gracefully?

Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 07-Aug-2014 12:21:52   

Where is the null exception thrown exactly?

(I threw away the test code already)

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 07-Aug-2014 13:24:33   

Otis wrote:

Where is the null exception thrown exactly? )

at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProvider2.PerformPreExecuteEntityProjectionTasks(QueryExpression toExecute)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProvider2.ExecuteEntityProjection(QueryExpression toExecute)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.ExecuteExpression(Expression handledExpression)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.Execute(Expression expression)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.System.Linq.IQueryProvider.Execute(Expression expression)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProQuery`1.System.Collections.IEnumerable.GetEnumerator()
Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 08-Aug-2014 09:29:32   

this likely crashes as the factory to produce instances isn't found. I can add a more graceful exception like 'factory not found' but that's about it. The problem is elsewhere, namely that it needs to create an entity projection but can't do that due to the interface cast, i.e. the query hasn't been properly handled. 'Factory not found' will be as helpful as the NRE.

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 11-Aug-2014 06:12:19   

Otis wrote:

'Factory not found' will be as helpful as the NRE.

I do think it is a little bit more helpful. As: * You might be less likely to think it's a LLBL bug, which means more likely to look at you own code. * You would be able to search the forums for it. * If you did correct it, then hit it again some time later, you are more likely to remember the solution.

Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 11-Aug-2014 10:22:24   

Good point. I'll add that check.

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 01-Apr-2015 01:21:40   

Otis wrote:

Good point. I'll add that check.

Thanks, I see you have the check for an entity projection (I get SD.LLBLGen.Pro.ORMSupportClasses.ORMQueryConstructionException: Can't obtain entity factory for type 'Northwind.DAL.HelperClasses.IProduct'.). But can you add one for 'Any()' etc. I still get:

System.NullReferenceException: Object reference not set to an instance of an object.
Result StackTrace:  
at SD.LLBLGen.Pro.LinqSupportClasses.LinqUtils.CreateDerivedTableAdapter(QueryExpression expressionToConvert, String alias, IElementCreatorCore generatedCodeElementCreator, ITemplateGroupSpecificCreator frameworkElementCreator)
   at SD.LLBLGen.Pro.LinqSupportClasses.LinqUtils.CreateDerivedTable(QueryExpression expressionToConvert, String alias, IElementCreatorCore generatedCodeElementCreator, MappingTracker trackedMappings, ITemplateGroupSpecificCreator frameworkElementCreator)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.CreateDerivedTable(QueryExpression expressionToConvert, String alias)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleAllAnyExpression(AllAnyExpression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.GenericExpressionHandler.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleProjectionExpression(ProjectionExpression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.GenericExpressionHandler.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.GenericExpressionHandler.HandleSelectExpression(SelectExpression expressionToHandle, SelectExpression newInstance)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.GenericExpressionHandler.HandleSelectExpression(SelectExpression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleSelectExpression(SelectExpression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.GenericExpressionHandler.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.ExpressionHandlers.QueryExpressionBuilder.HandleExpression(Expression expressionToHandle)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.HandleExpressionTree(Expression expression)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.PerformExecute(Expression expression, Type resultType)
   at SD.LLBLGen.Pro.LinqSupportClasses.LLBLGenProProviderBase.System.Linq.IQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.Any[TSource](IQueryable`1 source)
Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 01-Apr-2015 17:08:27   

Will do (in that location). The problem is that it's not always an error that the factory can't be found (as in: it might be harmless as e.g. the result is ignored anyway), so the stopgap check isn't in one place. The derived table however is an endpoint for a lot of methods so adding a check there will help in more locations.

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 07-Apr-2015 18:12:58   

There was one location where null checks were in place as it would be acceptable that the factory wasn't retrievable (e.g. custom type instead of entity). In all other cases the problem would always be a null ref exception. We've adjusted these locations with a central retrieval of the factory and checks whether it succeeds or not, so if the factory isn't retrievable, a proper exception is thrown.

I've attached a release build of this fix.

Attachments
Filename File size Added on Approval
ORMSupportClasses_42_04072015.zip 946,182 07-Apr-2015 18:13.05 Approved
Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 10-Apr-2015 05:55:35   

Otis wrote:

I've attached a release build of this fix.

With version 4.2.15.0407 I still get a NullReferenceException on Product.FilterByDiscontinuedI(true).Any()

Jeremy Thomas
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39888
Joined: 17-Aug-2003
# Posted on: 10-Apr-2015 11:13:25   

Of all the calls to the factory retrieval method I missed one flushed , which is the one you ran into. I double checked and still missed one, sorry for this. It's fixed now, will be in the next build released later today.

Frans Bouma | Lead developer LLBLGen Pro
TomDog
User
Posts: 623
Joined: 25-Oct-2005
# Posted on: 14-Apr-2015 12:49:57   

Otis wrote:

Of all the calls to the factory retrieval method I missed one flushed , which is the one you ran into. I double checked and still missed one, sorry for this. It's fixed now, will be in the next build released later today.

Yep V4.2.20150410 sorts it, ta.

Jeremy Thomas