ASP.NET EF seed one to many relation

Posted on

Problem

I am creating a simple model relation in ASP.Net core. My model relations are like this:

  1. A user can have many posts
  2. A post can have many comments
  3. 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:

  1. Can seeder class be modified to set one to many relations easily?
  2. Is it a good practice to have userId in the comment model?
  3. While running a migration, I had to change onDelete: ReferentialAction.Cascade to onDelete: 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.

Leave a Reply

Your email address will not be published. Required fields are marked *