This demonstrates how to configure one-to-many and many-to-many relationships in a code-first Entity Framework ASP.Net MVC application. The sample application is a blog containing posts, post categories (one-to-many), and post tags (many-to-many).
Note: When initializing a new object, e.g. a new Post, the collection of related entities will be null, so doing something like myNewPost.Tags.Add(new Tag())
will result in a null error. Either manually set myNewPost.Tags
to a new list of Tags, or do more fancy collection initialization as described in this StackOverflow answer.
One-To-Many Relationship
In this example, a post can be assigned an optional, single category. To set this up, the Post model needs to include a CategoryID property (nullable int) and a Category property (of type Category), and the Category model needs to contain a collection of Posts.
Post Model:
- CategoryId Property:
- Category ID, foreign key to the assigned Category
- Category Property:
- Category navigation property referenced by the CategoryId. It is `virtual` to enable lazy loading, which delays the loading of the Category object until it is accessed later on. (It actually creates and loads a derived proxy type.) Note that lazy loading only works when the property is first accessed in the same db context.
public class Post {
[Key]
public int Id { get; set; }
public String Title { get; set; }
public String Content { get; set; }
public int CategoryId { get; set; } // FK
public virtual Category Category { get; set; } // Navigation Property
}
Category Model:
- Posts Property:
- A collection navigation property containing posts assigned to this category. Again, `virtual` enables lazy loading, so values are not loaded until referenced.
public class Category {
[Key]
public int Id { get; set; }
[Required]
[Display(Name = "Category Name")]
public String Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } // Navigation Property
}
public Post() {
Tags = new List<Tag>(); // Prevent null reference when adding tags to a new post
}
Many-To-Many Relationship
Note: These instructions work for Entity Framework 6. For Entity Framework Core, additional manual steps are required to get a many-to-many relationship to work. More on that in this post: Relationships in Entity Framework Core 3
As an example of a many-to-many relationship, a post can be assigned multiple tags. To set this up, each model needs to contain a collection navigation property of the other model type. The Post model needs to contain a collection of Tags, and the Tag model needs to contain a collection of Posts.
In the database, neither the Posts nor Tags table will contain a reference to each other. Instead, a 3rd table will be automatically created with a composite PK made up of the FK for both Posts and Tags. In my case, the table was automatically named TagPosts, when I would have chosen PostTags; however, that won't affect how the many-to-many relationship works. (If you want a different table name, you'll have to use the Fluent method instead. It's explained this StackOverflow answer.)
Updated Post Model:
- Tags Property:
- Tags collection navigation property to reference all tags assigned to this post.
public class Post {
[Key]
public int Id { get; set; }
public String Title { get; set; }
public String Content { get; set; }
public int CategoryId { get; set; } // FK
public virtual Category Category { get; set; } // Navigation Property
public virtual ICollection<Tag> Tags { get; set; } // Navigation Property
}
Tag Model:
- Posts Property:
- Posts collection navigation property to reference all posts assigned to this tag.
public class Tag {
[Key]
public int Id { get; set; }
[Required]
[Display(Name = "Tag Name")]
public String Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } // Navigation Property
}
Adding/Updating Related Data
Adding data to a relationship is very simple. In the case of a one-to-many relationship, it's as easy as populating the parameter on the object. E.g.: post.Category = <some category>
. You'll encounter various scenarios (object loaded in current context or not, object is existing or new, etc.), and the many-to-many examples below demonstrate how to ensure the object is populated and included in the current context.
In the case of a many-to-many relationship, we add an entity to a collection (e.g. post.Tags.Add(<some tag>)
), and we can add it from either side of the relationship (e.g. tag.Posts.Add(<some post>)
). The following examples cover various ways to add tags to a post.
Scenario 1: If it's an existing entity (Tag) loaded in the current db context, simply add it to the related collection (Post.Tags):
post.Tags.Add(tag);
context.SaveChanges();
Scenario 2: If it's an existing entity (Tag) that wasn't loaded from the current context (maybe you received it via a parameter), attach it to the current context (context.Tags), then add it to the related collection:
context.Tags.Attach(tag);
post.Tags.Add(tag);
context.SaveChanges();
Scenario 3: If it's an existing entity (Tag) but you only have the Id, create a new entity with only the Id populated, attach it to the context, then add it to the related collection:
Tag tag = new Tag() { Id = t.Id };
context.Tags.Attach(tag);
post.Tags.Add(tag);
context.SaveChanges();
Scenario 4: If it's an entirely new entity (Tag), create the entity with details populated, add it to its own model collection (context.Tags), then add it to the related collection (Post.Tags):
Tag newTag = new Tag() { Name = "Some Value" };
context.Tags.Add(newTag);
post.Tags.Add(newTag);
context.SaveChanges();
Cascading Deletes
Whether or not ON DELETE CASCADE
is added to the relationship is dependent on whether or not the foreign key property in TableA is nullable (int vs int?
).
If the foreign key property in TalbeA is nullable, the relationship will be created without cascading deletions. When an entity in TableB is deleted, and it is referenced by any entities in TableA, an error will occur and the deletion will be blocked.
If the foreign key property in TableA is not nullable, the relationship will be created with cascading deletions (ON DELETE CASCADE
). When an entity in TableB is deleted, and it is referenced by any entities in TableA, the entities in TableA will also be deleted.
Model:
public int? CategoryId { get; set; }
T-SQL Generated: (no cascading)
CREATE TABLE [dbo].[Posts] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Title] NVARCHAR (MAX) NULL, [Content] NVARCHAR (MAX) NULL, [CategoryId] INT NULL, CONSTRAINT [PK_dbo.Posts] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_dbo.Posts_dbo.Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([Id]) ); GO CREATE NONCLUSTERED INDEX [IX_CategoryId] ON [dbo].[Posts]([CategoryId] ASC);
Model:
public int CategoryId { get; set; }
T-SQL Generated: (includes ON DELETE CASCADE
)
CREATE TABLE [dbo].[Posts] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Title] NVARCHAR (MAX) NULL, [Content] NVARCHAR (MAX) NULL, [CategoryId] INT NOT NULL, CONSTRAINT [PK_dbo.Posts] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_dbo.Posts_dbo.Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([Id]) ON DELETE CASCADE ); GO CREATE NONCLUSTERED INDEX [IX_CategoryId] ON [dbo].[Posts]([CategoryId] ASC);
If you don't want the foreign key to be nullable (i.e. a regular int
), and you also don't want cascading deletes, you will have to override the OnModelCreating
function and manually configure the relationship using Fluent:
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
modelBuilder.Entity()
.HasRequired(p => p.Category)
.WithMany(c => c.Posts)
.HasForeignKey(p => p.CategoryId)
.WillCascadeOnDelete(false);
}