EntityFramework
Composite Key
Contains Query
Database
.NET Core

EntityFramework - contains query of composite key

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Filtering Entity Framework queries by a list of composite keys is harder than filtering by a single key. Contains works naturally for one scalar value, but translation becomes trickier when the key is a pair or tuple of columns. The right solution depends on how many keys you have and whether you need a pure LINQ expression or a more database-oriented approach.

Why Plain Contains Gets Awkward

For a single key, EF can usually translate this cleanly:

csharp
1var ids = new[] { 1, 2, 3 };
2var rows = await db.OrderDetails
3    .Where(x => ids.Contains(x.OrderId))
4    .ToListAsync();

With a composite key such as (OrderId, ProductId), developers often try something conceptually similar:

csharp
var keys = new[] { (1, 10), (2, 20) };

The problem is that not every provider and EF version translates composite Contains patterns the same way. Some combinations work, some fall back badly, and some fail outright.

So the safe question is not “can EF ever do this?” It is “what pattern will translate predictably for my query shape?”

Small Key Sets: Build an OR Predicate

If the list of composite keys is small, the most predictable LINQ-only solution is to build a predicate that expands into OR conditions.

csharp
1using System.Linq.Expressions;
2
3public static Expression<Func<OrderDetail, bool>> BuildCompositeKeyPredicate(
4    IReadOnlyList<(int OrderId, int ProductId)> keys)
5{
6    var param = Expression.Parameter(typeof(OrderDetail), "x");
7    Expression body = Expression.Constant(false);
8
9    foreach (var key in keys)
10    {
11        var orderMatch = Expression.Equal(
12            Expression.Property(param, nameof(OrderDetail.OrderId)),
13            Expression.Constant(key.OrderId)
14        );
15
16        var productMatch = Expression.Equal(
17            Expression.Property(param, nameof(OrderDetail.ProductId)),
18            Expression.Constant(key.ProductId)
19        );
20
21        var pairMatch = Expression.AndAlso(orderMatch, productMatch);
22        body = Expression.OrElse(body, pairMatch);
23    }
24
25    return Expression.Lambda<Func<OrderDetail, bool>>(body, param);
26}

Usage:

csharp
1var keys = new List<(int, int)> { (1, 10), (2, 20) };
2var predicate = BuildCompositeKeyPredicate(keys);
3
4var rows = await db.OrderDetails
5    .Where(predicate)
6    .ToListAsync();

This is verbose, but it translates cleanly because EF sees a normal boolean expression tree.

Why Any Over an In-Memory List Is Risky

A common attempt is:

csharp
var rows = await db.OrderDetails
    .Where(x => keys.Any(k => k.OrderId == x.OrderId && k.ProductId == x.ProductId))
    .ToListAsync();

Depending on EF version and provider, this may or may not translate well. Even when it works in one environment, it is not always the most predictable path for production queries.

That is why many teams prefer explicit predicate building for small lists and database-side techniques for large lists.

Large Key Sets: Move the Keys to the Database

If the list is large, dynamically generating a long OR chain is not ideal. Better options include:

  • temporary tables,
  • table-valued parameters on SQL Server,
  • staging keys into a real table and joining,
  • raw SQL for the specific workload.

Conceptually, once the filter list becomes “data” instead of “a few constants,” the database should usually own that data instead of EF expanding it inline.

A join-based pattern is often more scalable than trying to force all keys through a huge LINQ expression.

Composite Key Modeling Matters Too

Make sure the entity is configured correctly in EF Core:

csharp
1protected override void OnModelCreating(ModelBuilder modelBuilder)
2{
3    modelBuilder.Entity<OrderDetail>()
4        .HasKey(x => new { x.OrderId, x.ProductId });
5}

This configuration does not solve the filtering problem by itself, but it ensures EF understands the entity identity model correctly.

A Practical Rule of Thumb

Use scalar Contains for single-column keys.

Use generated OR predicates for small composite key lists.

Use database-side staging or joins for large composite key lists.

That rule is not glamorous, but it avoids most translation surprises.

Common Pitfalls

  • Assuming composite-key Contains works as predictably as scalar Contains across all EF versions and providers.
  • Building huge OR predicates for large key lists and then wondering why performance degrades.
  • Using in-memory Any patterns without verifying the generated SQL or translation behavior.
  • Forgetting to configure the composite key correctly in the EF model before debugging query behavior.
  • Treating a large list of composite filter values as a pure LINQ problem when it should really be modeled as database-side data.

Summary

  • Composite-key filtering is harder than scalar Contains because query translation is less uniform.
  • For small key sets, a dynamically built OR predicate is often the most predictable LINQ solution.
  • For large key sets, move the filter data into the database and join against it.
  • Always verify the generated SQL when using provider-sensitive translation patterns.
  • Model the composite key explicitly in EF so the query logic rests on a correct entity definition.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.