I have a weird situation regarding EntityFramework 6 with .NET 4.5 (C#).
I have (almost) the same query in two different places. But one time it queries agains the database and the second time it queries against in-memory objects. And since I'm filtering for a substring, this is a crucial difference:
Database structure are tables Role, Right and a cross-table Role_Right
First time around I want to find all available rights that are not already assigned to the role plus (and that's where it gets complicated) a manual filter to reduce the result list:
Role role = ...;
string filter = ...;
var roleRightNames = role.Right.Select(roleRight => roleRight.RightName);
var filteredRights = context.Right.Where(right => !roleRightNames.Contains(right.RightName));
if (!string.IsNullOrWhiteSpace(filter))
{
filteredRights = filteredRights.Where(e => e.RightName.Contains(filter));
}
var result = filteredRights.ToList();
I cannot use IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0)
because this cannot be translated to SQL. But I'm fine with Contains
because it produces the desired result (see below).
When enabling the SQL output I get:
SELECT [Extent1].[RightName] AS [RightName]
FROM [dbo].[Right] AS [Extent1]
WHERE ( NOT ([Extent1].[RightName] IN ('Right_A1', 'Right_A2', 'Right_B1'))) AND ([Extent1].[RightName] LIKE @p__linq__0 ESCAPE '~'
-- p__linq__0: '%~_a%' (Type = AnsiString, Size = 8000)
Which is exactly what I want, a case-insensitive search on the filter "_a" to find for example 'Right_A3'
The second time I want to filter the existing associated rights for the same filter:
Role role = ...;
string filter = ...;
var filteredRights = string.IsNullOrWhiteSpace(filter)
? role.Right
: role.Right.Where(e => e.RightName.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0);
var result = filteredRights.ToList();
This time it forces me to use IndexOf
because it uses the Contains
method of the string
instead of translating it to an SQL LIKE
and string.Contains
is case-sensitive.
My problem is that I cannot - from looking at the code - predict when a query is executed against the database and when it is done in-memory and since I cannot use IndexOf
in the first query and Contains
in the second this seems to be a bit unpredictable to me. What happens when one day the second query is executed first and the data is not already in-memory?
Edit 10 Feb 2020
OK, so I figured out what the main difference is. context.Right
is of type DbSet
which is an IQueryable
and so is the subsequent extension method Where
. However userRole.Right
returns an ICollection
which is an IEnumerable
and so is the subsequent Where
. Is there a way to make the relationship property of an entity object to an IQueryable
? AsQueryable
did not work. Which means that all associated Right
entities are always gotten from the database before doing an in-memory Where
.
We're not talking about huge amounts of data and at least now this behaviour is predictable, but I find it unfortunate nonetheless.
OK, so I found two different solutions to always query against the database in case a relation contains a huge result set. Both solutions are not directly intuitive - IMHO - and you will need the DbContext
variable which you hadn't needed before.
Solution one is using the Role
table as a starting point and simply filtering for the entity with the correct Id.
Note You cannot use Single
because then you deal with a single entity object and you're right back where you've started. You need to use Where
and then a SelectMany
even though it's counter-intuitive:
Role role = ...;
string filter = ...;
var filteredRights = context.Role.Where(e => e.RoleId == userRole.RoleId).SelectMany(e => e.Right);
if (!string.IsNullOrWhiteSpace(filter))
{
filteredRights = filteredRights.Where(e => e.RightName.Contains(filter));
}
var rights = filteredRights.ToList();
which results in an SQL query against the DB:
SELECT
[Extent1].[RightName] AS [RightName]
FROM [dbo].[Role_Right] AS [Extent1]
WHERE ([Extent1].[RoleId] = @p__linq__0) AND ([Extent1].[RightName] LIKE @p__linq__1 ESCAPE '~')
-- p__linq__0: '42' (Type = Int32, IsNullable = false)
-- p__linq__1: '%~_a%' (Type = AnsiString, Size = 8000)
The second solution I found here: https://stackoverflow.com/a/7552985/2334520
In my case this results in:
Role role = ...;
string filter = ...;
var filteredRights = context.Entry(userRole).Collection(e => e.Right).Query();
if (!string.IsNullOrWhiteSpace(filter))
{
filteredRights = filteredRights.Where(e => e.RightName.Contains(filter));
}
var rights = filteredRights.ToList();
and SQL
SELECT
[Extent1].[RightName] AS [RightName]
FROM [dbo].[Role_Right] AS [Extent1]
WHERE ([Extent1].[RoleId] = @EntityKeyValue1) AND ([Extent1].[RightName] LIKE @p__linq__0 ESCAPE '~')
-- EntityKeyValue1: '42' (Type = Int32, IsNullable = false)
-- p__linq__0: '%~_a%' (Type = AnsiString, Size = 8000)
My problem is that I cannot - from looking at the code - predict when a query is executed against the database and when it is done in-memory and since I cannot use IndexOf in the first query and Contains in the second this seems to be a bit unpredictable to me.
You can use IndexOf
and Contains
in both queries, as long as you don't use the overload featuring a StringComparison
. As pointed by @BrettCaswell, the case matching is fixed by the collation of your Database/Table/Column.
A query will be translated to SQL if its root is a context's DbSet
and all method calls are translatable to SQL.
As soon as a method cannot be translated, the current state request is performed at SQL level and the remainder of the query is performed in the memory of the .Net application.
Also I think that p__linq__0
value should be '%~_a%'
as _
is a special character in LIKE
clauses.