Analyzer rule
LC017: Whole Entity Projection
LC017 flags EF Core queries that load whole entities when only a few properties are used. Project with Select to fetch just the needed columns.
- Default severity
- Info
- Code fix
- Available
- Category
- Performance
Change the severity in .editorconfig:
[*.cs]
dotnet_diagnostic.LC017.severity = error
LC017: Whole Entity Projection
In Plain Terms
Imagine you want to know your friend’s phone number. Instead of asking for just the number, you ask them to recite their entire autobiography—name, address, favorite foods, every vacation they’ve ever taken. You only needed one fact, but you got a whole book.
Overview
Diagnostic ID: LC017 Severity: Info Category: Performance
What It Detects
LC017 detects Entity Framework Core queries that load entire entities when only a small subset of properties are actually accessed. This is a common performance anti-pattern that wastes bandwidth, memory, and CPU.
The Crime
// Bad: Loads all 12 columns, but only uses Name
var products = context.Products.Where(p => p.Price > 100).ToList();
foreach (var p in products)
{
Console.WriteLine(p.Name); // Only Name is ever accessed!
}
When you query entities without a .Select() projection, EF Core retrieves ALL columns from the database. If your entity has many properties but you only use a few, you’re:
- Transferring unnecessary data over the network
- Allocating memory for unused properties
- Potentially causing change tracking overhead for data you don’t need
The Fix
Use .Select() to project only the properties you need:
// Good: Projects only the needed property
var names = context.Products
.Where(p => p.Price > 100)
.Select(p => p.Name)
.ToList();
foreach (var name in names)
{
Console.WriteLine(name);
}
Or project into a DTO:
// Good: Project into a DTO with only needed fields
var products = context.Products
.Where(p => p.Price > 100)
.Select(p => new ProductSummary
{
Id = p.Id,
Name = p.Name
})
.ToList();
Why This Matters
Performance Impact
Consider an entity with 12 properties, including a large Description field:
| Approach | Data Retrieved | Memory Used |
|---|---|---|
| No projection | All 12 columns (~2KB per row) | ~2MB for 1000 rows |
With .Select(p => p.Name) |
1 column (~50 bytes per row) | ~50KB for 1000 rows |
That’s a 40x reduction in data transfer and memory usage.
Additional Benefits
- Faster queries: SQL Server only reads necessary columns from disk/memory
- No change tracking: Projections to non-entity types bypass EF’s change tracker
- Cleaner code: DTOs explicitly declare what data the code needs
Conservative Detection
LC017 uses conservative detection to minimize false positives:
- Only flags large entities: Entities must have 10+ properties
- Only flags clear waste: Must access only 1-2 properties of the entity
- Only flags local usage: Skips when entities are returned from methods
- Skips external method calls: If entity is passed to another method, can’t track usage
- Skips updates: If a property of the entity is written (
e.Status = ...,e.Count++,e.Total += x), the query loads tracked entities to change and save them, which a projection cannot do - Skips stored results: If the list or an entity is assigned to a field, property, another local, an object initializer, an array or a tuple, its later use is out of sight
- Skips lambdas: If entity is used in a lambda/delegate, can’t reliably track
- Collection queries only: Flags
ToList()/ToArray(), not single-entityFirst()/Single()
When LC017 Does NOT Trigger
- Entities are changed: a write to the entity or through its navigations (
e.Owner.Name = ...,e.Visits++), a change to a navigation collection (e.Tags.Add(...),Remove,Clear), or a call to one of the entity’s own methods (e.Archive()) means the method loads the entities to update them. A projection returns untracked values, soSaveChangeswould persist nothing.// OK: the loop changes the tracked graph var libraries = context.Libraries.Include(l => l.FileTypes).ToList(); foreach (var library in libraries) library.FileTypes.Add(new LibraryFileType { Name = "epub" }); context.SaveChanges(); - Already using projection:
// OK: Already projected var names = context.Products.Select(p => p.Name).ToList(); - Small entities:
// OK: Entity has only 3 properties - not worth flagging var users = context.SmallEntities.ToList(); - Entity is returned:
// OK: Can't track how caller uses the entity public List<Product> GetProducts() => context.Products.ToList(); - Entity passed to method:
// OK: Can't track usage in external method var products = context.Products.ToList(); ProcessProducts(products); - Entities are updated:
// OK: A projection has read-only members and nothing to save. // For a bulk change like this, consider ExecuteUpdate (LC032). var products = context.Products.Where(p => p.Discontinued).ToList(); foreach (var p in products) p.UpdatedAt = now; context.SaveChanges(); - Result is stored:
// OK: The view model is used elsewhere var products = context.Products.ToList(); var vm = new CatalogViewModel { Products = products }; - Most properties accessed:
// OK: Accessing 7+ of 12 properties justifies full load foreach (var p in context.Products.ToList()) { Console.WriteLine($"{p.Id} {p.Name} {p.Description} {p.Price}..."); }
Code Fix
LC017 provides an automatic code fix that adds a .Select() projection before the materializer. The fix:
- Analyzes property accesses: Determines which properties of the entity are actually used in subsequent code
- Generates anonymous type projection: Creates a
.Select(e => new { e.Prop1, e.Prop2 })with only the accessed properties - Preserves supported access shapes: Includes properties used through direct foreach access, null-conditional access, and indexed collection access
- Declines unsafe conversion shapes: Keeps cast, interface, or conversion-based entity property access such as
((IHasName)e).Nameor((IHasName)e)?.Namediagnostic-only so the fix does not project a partial anonymous type while later code still needs the original entity - Sorts properties alphabetically: Ensures consistent, predictable output
Before Fix
var entities = db.LargeEntities.ToList();
foreach (var e in entities)
{
Console.WriteLine(e.Name);
}
After Fix
var entities = db.LargeEntities.Select(e => new { e.Name }).ToList();
foreach (var e in entities)
{
Console.WriteLine(e.Name);
}
Note: After applying the fix, you may need to adjust your code to work with the anonymous type instead of the full entity. Consider creating a named DTO class for better maintainability.
Configuration
You can configure the severity in your .editorconfig:
# Make LC017 an error
dotnet_diagnostic.LC017.severity = error
# Disable LC017
dotnet_diagnostic.LC017.severity = none
Related Analyzers
- LC002: Premature Materialization - Detects
ToList()before filtering - LC009: Missing AsNoTracking - Suggests
AsNoTracking()for read-only queries