2

As EFCore requires you to explicitly create join entities I am trying to find ways to make this code more manageable. I am following this tutorial (4 part series, this part and next part are the relevant ones): https://blog.oneunicorn.com/2017/09/25/many-to-many-relationships-in-ef-core-2-0-part-2-hiding-as-ienumerable/

It hides the join class away as a private ICollection and then populates another ICollection with the joined entitis so it works in similar fashion to EF6. Here is my implementation for tracking what cars people own:

public class Person
{
    public Person() => 
        Cars = new JoinCollectionFacade<Car, PersonCar>
        (PersonCars, pc => pc.Car, c => new PersonCar { Person = this, Car = c });

    public int Id { get; set; }
    public string Name { get; set; }

    private ICollection<PersonCar> PersonCars { get; } = new List<PersonCar>();

    [NotMapped]
    public ICollection<Car> Cars { get; }
}

public class Car
{
    public Car() => Persons = new JoinCollectionFacade<Person, PersonCar>
        (PersonCars, pc => pc.Person, p => new PersonCar { Person = p, Car = this });

    public int Id { get; set; }
    public string Manufacturer { get; set; }

    private ICollection<PersonCar> PersonCars { get; } = new List<PersonCar>();

    [NotMapped]
    public ICollection<Person> Persons { get; }
}

public class PersonCar
{
    public Person Person { get; set; }
    public int PersonId { get; set; }
    public Car Car { get; set; }
    public int CarId { get; set; }
}   

My mappings:

modelBuilder.Entity<PersonCar>(e => 
{
    e.HasKey(t => new { t.PersonId, t.CarId });
    e.HasOne(pc => pc.Person).WithMany("PersonCars");
    e.HasOne(pc => pc.Car).WithMany("PersonCars");
});

And some seed data:

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

    db.Persons.AddRange(
        new Person() { Name = "John" },
        new Person() { Name = "Peter" },
        new Person() { Name = "Paul" }
    );

    db.Cars.AddRange(
        new Car() { Manufacturer = "Audi" },
        new Car() { Manufacturer = "Honda" },
        new Car() { Manufacturer = "Mercedes" },
        new Car() { Manufacturer = "Ferrai" },
        new Car() { Manufacturer = "Porche" }
        );

    db.SaveChanges();

    db.PersonCars.AddRange(
        new PersonCar() { PersonId = 1, CarId = 2},
        new PersonCar() { PersonId = 1, CarId = 3 },
        new PersonCar() { PersonId = 2, CarId = 2 },
        new PersonCar() { PersonId = 3, CarId = 1 }
        );

    db.SaveChanges();
}

If I pull back a list of people including cars like below it works and outputs the data:

var drivers = db.Persons.Include("PersonCars.Car").ToList();
foreach(var person in drivers)
{
    foreach(var car in person.Cars)
    {
        Console.WriteLine($"{person.Name} has a {car.Manufacturer}");
    }
}

However, if I try to view the Cars collection results inside debugger then VS2017 crashes which isn't great but at code level it seems to work.

But, lets say I want to filter the list to just be Audi drivers, the below results in 0 results:

var audiDrivers = db.Persons.Include("PersonCars.Car").Where(x => x.Cars.Any(c => c.Manufacturer == "Audi"));

The series of articles is mainly focused on improving adding/removing and doesn't mention filtering. I'd like that functionality but I want to be able to filter the Persons by their Cars.

If I make PersonCars collection public then I can do:

var audiDrivers = db.Persons
    .Include(i => i.PersonCars).ThenInclude(i => i.Car)
    .Where(x => x.PersonCars.Select(pc => pc.Car).Any(c => c.Manufacturer == "Audi"))
    .ToList();

So to summarize:

If navigation property is private like article suggests, can lookup be filtered?

Why does VS2017 crash when looking at Car collection?

Is there a better way to do this that is simple like in EF6?

Guerrilla
  • 13,375
  • 31
  • 109
  • 210
  • 1
    Hrmm interesting idea, however its seemingly all just sugar that makes your life more difficult and most likely unpredictable. I have just come to learn to live with mapping tables. anyway good luck – TheGeneral Feb 13 '18 at 12:41
  • 1
    I like to [embrace the join table](https://lostechies.com/jimmybogard/2014/03/12/avoid-many-to-many-mappings-in-orms/) :) – Steve Greene Feb 13 '18 at 14:35
  • Don't waste your time with partially working hacks. Wait for EF Core support. – Ivan Stoev Feb 13 '18 at 18:15
  • @Ivan Stoev, could you have a look at my question here: https://stackoverflow.com/questions/54025974/is-it-possible-to-hide-the-collection-which-defines-the-relationship. It is similar to this one. – w0051977 Jan 03 '19 at 17:42
  • @w0051977 I've saw it. But the response would be the same - stay away from such hacks. EF Core doesn't support it yet, so all you'll get would be a problems - working here, not working there. The main problem is that when you hide the actual navigation property, you are losing the "navigate" support inside L2E queries. The unmapped collection *looks* like navigation property, but it isn't, so any usage inside L2E query will generate exception. – Ivan Stoev Jan 03 '19 at 17:50
  • @Ivan Stoev, do you know any good examples of apps with a many to many relationship (with EF Core) that use Domain Driven Design. I cannot find a single one. Thanks. – w0051977 Jan 03 '19 at 17:53
  • @w0051977 Sorry mate, I can't help with that. I'm afraid I'm on the "other side" - not liking DDD, especially when applied to EF entity (storage) model, which has it's own requirements, which are conflicting with DDD. If you ask me for opinion, keep DDD and entity (persistence) models separate and map between when needed. – Ivan Stoev Jan 03 '19 at 19:01
  • @Ivan Stoev, thanks. Do you mean something like this: https://stackoverflow.com/questions/20011041/advice-on-mapping-of-entities-to-domain-objects/20012730. Would you use a Memento like this: https://github.com/sapiens/ModernMembership/blob/master/src/ModernMembership/LocalMember.cs? How would you deal with change tracking if the domain model is separate to the data model? – w0051977 Jan 03 '19 at 22:38

1 Answers1

1

I personally would work directly with the join entities. It might not be "pretty" but it is the most straight forward approach. Ultimately it will be easier and faster to develop and maintain your code. The fact that you are hitting such basic problems with the alternative approach so early on in your development I think proves it.

My advice is: work with EF Core, not against it. It is not as mature as EF6 but it will get there eventually.

Daniel P
  • 3,314
  • 1
  • 36
  • 38
  • manually adding/removing the join entity entries can get pretty verbose compared to ef6 where you could just assign a collection. – Guerrilla Feb 13 '18 at 13:47