Search code examples
c#.net-coreentity-framework-core

Can a user get their own copy of an entity model?


I am new to Entity Framework and my personal project needs a fairly complex entity data structure.

I am using .NET 8 and Entity Framework Core v8.

I want a user to be able to get their own copy of my existing Books / Chapter / Pages entity and add their own data. For example, marking a chapter or page as IsCompleted=true or adding in user notes. I don't want to share the existing model between users, each user needs their own.

How can I achieve this? Use some kind of mapping table? What's the best procedure?

This is my current database context:

public class AppDbContext : IdentityDbContext<AppUser, IdentityRole, string>
{
     public DbSet<Book> Books { get; set; }
     public DbSet<Chapter> Chapters { get; set; }
     public DbSet<Page> Pages { get; set; }
     
     public AppDbContext(DbContextOptions options)
        : base(options)
     {
     }
}

A Book contains many Chapters, a chapter contains many Pages.

I have these model classes:

public class Book
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Required]
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; } = decimal.Zero;

    public ICollection<Chapter> Chapters { get; set; } = new List<Chapter>();
}

public class Chapter
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    [Required]
    public string Title { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;

    public ICollection<Page>? Pages { get; set; } = new List<Page>();

    [ForeignKey("BookId")]
    public string BookId { get; set; }
    public Book Book { get; set; }
}

public class Page
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();

    public int Number { get; set; } = 0;
    public string Notes { get; set; } = string.Empty;

    [ForeignKey("ChapterId")]
    public string ChapterId { get; set; }
    public Chapter Chapter { get; set; }
}

Any guidance is appreciated! I don't know how to give each user (I am using MS Identity AppUser) their own copy of these books objects. So if user1 marks a chapter or page IsCompleted=true, user2 is not affected.


Solution

  • Relationally something like that is known as a many-to-many relationship where you can store specific details for the relation. So for instance if you have a Book, Chapters, and Pages as a data structure which can be associated with users, and you want something like a Notes field on each level that each user can populate:

    [PrimaryKey(nameof(UserId), nameof(BookId))]
    public class UserBooks
    {
        public int UserId { get; set; }
        public int BookId { get; set; }
    
        [ForeignKey(nameof(UserId))]
        public virtual User User { get; set; }
        [ForeignKey(nameof(BookId))]
        public virtual Book Book { get; set; }
        public string? Notes { get; set; }
        public bool IsComplete { get; set; } = false;
        public virtual ICollection<UserChapter> UserChapters { get; protected set; } = new List<UserChapter>();
    }
    
    [PrimaryKey(nameof(UserId), nameof(ChapterId))]
    public class UserChapter
    {
        public int UserId { get; set; }
        public int ChapterId { get; set; }
    
        [ForeignKey(nameof(UserId))]
        public virtual User User { get; set; }
        ForeignKey(nameof(ChapterId))]
        public virtual Chapter Chapter { get; set; }
        public string? Notes { get; set; }
        public bool IsComplete { get; set; } = false;
        public virtual ICollection<UserPage> UserPages { get; protected set; } = new List<UserPage>();
    }
    
    [PrimaryKey(nameof(UserId), nameof(PageId))]
    public class UserPages
    {
        public int UserId { get; set; }
        public int PageId { get; set; }
    
        [ForeignKey(nameof(UserId))]
        public virtual User User { get; set; }
        [ForeignKey(nameof(PageId))]
        public virtual Page Page { get; set; }
        public string? Notes { get; set; }
    }
    

    So in your User class, rather than a collection of Books, you would have a collection of UserBook entities. If you want to associate a Book to a user, instead of:

    user.Books.Add(book);
    

    You would use:

    user.UserBooks.Add(new UserBook(book));
    

    or better, create an AddBook() method on User to handle creating the UserBook for the relationship. User would not have a collection of Books, rather UserBooks. UserBooks contains the book references associated to the user, but also any fields specific to that user and book combination, so for instance any notes that user wants to make about the book. If you want to access book specific details you can access those through The UserBook.Book reference. That Book entity still holds the base navigation properties to Chapters and Pages.

    Where it can get a little tricky is handling UserChapter and UserPages. You could opt to create a UserChapter and UserPage for each chapter and page in the Book when creating the UserBook, but chances are you would only want to create these for chapters or pages when a user actually goes to record notes or mark something as complete. In this case you can use something like FirstOrDefault() to find if there are UserChapter or UserPage records defined for a given chapter or page, and create one if the user is looking to record something.

    Edit: I corrected the examples above to better outlined the navigation properties and their associated foreign keys, as well as the primary key definition for the classes.

    Using navigation properties with say books and pages you have the option to use uni-directional references or bi-directional references. For example a uni-directional reference:

    public class Book
    {
        public virtual ICollection<Chapter> Chapters { get; protected set; } = new List<Chapter>();
    }
    

    Where Chapter has no reference back to the Book, versus a bi-directional reference:

    public class Book
    {
        public virtual ICollection<Chapter> Chapters { get; protected set; } = new List<Chapter>();
    }
    
    public class Chapter
    {
        public virtual Book Book { get; set; }
    }
    

    In most cases I recommend uni-directional by default where there isn't really a common case to load a chapter from a DbSet and get it's associated book. (You can always do it without Chapter.Book through Book, I.e. var book = context.Books.Where(b => b.Chapters.Any(c => c.ChapterId == chapterId))) Book is an aggregate root, chapters only make sense in relation to a book. In the case of a User and Book both of these can be treated as aggregate roots. Books can be associated to a user, but mean something to be loaded themselves. (Such as to associate to a user)

    When you use many-to-many relationships the use of an entity for the joining table (UserBooks) is optional if there are no additional details needed about the relationship. If you only want to associate one or more books to one or more users then User could contain a collection of Book entities, and Book could optionally also contain a collection of Users that are associated to that book. Behind the scenes EF can manage a UserBook table. However, in the case where you want to use additional details about that relationship (I.e. Notes about the Book for the particular User) then you need to map out the joining Entity. (UserBook) The PK for a joining table is typically a composite key for the two linked FKs. So in the case of a UserBook, the UserId and BookId. Commonly in the joining entity we will use bi-directional navigation properties So from a User.UserBooks we can get the Books, and from a Book.UserBooks we can get back to the Users as well.