Relationships
A relationship defines how two entities relate to each other. In a relational database, this is represented by a foreign key constraint.
Note
Most of the samples in this article use a one-to-many relationship to demonstrate concepts. For examples of one-to-one and many-to-many relationships see the Other Relationship Patterns section at the end of the article.
Definition of terms
There are a number of terms used to describe relationships
Dependent entity: This is the entity that contains the foreign key properties. Sometimes referred to as the 'child' of the relationship.
Principal entity: This is the entity that contains the primary/alternate key properties. Sometimes referred to as the 'parent' of the relationship.
Principal key: The properties that uniquely identify the principal entity. This may be the primary key or an alternate key.
Foreign key: The properties in the dependent entity that are used to store the principal key values for the related entity.
Navigation property: A property defined on the principal and/or dependent entity that references the related entity.
Collection navigation property: A navigation property that contains references to many related entities.
Reference navigation property: A navigation property that holds a reference to a single related entity.
Inverse navigation property: When discussing a particular navigation property, this term refers to the navigation property on the other end of the relationship.
Self-referencing relationship: A relationship in which the dependent and the principal entity types are the same.
The following code shows a one-to-many relationship between Blog
and Post
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Post
is the dependent entityBlog
is the principal entityBlog.BlogId
is the principal key (in this case it is a primary key rather than an alternate key)Post.BlogId
is the foreign keyPost.Blog
is a reference navigation propertyBlog.Posts
is a collection navigation propertyPost.Blog
is the inverse navigation property ofBlog.Posts
(and vice versa)
Conventions
By default, a relationship will be created when there is a navigation property discovered on a type. A property is considered a navigation property if the type it points to cannot be mapped as a scalar type by the current database provider.
Note
Relationships that are discovered by convention will always target the primary key of the principal entity. To target an alternate key, additional configuration must be performed using the Fluent API.
Fully defined relationships
The most common pattern for relationships is to have navigation properties defined on both ends of the relationship and a foreign key property defined in the dependent entity class.
If a pair of navigation properties is found between two types, then they will be configured as inverse navigation properties of the same relationship.
If the dependent entity contains a property with a name matching one of these patterns then it will be configured as the foreign key:
<navigation property name><principal key property name>
<navigation property name>Id
<principal entity name><principal key property name>
<principal entity name>Id
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
In this example the highlighted properties will be used to configure the relationship.
Note
If the property is the primary key or is of a type not compatible with the principal key then it won't be configured as the foreign key.
No foreign key property
While it is recommended to have a foreign key property defined in the dependent entity class, it is not required. If no foreign key property is found, a shadow foreign key property will be introduced with the name <navigation property name><principal key property name>
or <principal entity name><principal key property name>
if no navigation is present on the dependent type.
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
In this example, the shadow foreign key is BlogId
because prepending the navigation name would be redundant.
Note
If a property with the same name already exists, then the shadow property name will be suffixed with a number.
Single navigation property
Including just one navigation property (no inverse navigation, and no foreign key property) is enough to have a relationship defined by convention. You can also have a single navigation property and a foreign key property.
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
}
Limitations
When there are multiple navigation properties defined between two types (that is, more than just one pair of navigations that point to each other) the relationships represented by the navigation properties are ambiguous. You will need to manually configure them to resolve the ambiguity.
Cascade delete
By convention, cascade delete will be set to Cascade for required relationships and ClientSetNull for optional relationships. Cascade means dependent entities are also deleted. ClientSetNull means that dependent entities that are not loaded into memory will remain unchanged and must be manually deleted, or updated to point to a valid principal entity. For entities that are loaded into memory, EF Core will attempt to set the foreign key properties to null.
See the Required and Optional Relationships section for the difference between required and optional relationships.
See Cascade Delete for more details about the different delete behaviors and the defaults used by convention.
Manual configuration
To configure a relationship in the Fluent API, you start by identifying the navigation properties that make up the relationship. HasOne
or HasMany
identifies the navigation property on the entity type you are beginning the configuration on. You then chain a call to WithOne
or WithMany
to identify the inverse navigation. HasOne
/WithOne
are used for reference navigation properties and HasMany
/WithMany
are used for collection navigation properties.
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
Single navigation property
If you only have one navigation property then there are parameterless overloads of WithOne
and WithMany
. This indicates that there is conceptually a reference or collection on the other end of the relationship, but there is no navigation property included in the entity class.
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne();
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
}
Configuring navigation properties
After the navigation property has been created, you may need to further configure it.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne();
modelBuilder.Entity<Blog>()
.Navigation(b => b.Posts)
.UsePropertyAccessMode(PropertyAccessMode.Property);
}
Tip
Non-collection navigations can also be marked as required, see Required one-to-one dependents for more information.
Note
This call cannot be used to create a navigation property. It is only used to configure a navigation property which has been previously created by defining a relationship or from a convention.
Foreign key
You can use the Fluent API to configure which property should be used as the foreign key property for a given relationship:
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogForeignKey);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}
Shadow foreign key
You can use the string overload of HasForeignKey(...)
to configure a shadow property as a foreign key (see Shadow Properties for more information). The shadow property needs to exist before it can be used, you can explicitly declare it first as shown below.
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Add the shadow property to the model
modelBuilder.Entity<Post>()
.Property<int>("BlogForeignKey");
// Use the shadow property as a foreign key
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey("BlogForeignKey");
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
Foreign key constraint name
By convention, when targeting a relational database, foreign key constraints are named FK_<dependent type name>_<principal type name>_<foreign key property name>. For composite foreign keys, <foreign key property name> becomes an underscore separated list of foreign key property names.
You can also configure the constraint name as follows:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId)
.HasConstraintName("ForeignKey_Post_Blog");
}
Without navigation property
You don't necessarily need to provide a navigation property. You can simply provide a foreign key on one side of the relationship.
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne<Blog>()
.WithMany()
.HasForeignKey(p => p.BlogId);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
}
Principal key
If you want the foreign key to reference a property other than the primary key, you can use the Fluent API to configure the principal key property for the relationship. The property that you configure as the principal key will automatically be set up as an alternate key.
internal class MyContext : DbContext
{
public DbSet<Car> Cars { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RecordOfSale>()
.HasOne(s => s.Car)
.WithMany(c => c.SaleHistory)
.HasForeignKey(s => s.CarLicensePlate)
.HasPrincipalKey(c => c.LicensePlate);
}
}
public class Car
{
public int CarId { get; set; }
public string LicensePlate { get; set; }
public string Make { get; set; }
public string Model { get; set; }
public List<RecordOfSale> SaleHistory { get; set; }
}
public class RecordOfSale
{
public int RecordOfSaleId { get; set; }
public DateTime DateSold { get; set; }
public decimal Price { get; set; }
public string CarLicensePlate { get; set; }
public Car Car { get; set; }
}
Required and optional relationships
You can use the Fluent API to configure whether the relationship is required or optional. Ultimately this controls whether the foreign key property is required or optional. This is most useful when you are using a shadow state foreign key. If you have a foreign key property in your entity class then the requiredness of the relationship is determined based on whether the foreign key property is required or optional (see Required and Optional properties for more information).
The foreign key properties are located on the dependent entity type, so if they are configured as required it means that every dependent entity is required to have a corresponding principal entity.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.IsRequired();
}
Note
Calling IsRequired(false)
also makes the foreign key property optional unless it's configured otherwise.
Cascade delete
You can use the Fluent API to configure the cascade delete behavior for a given relationship explicitly.
See Cascade Delete for a detailed discussion of each option.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.OnDelete(DeleteBehavior.Cascade);
}
Other relationship patterns
One-to-one
One to one relationships have a reference navigation property on both sides. They follow the same conventions as one-to-many relationships, but a unique index is introduced on the foreign key property to ensure only one dependent is related to each principal.
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public BlogImage BlogImage { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
Note
EF will choose one of the entities to be the dependent based on its ability to detect a foreign key property. If the wrong entity is chosen as the dependent, you can use the Fluent API to correct this.
When configuring the relationship with the Fluent API, you use the HasOne
and WithOne
methods.
When configuring the foreign key you need to specify the dependent entity type - notice the generic parameter provided to HasForeignKey
in the listing below. In a one-to-many relationship it is clear that the entity with the reference navigation is the dependent and the one with the collection is the principal. But this is not so in a one-to-one relationship - hence the need to explicitly define it.
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<BlogImage> BlogImages { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasOne(b => b.BlogImage)
.WithOne(i => i.Blog)
.HasForeignKey<BlogImage>(b => b.BlogForeignKey);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public BlogImage BlogImage { get; set; }
}
public class BlogImage
{
public int BlogImageId { get; set; }
public byte[] Image { get; set; }
public string Caption { get; set; }
public int BlogForeignKey { get; set; }
public Blog Blog { get; set; }
}
The dependent side is considered optional by default, but can be configured as required. However EF will not validate whether a dependent entity was provided, so this configuration will only make a difference when the database mapping allows it to be enforced. A common scenario for this are reference owned types that use table splitting by default.
modelBuilder.Entity<Order>(
ob =>
{
ob.OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.Property(p => p.Street).IsRequired();
sa.Property(p => p.City).IsRequired();
});
ob.Navigation(o => o.ShippingAddress)
.IsRequired();
});
With this configuration the columns corresponding to ShippingAddress
will be marked as non-nullable in the database.
Note
If you are using non-nullable reference types calling IsRequired
is not necessary.
Many-to-many
Many-to-many relationships require a collection navigation property on both sides. They will be discovered by convention like other types of relationships.
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public ICollection<Post> Posts { get; set; }
}
The way this relationship is implemented in the database is by a join table that contains foreign keys to both Post
and Tag
. For example this is what EF will create in a relational database for the above model.
CREATE TABLE [Posts] (
[PostId] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Content] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([PostId])
);
CREATE TABLE [Tags] (
[TagId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([TagId])
);
CREATE TABLE [PostTag] (
[PostsId] int NOT NULL,
[TagsId] nvarchar(450) NOT NULL,
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
CONSTRAINT [FK_PostTag_Posts_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([PostId]) ON DELETE CASCADE,
CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([TagId]) ON DELETE CASCADE
);
Internally, EF creates an entity type to represent the join table that will be referred to as the join entity type. Dictionary<string, object>
is currently used for it to handle any combination of foreign key properties, see property bag entity types for more information. More than one many-to-many relationships can exist in the model, therefore the join entity type must be given a unique name, in this case PostTag
. The feature that allows this is called shared-type entity type.
Important
The CLR type used for join entity types by convention may change in future releases to improve performance. Do not depend on the join type being Dictionary<string, object>
unless this has been explicitly configured, as described in the next section.
The many-to-many navigations are called skip navigations as they effectively skip over the join entity type. If you are employing bulk configuration all skip navigations can be obtained from GetSkipNavigations.
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var skipNavigation in entityType.GetSkipNavigations())
{
Console.WriteLine(entityType.DisplayName() + "." + skipNavigation.Name);
}
}
Join entity type configuration
It is common to apply configuration to the join entity type. This action can be accomplished via UsingEntity
.
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.ToTable("PostTags"));
Tip
If there is no navigation on the other side WithMany()
can be called without any arguments.
Model seed data can be provided for the join entity type by using anonymous types. You can examine the model debug view to determine the property names created by convention.
modelBuilder
.Entity<Post>()
.HasData(new Post { PostId = 1, Title = "First" });
modelBuilder
.Entity<Tag>()
.HasData(new Tag { TagId = "ef" });
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity(j => j.HasData(new { PostsPostId = 1, TagsTagId = "ef" }));
Additional data can be stored in the join entity type, but for this it's best to create a bespoke CLR type. When configuring the relationship with a custom join entity type both foreign keys need to be specified explicitly.
internal class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public DateTime PublicationDate { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
Joining relationships configuration
EF uses two one-to-many relationships on the join entity type to represent the many-to-many relationship. You can configure these relationships in the UsingEntity
arguments.
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, object>>(
"PostTag",
j => j
.HasOne<Tag>()
.WithMany()
.HasForeignKey("TagsId")
.HasConstraintName("FK_PostTag_Tags_TagId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne<Post>()
.WithMany()
.HasForeignKey("PostsId")
.HasConstraintName("FK_PostTag_Posts_PostId")
.OnDelete(DeleteBehavior.ClientCascade));
Note
The UsingEntity
overloads that don't have a Action<EntityTypeBuilder> configureJoinEntityType
parameter return an EntityTypeBuilder
for the join entity type, so the configuration can be chained. Also, starting with EF Core 7.0 there are overloads without a Type
parameter. These will assume that the type is Dictionary<string, object>
, which is recommended when you don't plan on using the join entity directly.
Indirect many-to-many relationships
You can also represent a many-to-many relationship by just adding the join entity type and mapping two separate one-to-many relationships.
public class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PostTag>()
.HasKey(t => new { t.PostId, t.TagId });
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public DateTime PublicationDate { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
Note
Support for scaffolding many-to-many relationships from the database is not yet added. See tracking issue.
Additional resources
- .NET Data Community Standup session, with a deep dive into Many-to-many and the infrastructure underpinning it.
Feedback
Submit and view feedback for