Analyzer rule
LC011: Entity Missing Primary Key
LC011 detects EF Core entity types exposed through a DbSet that have no primary key from convention, a [Key] attribute or fluent configuration.
- Default severity
- Warning
- Code fix
- Available
- Category
- Design
- Domain
- Schema & Modeling
Change the severity in .editorconfig:
[*.cs]
dotnet_diagnostic.LC011.severity = error
LC011: Entity Missing Primary Key
In Plain Terms
Imagine a library where books have no titles or ISBN numbers. You ask for a book, but because there’s no unique way to identify it, the librarian can’t find it, or worse, gives you the wrong one.
Goal
Detect DbSet<TEntity> entity types that EF Core would treat as regular tracked entities but that have no primary key definition.
The Problem
EF Core needs a primary key for tracked entities so it can identify rows, perform updates/deletes, resolve relationships, and maintain identity in the change tracker. A missing key usually fails model validation at startup or leaves the entity usable only as a keyless query type when that was not intended.
Example Violation
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
}
public class Product
{
public string Name { get; set; }
}
Supported Safe Shapes
LC011 treats these as valid primary-key or intentional opt-out patterns:
- Convention keys: public mapped
Idor{EntityName}Idproperties. - Data annotations: public mapped properties with
System.ComponentModel.DataAnnotations.KeyAttribute. - EF Core composite keys: class-level
Microsoft.EntityFrameworkCore.PrimaryKeyAttributewhere all referenced properties exist, are public, mapped, and key-compatible. - Fluent API keys in
OnModelCreating:modelBuilder.Entity<TEntity>().HasKey(...),modelBuilder.Entity<TEntity>(b => b.HasKey(...)), scoped local builder variables, and chained builder calls such asentity.ToTable("Products").HasKey(...). Entity types declared in a referenced project resolve like the context’s own. - Configuration helpers: source methods that
OnModelCreatingpasses itsModelBuilderor anEntityTypeBuilder<TEntity>to, such as a staticProduct.OnModelCreating(modelBuilder)or amodelBuilder.ConfigureCatalog()extension, followed up to four calls deep. A helper that is never called fromOnModelCreatingdoes not count. - Builder methods passed as method groups:
modelBuilder.Entity<Package>(BuildPackageEntity), whereBuildPackageEntity(EntityTypeBuilder<Package> package)callspackage.HasKey(...), as BaGet does.this.Build...andSomeClass.Build...method groups count the same way. - C# 14 extension blocks: a helper declared as
extension(ModelBuilder modelBuilder) { public void ConfigureStore(...) { ... } }orextension(EntityTypeBuilder<Tag> tag) { ... }is followed like a classicthis ModelBuilderextension method, as in Duende IdentityServer’sConfigurePersistedGrantContext. The key shape does not matter:HasKey(x => new { x.UserCode })counts like any otherHasKey. - Applied configurations: inline or local
modelBuilder.ApplyConfiguration(...)calls, andApplyConfigurationsFromAssembly(...)when the assembly argument is proven to be the current source assembly (typeof(LocalType).Assembly,System.Reflection.Assembly.GetExecutingAssembly(), the context’s ownGetType().Assemblyorthis.GetType().Assemblywhen the context is not abstract,global::System.Reflection.Assembly.GetExecutingAssembly(), ausing Assembly = System.Reflection.Assemblyalias, or an unshadowed local/readonly-member alias of those expressions) and the configuration’sConfiguremethod callsHasKey(...)orHasNoKey()for its ownEntityTypeBuilder<TEntity>. - Intentional keyless types:
Microsoft.EntityFrameworkCore.KeylessAttributeorHasNoKey(). - Many-to-many join entities: the type argument of EF Core’s
UsingEntity<TJoin>(...), declared in the referencedMicrosoft.EntityFrameworkCoreassembly itself (a project method, or one from another assembly such asMicrosoft.EntityFrameworkCore.Extensions, does not count, even in that namespace), which EF Core keys by convention with the composite of its two foreign keys. - Owned types:
Microsoft.EntityFrameworkCore.OwnedAttribute, generic ownership such asOwnsOne<Address>(...), and inferred ownership such asOwnsOne(e => e.Address)/OwnsMany(e => e.Items).
Attributes are namespace-checked. A custom class named KeyAttribute, PrimaryKeyAttribute, or KeylessAttribute does not suppress LC011.
Fixes
Preferred fixes are domain-specific:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
modelBuilder.Entity<Product>().HasKey(p => p.ProductCode);
modelBuilder.ApplyConfiguration(new ProductConfiguration());
public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.ProductCode);
}
}
The code fix offers public int Id { get; set; } only when the entity is source-editable and has no existing Id member. It intentionally avoids generating a duplicate Id when the current member is private, ignored, or a navigation property; choose the correct domain key manually in those cases.
Analyzer Logic
ID: LC011
Category: Design
Severity: Warning
LC011 starts from DbSet<TEntity> members on source DbContext types, checks the entity’s inheritance chain for valid mapped key properties, then folds in keyless/owned/fluent configuration discovered from the analyzed context. Standalone IEntityTypeConfiguration<TEntity> classes are not trusted unless the context applies them, preventing one context’s configuration from suppressing diagnostics in another context.
Malformed self-referential builder locals in OnModelCreating are treated as unresolved builder expressions rather than followed recursively. This keeps live IDE analysis from crashing on incomplete edits such as a local variable that accidentally refers to itself before it has been declared.