简体   繁体   中英

Create Direct Navigation Property in EF Core Many to Many Relationship

I've been working through the following tutorial on how to create a many-to-many relationship using Entity Framework Core: https://docs.microsoft.com/en-us/ef/core/modeling/relationships .

I'm working on a group management feature and my models are the following:

public class Group
{
    public int GroupId { get; set; }
    public string GroupName { get; set;}            
    public virtual List<GroupMember> GroupMembers { get; set; } = new List<GroupMember>();
}

public class GroupMember
{
    public int GroupId { get; set; }
    public Group Group { get; set; }

    public int UserId { get; set; }
    public User User{ get; set; } 
}

public class User
{
    public int UserId { get; set; }
    public string Email { get; set; }
    public List<GroupMember> MemberOf {get; set;} = new List<GroupMember>();
}

And in my dbContext I have defined my join table for mapping two separate one-to-many relationships:

public DbSet<Group> Groups { get; set; }   
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{ 
                modelBuilder.Entity<GroupMember>()
                    .HasKey(t => new { t.UserId, t.GroupId });

                modelBuilder.Entity<GroupMember>()
                    .HasOne(pt => pt.User)
                    .WithMany(p => p.MemberOf)
                    .HasForeignKey(pt => pt.UserId);

                modelBuilder.Entity<GroupMember>()
                    .HasOne(pt => pt.Group)
                    .WithMany(t => t.GroupMembers)
                    .HasForeignKey(pt => pt.GroupId);
}

What I need is to create a navigation property to access a group's members directly rather than having to use a .Include() to include the GroupMembers join table, followed by a second .Include() to include the User objects.

The reason for this is that a) the client side is expecting a group object with a property for an array of user objects at the first level and b) I am unable to serialize the returned object in json because it is resulting in an in self referencing loops for the group property of the GroupMember table.

In EF6 you would just omit the Linking Entity and EF will create the linking table behind the scenes.

eg

public class Group
{
    public int GroupId { get; set; }
    public string GroupName { get; set;}            
    public virtual List<User> GroupMembers { get; set; } = new List<User>();
}
public class User
{
    public int UserId { get; set; }
    public string Email { get; set; }
    public virtual List<Group> MemberOf {get; set;} = new List<Group>();
}

But, that doesn't work on EF Core. So let's go around to the workshop and build a workaround.

One idea is to put a NotMapped property on the entities that gives us the skip-level navigation property, and then ignore the real Navigation Properties in JSON serialization. Also to break the cycles there's a ContractResolver that will skip serialization of the "navigation property" to eliminate cycles in the object graph.

Like this:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json.Serialization;
using System.Reflection;

namespace EFCore2Test
{
    public class Group
    {
        public int GroupId { get; set; }
        public string GroupName { get; set; }
        [JsonIgnore]
        public virtual ICollection<GroupMember> GroupMembers { get; } = new HashSet<GroupMember>();

        [NotMapped]
        public IList<User> Users => GroupMembers.Select(m => m.User).ToList();
    }

    public class GroupMember
    {
        public int GroupId { get; set; }
        public Group Group { get; set; }
        public int UserId { get; set; }
        public User User { get; set; }
    }

    public class User
    {

        public int UserId { get; set; }
        public string Email { get; set; }

        [JsonIgnore]
        public virtual ICollection<GroupMember> MemberOf { get; } = new HashSet<GroupMember>();

        [NotMapped]
        public IList<Group> Groups => MemberOf.Select(m => m.Group).ToList();
    }

    public class Db : DbContext
    {


        public DbSet<User> Users { get; set; }
        public DbSet<Group> Groups { get; set; }

        public DbSet<GroupMember> GroupMembers { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<GroupMember>()
                .HasKey(t => new { t.UserId, t.GroupId });

            modelBuilder.Entity<GroupMember>()
                .HasOne(pt => pt.User)
                .WithMany(p => p.MemberOf)
                .HasForeignKey(pt => pt.UserId);

            modelBuilder.Entity<GroupMember>()
                .HasOne(pt => pt.Group)
                .WithMany(t => t.GroupMembers)
                .HasForeignKey(pt => pt.GroupId);
        }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer("Server=(local);Database=Test;Trusted_Connection=True;MultipleActiveResultSets=true");
            base.OnConfiguring(optionsBuilder);
        }
    }


    class Program
    {

        public class DontSerialze<T> : DefaultContractResolver
        {


            protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
            {

                JsonProperty property = base.CreateProperty(member, memberSerialization);

                if (property.PropertyType == typeof(T))
                {
                    property.ShouldSerialize = o => false;
                }

                return property;
            }
        }
        static void Main(string[] args)
        {

            using (var db = new Db())
            {
                db.Database.EnsureDeleted();
                db.Database.EnsureCreated();

                var users = Enumerable.Range(1, 20).Select(i => new User() { Email = $"user{i}@wherever" }).ToList();

                var groups = Enumerable.Range(1, 5).Select(i => new Group() { GroupName = $"group{i}" }).ToList();

                var userGroups = (from u in users from g in groups select new GroupMember() { User = u, Group = g })
                                 .OrderBy(gm => (gm.Group.GroupName + gm.User.Email).GetHashCode())
                                 .Take(100)
                                 .ToList();

                db.Users.AddRange(users);
                db.Groups.AddRange(groups);
                db.GroupMembers.AddRange(userGroups);

                db.SaveChanges();

                var ser = new JsonSerializer();
                ser.Formatting = Formatting.Indented;
                ser.ContractResolver = new DontSerialze<IList<User>>();

                foreach (var u in users.Take(2))
                {
                    ser.Serialize(Console.Out, u);
                    Console.WriteLine();
                }

            }
            Console.WriteLine("Hit any key to exit");
            Console.ReadKey();
        }
    }
}

outputs

{
  "UserId": 20,
  "Email": "user1@wherever",
  "Groups": [
    {
      "GroupId": 4,
      "GroupName": "group1"
    },
    {
      "GroupId": 2,
      "GroupName": "group3"
    },
    {
      "GroupId": 5,
      "GroupName": "group4"
    },
    {
      "GroupId": 1,
      "GroupName": "group5"
    },
    {
      "GroupId": 3,
      "GroupName": "group2"
    }
  ]
}
{
  "UserId": 18,
  "Email": "user2@wherever",
  "Groups": [
    {
      "GroupId": 2,
      "GroupName": "group3"
    },
    {
      "GroupId": 1,
      "GroupName": "group5"
    },
    {
      "GroupId": 5,
      "GroupName": "group4"
    },
    {
      "GroupId": 3,
      "GroupName": "group2"
    },
    {
      "GroupId": 4,
      "GroupName": "group1"
    }
  ]
}
Hit any key to exit

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM