Laden...

EF6: Entity mit IList bringt "Navigationproperty xyz is not valid"

Erstellt von Palladin007 vor 6 Jahren Letzter Beitrag vor 6 Jahren 2.883 Views
Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor 6 Jahren
EF6: Entity mit IList bringt "Navigationproperty xyz is not valid"

Guten Abend,

ich würde gerne IList in einer Code-First-Klasse implementieren.
In der Implementierung wird alles entsprechend (mit Index, daher der ganze Spaß) an die "Zwischen-Klasse" für eine m2n-Beziehung weiter geleitet.

Allerdings bekomme ich immer folgenden Fehler:

Fehlermeldung:
FromRole: NavigationProperty 'mTableCollection' is not valid. Type 'mTable' of FromRole 'm2nTable_nTable_Target' in AssociationType 'm2nTable_nTable' must exactly match with the type 'nTable' on which this NavigationProperty is declared on.

mTable, nTable sind die jeweiligen "großen" Tabellen.
m2nTable ist die Verbindung für die m2n-Beziehung.

Die mTable hat IList<nTable> implementiert.
Die Verbindungs-Tabelle m2nTable beinhaltet einen Index, den ich in der Implementierung beachte.
Nehme ich nur die Interface-Implementierung (und die expliziten Implementierungen) weg, funktioniert alles.

Das Mapping läuft über entsprechende Mapping-Klassen (EntityTypeConfiguration<xyzTable>).

Geht mein Vorhaben überhaupt, oder gibt es einen Weg, wie ich das erreichen kann?

Beste Grüße

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

D
985 Beiträge seit 2014
vor 6 Jahren

Zeig doch mal ein wenig Code ...

Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor 6 Jahren

Im Anhang liegt ein Test-Projekt.
Über die Benamsung kann man sicher streiten, aber sie ist ja nur zum Test.

Hier besteht zwischen den Tabellen T_Left und T_Right eine m2n-Beziehung.
Dafür braucht es die Tabelle T_LeftRight, die die entsprechenden Fremdschlüssel als Primary Keys hat.

Die Entity-Klasse "T_Left" hat IList<T_Right> implementiert.
Führe ich das Programm so aus, läuft es auf einen Fehler.

Nehme ich das Interface IList<T_Right> und die explizite Implementierung von GetEnumerator raus, dann geht es.
Alle anderen Member können drin bleiben.

Die Fehler-Meldung:

Fehlermeldung:
One or more validation errors were detected during model generation:

LeftRightCollection: FromRole: NavigationProperty 'LeftRightCollection' is not valid. Type 'T_Right' of FromRole 'T_LeftRight_Left_Target' in AssociationType 'T_LeftRight_Left' must exactly match with the type 'T_Left' on which this NavigationProperty is declared on.

PS:

Ich hab gelesen (und eben auch ausprobiert), dass EF6 eine m2n-Beziehung auch von sich aus kann (siehe hier). Entsprechende Collection auf die andere Tabelle und im Mapping HasMany().WithMany()
An sich ziemlich praktisch, allerdings brauche ich eine Index-Spalte in dieser Verbindungs-Tabelle, da ich die Listen umsortieren können will. Die Tabelle, die EF6 dafür erstellt, kann das nicht.

PPS:
Interessanterweise verbietet EF6 das Zuweisen einer Collection.
Ich muss in T_Left die Property LeftRightCollection also private set machen, da ich die Entity sonst nicht erstellen und noch vor dem Speichern die Liste nutzen kann.

Das ist für mich jetzt nicht weiter wichtig, mach ich den Setter eben private.
Aber wenn jemand weiß, warum das so ist oder wie man das "richtig" machen kann, würde es mich schon interessieren ^^

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

D
985 Beiträge seit 2014
vor 6 Jahren

Ohne bislang einen Blick auf das Projekt geworfen zu haben:

Bist du dir sicher, dass du hier nicht Business- und Data-Layer vermischst? Deine Erläuterung riecht danach.

Diese EF-POCO Klassen sind einfache Datentransporteure ohne Gebimmel und Gebammel, denn dieses gehört in den Business-Layer und dessen Klassen.

Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor 6 Jahren

Bist du dir sicher, dass du hier nicht Business- und Data-Layer vermischst? Deine Erläuterung riecht danach.

Ja, ich bin sicher, dass ich das tue 😄

Das mache ich, weil es sich für mich nicht lohnt, das aufzutrennen.
Das Projekt ist klein und privat, da steht also weder viel Geld noch viel Zeit hinter.

Ich hab bisher auch noch keinen zufriedenstellenden Weg gefunden, das aufzusplitten.
Ich kenne nur CSLA und das ist wie die berühmte Kanonenkugel auf eine Fliege.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

16.834 Beiträge seit 2008
vor 6 Jahren

Das EF ist ja eigentlich für die Trennung verantwortlich.
Im EF hast Du streng genommen (der Idee nach) nur Business Modelle und die Entitäten siehst Du nie. Das macht das EF im Hintergrund.
So echte Business Modelle können das natürlich nicht werden.

Dass T_Left IList implementiert, macht in meinen Augen irgendwie keinen Sinn.
Wie soll das denn in einer relationalen DB gemappt werden? Das wäre ja - flach betrachtet - im Prinzip n-m:n-m.
Riecht für mich aktuell ziemlich nach: SQL nicht gut, NoSql gut.

Kannst Du mal erklären, was Du eigentlich tun willst?

D
985 Beiträge seit 2014
vor 6 Jahren

Also hier mal ein Beispiel für so eine m-n Beziehung mit Index.

Der DbContext mit POCO-Klassen:


using System.Collections;
namespace ConsoleApp36
{
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;

    public class TestModel : DbContext
    {
        public TestModel()
            : base("name=TestModel")
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            var article = modelBuilder.Entity<Article>();
            article.HasKey(e => e.Id);
            article.Property(e => e.Name).IsRequired().HasMaxLength(100);

            var order = modelBuilder.Entity<Order>();
            order.HasKey(e => e.Id);
            order.Property(e => e.CreatedAt).HasColumnType("datetime2").IsRequired();

            var orderpos = modelBuilder.Entity<OrderPosition>();
            orderpos.HasKey(e => new { e.OrderId, e.Position });
            orderpos.HasRequired(e => e.Order).WithMany(e => e.Positions).HasForeignKey(e => e.OrderId).WillCascadeOnDelete(true);
            orderpos.HasRequired(e => e.Article).WithMany().HasForeignKey(e => e.ArticleId).WillCascadeOnDelete(false);

        }

        public virtual DbSet<Article> Articles { get; set; }

        public virtual DbSet<Order> Orders { get; set; }
        public virtual DbSet<OrderPosition> OrderPositions { get; set; }
    }

    public class Article
    {
        public long Id { get; set; }
        public string Name { get; set; }
    }

    public class Order
    {
        public long Id { get; set; }
        public DateTime CreatedAt { get; set; }
        public virtual ICollection<OrderPosition> Positions { get; set; }
    }

    public class OrderPosition
    {
        public long OrderId { get; set; }
        public virtual Order Order { get; set; }

        public int Position { get; set; }

        public long ArticleId { get; set; }
        public virtual Article Article { get; set; }
    }
}

Die entsprechende Migration


namespace ConsoleApp36.Migrations
{
    using System;
    using System.Data.Entity.Migrations;
    
    public partial class InitialCreate : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "dbo.Articles",
                c => new
                    {
                        Id = c.Long(nullable: false, identity: true),
                        Name = c.String(nullable: false, maxLength: 100),
                    })
                .PrimaryKey(t => t.Id);
            
            CreateTable(
                "dbo.OrderPositions",
                c => new
                    {
                        OrderId = c.Long(nullable: false),
                        Position = c.Int(nullable: false),
                        ArticleId = c.Long(nullable: false),
                    })
                .PrimaryKey(t => new { t.OrderId, t.Position })
                .ForeignKey("dbo.Articles", t => t.ArticleId)
                .ForeignKey("dbo.Orders", t => t.OrderId, cascadeDelete: true)
                .Index(t => t.OrderId)
                .Index(t => t.ArticleId);
            
            CreateTable(
                "dbo.Orders",
                c => new
                    {
                        Id = c.Long(nullable: false, identity: true),
                        CreatedAt = c.DateTime(nullable: false, precision: 7, storeType: "datetime2"),
                    })
                .PrimaryKey(t => t.Id);
            
        }
        
        public override void Down()
        {
            DropForeignKey("dbo.OrderPositions", "OrderId", "dbo.Orders");
            DropForeignKey("dbo.OrderPositions", "ArticleId", "dbo.Articles");
            DropIndex("dbo.OrderPositions", new[] { "ArticleId" });
            DropIndex("dbo.OrderPositions", new[] { "OrderId" });
            DropTable("dbo.Orders");
            DropTable("dbo.OrderPositions");
            DropTable("dbo.Articles");
        }
    }
}

und ein kleiner Test


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp36
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var ctx = new TestModel())
            {
                var articles = ctx.Articles.Take(2).ToList();

                var order = new Order
                {
                    CreatedAt = DateTime.Now,
                    Positions = articles.Select((e, i) => new OrderPosition { Article = e, Position = i + 1, }).ToList(),
                };

                ctx.Orders.Add(order);
                ctx.SaveChanges();
            }
        }
    }
}

Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor 6 Jahren

Genau so brauche ich das, allerdings war mein Versuch, dass ich mich nicht mehr um die Position kümmern muss, sondern dass die Sortierung an genau einer Stelle - nämlich im (Möchtegern-) BusinessModel - liegt.

Ich hab dann nur noch die Order-Entity, der ich mit Add oder Insert Artikel hinzufügen kann.
Bei Add wird die Anzahl der bereits vorhandenen Einträge als Position verwendet (weil die Letzte), bei Insert passt es die Position aller folgenden Einträge an.
Selber dazu gebaut hab ich noch eine Move-Methode, wie sie die ObservableCollection hat.

In deinem Beispiel setzt Du die Position selber.
Da spricht auch nichts gegen, bis zu dem Zeitpunkt, wo z.B. der User per Drag&Drop einen Artikel in der Liste verschieben will. Dann muss nicht nur der einzelne Eintrag bzw. dessen Position angepasst werden, sondern alle anderen Einträge unter Umständen auch. Diese komplette Logik liegt hinter meiner IList-Implementierung.

Ein anderes Beispiel, wo das noch relevant sein kann: Ein Musik-Album
Die Einträge haben ja für gewöhnlich eine bestimmte Reihenfolge.
Damit diese Reihenfolge geändert werden kann, braucht es einen ebenfalls gespeicherten Index.

Prinzipiell brauch ich das IList-Interface nicht, es reichen ja auch die Methoden, die ja schon implementiert sind.
Es wäre bloß schade, darauf verzichten zu müssen, weil ich dann auch alle Vorteile von IList nicht mehr habe. Z.B. kann das an vielen Stellen als Parameter übergeben werden, das ginge dann nicht mehr.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

16.834 Beiträge seit 2008
vor 6 Jahren

Das kann aber eine SQL DB in der Form nicht, weil sie nicht hellsehen kann, was Du da mappen willst.
Bei einer NoSQL DB würde das funktionieren, weil Deine im Programm sorierte Liste einfach 1:1 als Objekt serialisiert wird und nichts gemappt werden muss.

Palladin007 Themenstarter:in
2.079 Beiträge seit 2012
vor 6 Jahren

Mir würde es ja reichen, wenn EF6 die Liste gar nicht erst versucht zu mappen.
Es soll nur das mappen, was ich explizit im Mapping angegeben habe.
Das IList soll es einfach ignorieren - wobei das "einfach" vermutlich sehr viel einfacher gesagt/geschrieben als getan ist 😄

Gemapped sind dann die zwei "Haupt"-Tabellen und die eine Verbindungs-Tabelle mit Index und das kann eine SQL-DB darstellen.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.