Problem
I am creating a simple model relation in ASP.Net core. My model relations are like this:
- A user can have many posts
- A post can have many comments
- A user can have many comments
Now I am trying to seed the relations using the dbcontext. I have a feeling that I am doing something wrong. Can anybody determine if I am doing it the right way or something can be improved?
Base entity has create and update timestamp only.
User model
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Transactions;
namespace SocialAppApi.Models
{
public class User : BaseEntity
{
[DatabaseGenerated( DatabaseGeneratedOption.Identity )] [Column( "id", Order = 1 )]
public int Id { get; set; }
[Column( "first_name", TypeName = "VARCHAR(100)", Order = 2 )]
public string FirstName { get; set; }
[Column( "middle_name", TypeName = "VARCHAR(100)", Order = 3 )]
public string MiddleName { get; set; }
[Column( "last_name", TypeName = "VARCHAR(100)", Order = 4 )]
public string LastName { get; set; }
public ICollection< Comment > Comments { get; set; }
public ICollection<Post> Posts { get; set; }
}
}
Post model
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace SocialAppApi.Models
{
public class Post : BaseEntity
{
[DatabaseGenerated( DatabaseGeneratedOption.Identity )]
[Column( "id", Order = 1 )]
public int Id { get; set; }
[Column( "content", TypeName = "VARCHAR(MAX)", Order = 2 )]
public string Content { get; set; }
[Column( "image", TypeName = "VARCHAR(255)", Order = 3 )]
public string Image { get; set; }
//Foreign keys
[Column( "user_id" )]
[ForeignKey( "id" )]
public int UserId { get; set; }
public User User { get; set; }
public ICollection< Comment > Comments { get; set; }
}
}
Comment model
using System.ComponentModel.DataAnnotations.Schema;
namespace SocialAppApi.Models
{
public class Comment : BaseEntity
{
[DatabaseGenerated( DatabaseGeneratedOption.Identity )]
[Column( "id", Order = 1 )]
public int Id { get; set; }
[Column( "content", TypeName = "VARCHAR(MAX)", Order = 2 )]
public string Content { get; set; }
public User User { get; set; }
[Column( "user_id", Order = 3 )]
[ForeignKey( "id" )]
public int UserId { get; set; }
public Post Post { get; set; }
[Column( "post_id", Order = 4 )]
[ForeignKey( "id" )]
public int PostId { get; set; }
}
}
And finally my seeder class
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using SocialAppApi.Models;
using SocialAppApi.Repositories;
namespace SocialAppApi.Seeders
{
public class DatabaseSeeder
{
public static void SeedDatabase( ApplicationDbContext applicationDbContext )
{
applicationDbContext.Database.Migrate();
if ( !applicationDbContext.Users.Any() )
{
User user1 = new User
{
FirstName = "Bikram",
LastName = "Bhandari",
};
User user2 = new User
{
FirstName = "Sabina",
LastName = "Lamichhane",
};
User user3 = new User
{
FirstName = "Jack",
LastName = "Wonderland",
};
Post user1Post = new Post
{
Content = "This post is about fires going on in NSW"
};
List< Comment > user1PostComments = new List< Comment >
{
new Comment {Content = "So sad to see this"},
new Comment {Content = "Let's hope for a rain"},
};
//add user1 post
user1.Posts = new List< Post > {user1Post};
**Is this right?**
user1Post.Comments = user1PostComments;
user1.Comments = user1PostComments;
**Those two lines above**
//add user1 post comments
applicationDbContext.Users.AddRange( user1, user2, user3 );
applicationDbContext.SaveChanges();
}
}
}
}
Questions:
- Can seeder class be modified to set one to many relations easily?
- Is it a good practice to have
userId
in the comment model? -
While running a migration, I had to change
onDelete: ReferentialAction.Cascade
toonDelete: ReferentialAction.NoAction
because it was complaining as this:introducing foreign key on table may cause cycles or multiple paths
Solution
By default, Entity Framework will create the foreign key with the name of the entity primary key that is referenced by the navigation property.
// Navigation Propertites //
// (one-to-x) relation
public User User { get; set; }
// (many-to-x) relation
public ICollection<User> Users { get; set; }
// works vice versa
For the ForeignKey
attribute :
//########### Version 1 ###########
public int UserId { get; set; }
[ForeignKey( "UserId" )] // refer to the UserId (the foreign key) Property
public User User { get; set; }
//########### Version 2 ###########
[ForeignKey( "User" )] // refer to the User Navigation Property
public int UserId { get; set; } // will create this FK with the exact name (UserId)
public User User { get; set; }
both versions will create a foreign key column named UserId
. as it’s also, a vice versa configuration. So, you just need to change the name of each ForeignKey
attribute to target its correct property.
Also, ForeignKey
is handy if you want a custom naming for your foreign keys, but in your case, you’re rewriting what entity framework does. If both entities primary key name are the same,then it’ll rename it to EntityName_ForeignKeyName
. For example, User_Id
. Which what you did explicitly.
I would also suggest using EF fluent API to control the models instead of using Data Annotations
attributes. It has more control than attributes, and it’s also readable.
you can use OnModelCreating
to configure your models with DbModelBuilder
. You can create a class to configure all models so you only pass DbModelBuilder
to the constructor, something I do with large entities, I love to keep the configuration for the entities in one place. So, it’s more flexible.
So, converting your attributes to EF fluent API would be :
public partial class ApplicationDbContext : DbContext
{
public ApplicationDbContext () : base("name=DbContext")
{
}
public virtual DbSet<Comment> Comments { get; set; }
public virtual DbSet<Post> Posts { get; set; }
public virtual DbSet<User> Users { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
/*
User
*/
modelBuilder.Entity<User>()
.Property(p => p.Id)
.HasColumnName("id")
.HasColumnOrder(1)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<User>()
.Property(e => e.FirstName)
.HasColumnName("first_name")
.HasMaxLength(100)
.HasColumnOrder(2)
.IsUnicode(false);
modelBuilder.Entity<User>()
.Property(e => e.MiddleName)
.HasColumnName("middle_name")
.HasMaxLength(100)
.HasColumnOrder(3)
.IsUnicode(false);
modelBuilder.Entity<User>()
.Property(e => e.LastName)
.HasColumnName("last_name")
.HasMaxLength(100)
.HasColumnOrder(4)
.IsUnicode(false);
// Foreign Keys
modelBuilder.Entity<User>()
.HasMany(e => e.Comments)
.WithRequired(e => e.User)
.HasForeignKey(e => e.UserId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<User>()
.HasMany(e => e.Posts)
.WithRequired(e => e.User)
.HasForeignKey(e => e.UserId);
/*
Post
*/
modelBuilder.Entity<Post>()
.Property(p => p.Id)
.HasColumnName("id")
.HasColumnOrder(1)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Post>()
.Property(e => e.Content)
.HasColumnName("content")
.HasColumnOrder(2)
.IsUnicode(false);
modelBuilder.Entity<Post>()
.Property(e => e.Image)
.HasColumnName("image")
.HasMaxLength(255)
.HasColumnOrder(3)
.IsUnicode(false);
// Foreign Keys
modelBuilder.Entity<Post>()
.HasMany(e => e.Comments)
.WithRequired(e => e.Post)
.HasForeignKey(e => e.PostId);
/*
Comment
*/
modelBuilder.Entity<Comment>()
.Property(p => p.Id)
.HasColumnName("id")
.HasColumnOrder(1)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Comment>()
.Property(e => e.Content)
.HasColumnName("content")
.HasColumnOrder(2)
.IsUnicode(false);
}
}
For the models structure, it’s fine. but I think it can be improved. I think there is no need for Comments
and Posts
in User
and Post
for a couple of reasons. The most obvious reason is performance
. assume you need to query User entity to get all current users, each user will be stored along with his posts and comments. The more users, the more collections. This would consume a lot of memory and also affect the performance as well, especially with large entities. So, separating entities and keep each one of them store its own data, is a good practice. There are some cases where using collections of other entities is a must, but always rethink about it before including it. Another reason is to minimize the code redundancy.
so, taking your models, we can do this :
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Content { get; set; }
public string Image { get; set; }
public User User { get; set; }
}
public class Comment
{
public int Id { get; set; }
public string Content { get; set; }
public Post Post { get; set; }
public User User { get; set; }
}
and the process would something like this :
var user1 = new User
{
FirstName = "Bikram",
LastName = "Bhandari",
};
var user2 = new User
{
FirstName = "Sabina",
LastName = "Lamichhane"
};
var user3 = new User
{
FirstName = "Jack",
LastName = "Wonderland"
};
var post1 = new Post
{
User = user1,
Content = "This post is about fires going on in NSW"
};
var comments = new List<Comment>
{
new Comment
{
User = user1,
Post = post1,
Content = "So sad to see this"
},
new Comment
{
User = user1,
Post = post1,
Content = "Let's hope for a rain"
}
};
applicationDbContext.Users.AddRange(new List<User> { user1, user2, user3 });
applicationDbContext.Posts.Add(post1);
applicationDbContext.Comments.AddRange(comments);
applicationDbContext.SaveChanges();
The collection then can be used on your business logic where needed.