Analyzer rule
LC027: Missing Explicit Foreign Key Property
LC027 flags EF Core reference navigations whose dependent entity has no matching foreign key property, leaving EF Core to create a shadow key.
- Default severity
- Info
- Code fix
- Available
- Category
- Design
- Domain
- Schema & Modeling
Change the severity in .editorconfig:
[*.cs]
dotnet_diagnostic.LC027.severity = error
LC027: Missing Explicit Foreign Key Property
In Plain Terms
Imagine you have a friend’s phone number written on a secret sticky note hidden under your desk. When someone asks “What’s your friend’s number?”, you have to crawl under the desk to find it. If you just wrote the number in your address book (an explicit FK), anyone could look it up instantly.
What it flags
Flags public reference navigation properties between DbSet entity types when the dependent entity does not expose a matching foreign-key property and the relationship is not otherwise configured.
Why it matters
Shadow foreign keys make it harder to set or inspect relationships without loading the navigation entity. Explicit FK properties are easier to serialize, update, and reason about during schema reviews.
Typical fix
Add the concrete foreign-key property and align the relationship configuration so the model exposes the key directly. The code fix inserts <NavigationName>Id above the navigation, uses the principal entity’s primary-key type when it can be found from conventions, real key attributes, or visible single-property Fluent HasKey(...) configuration, and keeps optional nullable navigations nullable for value-type keys. The key type is written as it must be spelled in the dependent entity’s file, so a key type from a namespace that file does not import (for example a strongly typed ID) comes out namespace-qualified instead of as a bare name that would not compile.
Safe shapes
LC027 does not report collection navigations, owned types configured with OwnsOne / OwnsMany, relationships configured with HasForeignKey(...), navigations or FK properties annotated with [ForeignKey], or conventional FK properties such as CustomerId / CustomerTypeId.
The Fluent configuration check includes direct chains and a single-assignment relationship-builder local:
var relationship = builder.HasOne(o => o.Customer).WithMany(c => c.Orders);
relationship.HasForeignKey("CustomerShadowId");
One-to-one builder continuations are supported too:
var relationship = builder.HasOne(o => o.Customer);
relationship.WithOne(c => c.Order).HasForeignKey("CustomerShadowId");
If the relationship-builder local is reassigned before HasForeignKey(...), LC027 stays conservative because the configured navigation is ambiguous.
The configuration can live in the project’s own helpers that OnModelCreating calls with the ModelBuilder or an EntityTypeBuilder<T>, up to four calls deep, and the chain can start from an entity-builder local or an Entity<T>(b => ...) lambda:
protected override void OnModelCreating(ModelBuilder builder) => ApiKeyData.OnModelCreating(builder);
public static void OnModelCreating(ModelBuilder builder)
{
var b = builder.Entity<ApiKeyData>();
b.HasOne(o => o.StoreData).WithMany(s => s.ApiKeys).HasForeignKey(o => o.StoreId);
}
LC027 also skips navigations EF Core does not map: [NotMapped] properties and computed properties without a setter or a conventionally named backing field, such as public PlanData NextPlan => NewPlan ?? Plan;. A navigation whose target type has a {Entity}Id property back to this entity, such as User.ProfileImage with ImageInfo.UserId, is the principal side of a one-to-one and has no key to add.
When a team intentionally uses a configured shadow FK, keep the Fluent configuration explicit. LC027 treats that as intentional model design and does not offer a fixer.
Samples
See samples/LinqContraband.Sample/Samples/LC027_MissingExplicitForeignKey/ for a focused example.
The crime
public sealed class Order
{
public Customer Customer { get; set; } = default!;
}
A better shape
public sealed class Order
{
public int CustomerId { get; set; }
public Customer Customer { get; set; } = default!;
}
Optional relationships should keep the FK optional too:
public sealed class Order
{
public int? CustomerId { get; set; }
public Customer? Customer { get; set; }
}