given a list of ids, I can query all relevant rows by:
context.Table.Where(q => listOfIds.Contains(q.Id));
But how do you achieve the same functionality when the Table has a composite key?
Entity Framework Classic Include The Include method lets you add related entities to the query result. In EF Classic, the Include method no longer returns an IQueryable but instead an IncludeDbQuery that allows you to chain multiple related objects to the query result by using the AlsoInclude and ThenInclude methods.
You can also configure multiple properties to be the key of an entity - this is known as a composite key. Composite keys can only be configured using the Fluent API; conventions will never set up a composite key, and you can not use Data Annotations to configure one.
The only way to configure composite keys is to use the HasKey method. You specify the properties that form the composite key by passing them in as properties of an anonymous type to the HasKey method.
This is a nasty problem for which I don't know any elegant solution.
Suppose you have these key combinations, and you only want to select the marked ones (*).
Id1 Id2 --- --- 1 2 * 1 3 1 6 2 2 * 2 3 * ... (many more)
How to do this is a way that Entity Framework is happy? Let's look at some possible solutions and see if they're any good.
Join
(or Contains
) with pairsThe best solution would be to create a list of the pairs you want, for instance Tuples, (List<Tuple<int,int>>
) and join the database data with this list:
from entity in db.Table // db is a DbContext join pair in Tuples on new { entity.Id1, entity.Id2 } equals new { Id1 = pair.Item1, Id2 = pair.Item2 } select entity
In LINQ to objects this would be perfect, but, too bad, EF will throw an exception like
Unable to create a constant value of type 'System.Tuple`2 (...) Only primitive types or enumeration types are supported in this context.
which is a rather clumsy way to tell you that it can't translate this statement into SQL, because Tuples
is not a list of primitive values (like int
or string
).1. For the same reason a similar statement using Contains
(or any other LINQ statement) would fail.
Of course we could turn the problem into simple LINQ to objects like so:
from entity in db.Table.AsEnumerable() // fetch db.Table into memory first join pair Tuples on new { entity.Id1, entity.Id2 } equals new { Id1 = pair.Item1, Id2 = pair.Item2 } select entity
Needless to say that this is not a good solution. db.Table
could contain millions of records.
Contains
statementsSo let's offer EF two lists of primitive values, [1,2]
for Id1
and [2,3]
for Id2
. We don't want to use join (see side note), so let's use Contains
:
from entity in db.Table where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2) select entity
But now the results also contains entity {1,3}
! Well, of course, this entity perfectly matches the two predicates. But let's keep in mind that we're getting closer. In stead of pulling millions of entities into memory, we now only get four of them.
Contains
with computed valuesSolution 3 failed because the two separate Contains
statements don't only filter the combinations of their values. What if we create a list of combinations first and try to match these combinations? We know from solution 1 that this list should contain primitive values. For instance:
var computed = ids1.Zip(ids2, (i1,i2) => i1 * i2); // [2,6]
and the LINQ statement:
from entity in db.Table where computed.Contains(entity.Id1 * entity.Id2) select entity
There are some problems with this approach. First, you'll see that this also returns entity {1,6}
. The combination function (a*b) does not produce values that uniquely identify a pair in the database. Now we could create a list of strings like ["Id1=1,Id2=2","Id1=2,Id2=3]"
and do
from entity in db.Table where computed.Contains("Id1=" + entity.Id1 + "," + "Id2=" + entity.Id2) select entity
(This would work in EF6, not in earlier versions).
This is getting pretty messy. But a more important problem is that this solution is not sargable, which means: it bypasses any database indexes on Id1
and Id2
that could have been used otherwise. This will perform very very poorly.
So the only viable solution I can think of is a combination of Contains
and a join
in memory: First do the contains statement as in solution 3. Remember, it got us very close to what we wanted. Then refine the query result by joining the result as an in-memory list:
var rawSelection = from entity in db.Table where ids1.Contains(entity.Id1) && ids2.Contains(entity.Id2) select entity; var refined = from entity in rawSelection.AsEnumerable() join pair in Tuples on new { entity.Id1, entity.Id2 } equals new { Id1 = pair.Item1, Id2 = pair.Item2 } select entity;
It's not elegant, messy all the same maybe, but so far it's the only scalable2 solution to this problem I found, and applied in my own code.
Using a Predicate builder like Linqkit or alternatives, you can build a query that contains an OR clause for each element in the list of combinations. This could be a viable option for really short lists. With a couple of hundreds of elements, the query will start performing very poorly. So I don't consider this a good solution unless you can be 100% sure that there will always be a small number of elements. One elaboration of this option can be found here.
1As a funny side note, EF does create a SQL statement when you join a primitive list, like so
from entity in db.Table // db is a DbContext join i in MyIntegers on entity.Id1 equals i select entity
But the generated SQL is, well, absurd. A real-life example where MyIntegers
contains only 5(!) integers looks like this:
SELECT [Extent1].[CmpId] AS [CmpId], [Extent1].[Name] AS [Name], FROM [dbo].[Company] AS [Extent1] INNER JOIN (SELECT [UnionAll3].[C1] AS [C1] FROM (SELECT [UnionAll2].[C1] AS [C1] FROM (SELECT [UnionAll1].[C1] AS [C1] FROM (SELECT 1 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable1] UNION ALL SELECT 2 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable2]) AS [UnionAll1] UNION ALL SELECT 3 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable3]) AS [UnionAll2] UNION ALL SELECT 4 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable4]) AS [UnionAll3] UNION ALL SELECT 5 AS [C1] FROM ( SELECT 1 AS X ) AS [SingleRowTable5]) AS [UnionAll4] ON [Extent1].[CmpId] = [UnionAll4].[C1]
There are n-1 UNION
s. Of course that's not scalable at all.
Later addition:
Somewhere along the road to EF version 6.1.3 this has been greatly improved. The UNION
s have become simpler and they are no longer nested. Previously the query would give up with less than 50 elements in the local sequence (SQL exception: Some part of your SQL statement is nested too deeply.) The non-nested UNION
allow local sequences up to a couple of thousands(!) of elements. It's still slow though with "many" elements.
2As far as the Contains
statement is scalable: Scalable Contains method for LINQ against a SQL backend
You can use Union
for each composite primary key:
var compositeKeys = new List<CK> { new CK { id1 = 1, id2 = 2 }, new CK { id1 = 1, id2 = 3 }, new CK { id1 = 2, id2 = 4 } }; IQuerable<CK> query = null; foreach(var ck in compositeKeys) { var temp = context.Table.Where(x => x.id1 == ck.id1 && x.id2 == ck.id2); query = query == null ? temp : query.Union(temp); } var result = query.ToList();
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With