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

Change the severity in .editorconfig:

[*.cs]
dotnet_diagnostic.LC011.severity = error

Sample code · Analyzer source

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 Id or {EntityName}Id properties.
  • Data annotations: public mapped properties with System.ComponentModel.DataAnnotations.KeyAttribute.
  • EF Core composite keys: class-level Microsoft.EntityFrameworkCore.PrimaryKeyAttribute where 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 as entity.ToTable("Products").HasKey(...). Entity types declared in a referenced project resolve like the context’s own.
  • Configuration helpers: source methods that OnModelCreating passes its ModelBuilder or an EntityTypeBuilder<TEntity> to, such as a static Product.OnModelCreating(modelBuilder) or a modelBuilder.ConfigureCatalog() extension, followed up to four calls deep. A helper that is never called from OnModelCreating does not count.
  • Builder methods passed as method groups: modelBuilder.Entity<Package>(BuildPackageEntity), where BuildPackageEntity(EntityTypeBuilder<Package> package) calls package.HasKey(...), as BaGet does. this.Build... and SomeClass.Build... method groups count the same way.
  • C# 14 extension blocks: a helper declared as extension(ModelBuilder modelBuilder) { public void ConfigureStore(...) { ... } } or extension(EntityTypeBuilder<Tag> tag) { ... } is followed like a classic this ModelBuilder extension method, as in Duende IdentityServer’s ConfigurePersistedGrantContext. The key shape does not matter: HasKey(x => new { x.UserCode }) counts like any other HasKey.
  • Applied configurations: inline or local modelBuilder.ApplyConfiguration(...) calls, and ApplyConfigurationsFromAssembly(...) when the assembly argument is proven to be the current source assembly (typeof(LocalType).Assembly, System.Reflection.Assembly.GetExecutingAssembly(), the context’s own GetType().Assembly or this.GetType().Assembly when the context is not abstract, global::System.Reflection.Assembly.GetExecutingAssembly(), a using Assembly = System.Reflection.Assembly alias, or an unshadowed local/readonly-member alias of those expressions) and the configuration’s Configure method calls HasKey(...) or HasNoKey() for its own EntityTypeBuilder<TEntity>.
  • Intentional keyless types: Microsoft.EntityFrameworkCore.KeylessAttribute or HasNoKey().
  • Many-to-many join entities: the type argument of EF Core’s UsingEntity<TJoin>(...), declared in the referenced Microsoft.EntityFrameworkCore assembly itself (a project method, or one from another assembly such as Microsoft.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 as OwnsOne<Address>(...), and inferred ownership such as OwnsOne(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.