Linq take returns zero elements from OData collection
I can't wrap my brain around this, I don't get why this behaves likes this. I have made an OData query that returns a collection of 175 items
from c in navClient.Item where c.No != "" && c.Last_Date_Modified > dtfrom select c
navClient.Item is a System.Data.Services.Client.DataServiceQuery
However I want to take first 100 items from the collection using .Take(100) but get 0 items. It isn't until I do .Take(121) I get my first item, which is the first item in the collection, .Take(122) returns the first two items and so on.
Any idea why this behaves like this?
Edit: Doing ToList first then Take(100) returns first 100 as expected. My only theory right know is that the table I'm running my query against is just a temp table that is out of sync with the database.
You have described what looks like an issue in the server implementation that handles your request. This behaviour would occur if the
.Take(100)was evaluated before the filter criteria. This issues can easily occur in the client or the server implementations. The
.ToList()works because it brings back the entire collection to the client and then applies the filter criteria with standard linq to objects evaluation.
While not the cause today...
as a general rule, whenever you specify
.Skip()you should have also specified the explicit
.OrderBy(), when the order is ambiguous in a limiting or paging query so too can be the results.
There are 3 common levels at play here:
Client Query Resolution
Your linq query is first resolved into a URL that will be used to make the request to the API, you should first check that the URL is correctly constructed.
You should be expecting a URL similar to:
/odata/Item?$top=100&$filter=No ne '' AND Last_Date_Modified gt '2020-01-20T17:24:21.3605918+11:00'
You can inspect the URL using the
.RequestUriproperty on the query, try something like the following to capture it:
var query = from c in navClient.Item where c.No != "" && c.Last_Date_Modified > dtfrom select c; query = query.Take(100); System.Diagnostics.Debug.WriteLine(((DataServiceQuery<Item>)query).RequestUri);
The URL needs to through both the
$filterquery options to the server.
The client is expecting the controller to handle or pass through both the
$filtercriteria and the
$topexpression to the underlying data store.
Many default EF or Unit of work based OData implementations would simply return a proper
IQueryable<T>expression using deferred execution. However if the backend store uses a repository pattern or the controller is otherwise constructing the dataset then that code may need to explicitly handle the
$filtercriteria before applying the
There are many simple OData controller examples out there that have a mocked
IEnumerable<T>backend, the red flag is that if in the controller code you see a
.AsQueryable()on the main expression, or it returns an
IEnumerable<T>response, then it indicates that the expression is not using deferred execution at all. This means that it most likely needs to manage the query options manually.
OData Query Expression Resolver
Especially in the .Net Implementation, the
EnableQueryAttributeby default applies the query options to the final IQueryable output immediately before or effectively as part of the serialization process.
$expand) will be applied again even if the controller method has already evaluated these options. This isn't normally a problem, its a validation fail-safe and means that you do not strictly have to return
IQueryable<T>from your controller methods at all.
Due to this, if our backend supports deferred
IQueryable<T>linq expressions (like
DbSet<T>in an EF
DbContext) then in the controller implementation we do not usually do anything with the query options, you let the
EnableQueryAttributeprocess it for you.
However, if the controller were to process the paging expression first
.Take(100)and not apply the
$filtercorrectly or at all, then the
EnableQueryAttributewould apply the criteria to a list that had already been restricted, in your example, perhaps the first 100 items