PROBLEM with LINQKit pluggable expressions on OData V3 Query in LINQPad v5.22.02
When trying to run the following VB statements in LINQPad v5.22.02 (I've not tested this on other older versions of LINQPad though), I got a NotSupportedException with the message "The method 'Contains' is not supported". So obviously, OData V3 doesn't support the usage of the Contains operator in such a manner:
So, I turned to LINQKit as a solution and came out with the following statements instead:
Although the above statements work (the outcome is as expected), I'm disappointed that the filtering is done on the client (LINQPad itself) not on the server since according to LINQPad's Request Log the query above translates into the following request:
https://MyServer/MyApp/MyOdataV3.svc/refProposalStatusSet
I was actually expecting that the generated request would resemble the following instead:
https://MyServer/MyApp/MyOdataV3.svc/refProposalStatusSet()?$filter=proposalStatusID eq 1 or proposalStatusID eq 2 or proposalStatusID eq 3
Did I do something wrong or Is this a bug in LINQKit, LINQPad's built-in WCF Data Services 5.5 (OData 3) built-in driver or LINQPad itself?
If the source EntitySet contains just a handful of records, I don't mind if LINQPad downloads the whole set onto the client's memory and filters it locally but it's simply not feasible for large sets. I plan to also fetch related entities within the same query so the amount of data to be downloaded will be huge. That's why server-side filtering is very important for me.
I tried both LinqKit for EntityFramework (with IAsync support) v1.1.9.0 and LinqKit.Core v1.1.9.0. Both could not produce the expected Odata request (with server-side filtering).
I hope somebody (especially @JoeAlbahari , since you are involved in the creation of both LINQPad and LINQKit) could offer a solution to my problem.
Dim SpecificIDsToFetch As Integer() = {1, 2, 3}
Dim QueryFromOdataV3Source = From r In refProposalStatusSet _
Where SpecificIDsToFetch.Contains(r.proposalStatusID) _
Select r
QueryFromOdataV3Source.Dump
So, I turned to LINQKit as a solution and came out with the following statements instead:
Dim SpecificIDsToFetch = Enumerable.Range(1,10).ToArray
Dim predicate = SpecificIDsToFetch.Aggregate(PredicateBuilder.[New](Of refProposalStatus),
Function(es, SpecificID) es.Or(Function(s) s.proposalStatusID = SpecificID) )
Dim QueryFromOdataV3Source = refProposalStatusSet.AsExpandable.Where(predicate.Compile)
QueryFromOdataV3Source.Dump
Although the above statements work (the outcome is as expected), I'm disappointed that the filtering is done on the client (LINQPad itself) not on the server since according to LINQPad's Request Log the query above translates into the following request:
https://MyServer/MyApp/MyOdataV3.svc/refProposalStatusSet
I was actually expecting that the generated request would resemble the following instead:
https://MyServer/MyApp/MyOdataV3.svc/refProposalStatusSet()?$filter=proposalStatusID eq 1 or proposalStatusID eq 2 or proposalStatusID eq 3
Did I do something wrong or Is this a bug in LINQKit, LINQPad's built-in WCF Data Services 5.5 (OData 3) built-in driver or LINQPad itself?
If the source EntitySet contains just a handful of records, I don't mind if LINQPad downloads the whole set onto the client's memory and filters it locally but it's simply not feasible for large sets. I plan to also fetch related entities within the same query so the amount of data to be downloaded will be huge. That's why server-side filtering is very important for me.
I tried both LinqKit for EntityFramework (with IAsync support) v1.1.9.0 and LinqKit.Core v1.1.9.0. Both could not produce the expected Odata request (with server-side filtering).
I hope somebody (especially @JoeAlbahari , since you are involved in the creation of both LINQPad and LINQKit) could offer a solution to my problem.
Comments
Try calling:
refProposalStatusSet.Where(predicate.Expand())
Unfortunately when I tried your suggestion, I got the following error instead:
BC30519 Overload resolution failed because no accessible 'Where' can be called without a narrowing conversion:
Extension method 'Public Function Where(predicate As Expression(Of Func(Of refProposalStatus, Boolean))) As IQueryable(Of refProposalStatus)' defined in 'Queryable': Argument matching parameter 'predicate' narrows from 'Expression' to 'Expression(Of Func(Of refProposalStatus, Boolean))'.
Extension method 'Public Function Where(predicate As Expression(Of Func(Of refProposalStatus, Integer, Boolean))) As IQueryable(Of refProposalStatus)' defined in 'Queryable': Argument matching parameter 'predicate' narrows from 'Expression' to 'Expression(Of Func(Of refProposalStatus, Integer, Boolean))'.
Also, an additional question: What 'version' of LINQKit should I actually use for querying OData services? LINQKit.Core or "LinqKit for EntityFramework (with IAsync support)"?
http://www.albahari.com/nutshell/linqkit.aspx
If the overload resolution still doesn't work, does it make any difference if you try it in C#?
It's really slow for me to rethink almost everything in C# (being a VB.NET developer for over a decade now).
Besides, I'm building this query for my non-programmer users and I've just finished teaching them how to modify/customize my base queries (all of which are in VB) themselves. I think VB is the easiest for them to understand since, among other things, it features more English words than C# (imho), case-insensitivity when using reserved keywords and there's no need to end statements with a delimiter (";" in case of C#).
Anyway, I managed to "translate" the simple query above into C# by hand and as you've said, it works (the expected OData query is produced) except that I have to use
PredicateBuilder.False<refProposalStatus>()
instead sincePredicateBuilder.New<T>()
doesn't exist in the plain LINQKit.HOWEVER, when I swapped out the plain LINQKit for LINQKit.Core or the "LinqKit for EntityFramework (with IAsync support)" and use
PredicateBuilder.New<T>()
instead, a similar error message is flagged at thepredicate.Expand()
line :CS1503 Argument 2: cannot convert from 'System.Linq.Expressions.Expression' to 'System.Linq.Expressions.Expression<System.Func<LINQPad.User.refProposalStatus, bool>>'
So, somehow the Expand() function is incompatible with PredicateBuilder.New() , right?
When I replaced
PredicateBuilder.New<T>()
withPredicateBuilder.False<T>()
, the code runs despite the warning below:CS0618 'PredicateBuilder.False<t>()' is obsolete: 'Use PredicateBuilder.New() instead'
When I switched back to the VB version of the query, it works as expected (i got the server-side filtering OData query) when i changed
PredicateBuilder.[New](Of refProposalStatus)
toPredicateBuilder.False(Of refProposalStatus)
(using all 3 versions of LINQKit). This is of course accompanied by the obsolete warning message when using the non-plain versions.So, I guess this is not a language-specific thing. A bug somewhere else perhaps?
When using
PredicateBuilder.New<T>()
, the result of the Expand() function needs to be explicitly casted to the typeExpression<Func<T, bool>>
in order for the CS1503 error message to go away and allowing the query to be compiled and executed.So, instead of a clean :
refProposalStatusSet.Where(predicate.Expand())
I have to do the following instead:
refProposalStatusSet.Where(DirectCast(predicate.Expand(), Expression(Of Func(Of refProposalStatus, Boolean))))
A bit ugly in my opinion but it gets the code running and achieves the expected server-side query.
I still hope that this return type mismatch will be fixed later in LINQKit or whichever components that caused it.