Skip to content

Treat lookups by key as bounded, and skip Includes EF ignores - #17

Merged
SimonCropp merged 1 commit into
mainfrom
key-lookups-and-ignored-includes
Sep 26, 2026
Merged

SimonCropp merged 1 commit into
mainfrom
key-lookups-and-ignored-includes

Conversation

@SimonCropp

Copy link
Copy Markdown
Owner

Two false positives, both found by comparing the checks with the rules in LinqContraband (LC031 and LC049).

Lookups by key are bounded

RejectUnbounded fired for Employees.Where(_ => _.Id == id), which returns at most one row. A Where that compares a key with a value now bounds the query like a Take:

  • Other conditions can be added with &&. With ||, each side has to be a lookup. A composite key needs every part compared.
  • The key is the primary key or an alternate key, read from the model, so shadow keys and EF.Property work. A unique index does not count: a filter can make it unique among only some rows, and a column that allows null can hold null in many rows.
  • ids.Contains(_.Id) returns a row for each value, so it is only bounded when MaxInValues is set. The log and throw levels each decide that with their own MaxInValues, so the shape carries the unbounded row types both with lists limited and without.
  • Only the rows of a DbSet can be looked up, with only filters, ordering, Skip, Take, Distinct, OfType and options such as Include between them. After Concat, SelectMany, Join or Select, or on FromSql, the same key can be in many rows.

Includes Entity Framework ignores are not counted

MaxSingleQueryCollections counted 2 for Departments.Include(_ => _.Employees).Include(_ => _.Projects).Select(_ => _.Name), which loads no collections. An Include before a Select that returns no entity, or an aggregate like Count(), is now skipped. A projection that could return an entity, including one passing it to a method, keeps the Includes counted. MaxIncludes and MaxIncludeDepth still count every Include as written.

Checked against Entity Framework on six queries: in each, the count agrees with Entity Framework's MultipleCollectionIncludeWarning and with the joins in the SQL it generates.

Tests

38 new tests, in KeyLookupTests (with a KeyContext model for composite, alternate, unique index and derived keys), UnboundedTests and ShapeTests. The full suite passes locally, 216 tests including the LocalDB ones.

RejectUnbounded no longer fires for a Where that compares a primary or alternate key with a value, such as Where(_ => _.Id == id), since it returns at most one row. A key looked up in a list, such as ids.Contains(_.Id), is only bounded when MaxInValues is set, and each set of levels decides that with its own MaxInValues. A lookup only counts on the rows of a DbSet, since after Concat, SelectMany, Join, Select or FromSql the same key can be in many rows.

MaxSingleQueryCollections no longer counts an Include that a later Select or aggregate makes Entity Framework ignore, since the query returns no entity for it to load into. A projection that could return an entity keeps the Includes counted.
@SimonCropp SimonCropp added this to the 0.5.0 milestone Sep 26, 2026
@SimonCropp
SimonCropp merged commit 4873e75 into main Sep 26, 2026
5 checks passed
@SimonCropp
SimonCropp deleted the key-lookups-and-ignored-includes branch September 26, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant