C# EF Core Issue beim Hinzufügen einer Entität

Karbe

Rear Admiral
Registriert
Feb. 2008
Beiträge
5.399
Ich hab ein Problem beim Hinzufügen einer Entität zum DB-Kontext:
Die Product-Objekte kommen aus einem json-Objekt und enthalten ein Manufactor-Objekt (1:n-Beziehung)
C#:
Product product = j.ToObject<Product>();

ctx.Entry(product.Manufactor).State =
    ctx.Products.Any(p => p.Manufactor == product.Manufactor) ||
    ctx.Manufactors.Any(m => m.ManufactorId == product.Manufactor.ManufactorId) ?
    EntityState.Unchanged :
    EntityState.Added;

ctx.Entry(product).State =
     ctx.Products.Any(p => p.ProductId == product.ProductId) ?
     EntityState.Modified :
     EntityState.Added;

Beim Speichern kommt nun folgender Fehler:
The instance of entity type 'Manufactor' cannot be tracked because another instance with the same key value for {'ManufactorId'} is already being tracked.
When attaching existing entities, ensure that only one entity instance with a given key value is attached.


Wie kann ich denn die schon vorhandenen Manufactor ausschließen?

Grüße
 
Wo kommt das Object "Manufactor" denn her? Klar, aus dem XML, aber hast Du bereits eine Tabelle in der Datenbank mit dem Namen oder ist das 2fach im Projekt deklariert?

Ich denke, das Du "Code First" arbeitest?

Was steht denn in der Datenbank?
 
Wieso benutzt du hier ctx.Entry? Ich kenne das nur so das man die Entity erstmal als Objekt baut, und dann mit der Add Methode hinzufügt und SaveChangesAsync() aufruft.

Das State tracking manuell zu steuern braucht man bei einfachen Fällen nicht, das ist für Sonderfälle in denen EF Core nicht erkennen kann ob etwas geändert wurde. Hier fehlt auch sicher einiges an Code, weil dieser Ausschnitt gar nichts in die Datenbank speichert.
 
Ja, ist nur ein Ausschnitt, da dort denke ich das Problem liegt.
Die Manufactor werden bereits vorher geladen und sind ein einer seperaten Tabelle.
Die Product-Objekte enthalten ein komplettes Manufactor-Objekt und ich möchte die ManufatorId daraus als Fremdschlüssel verwenden.

Grüße
Ergänzung ()

Dalek schrieb:
Wieso benutzt du hier ctx.Entry? Ich kenne das nur so das man die Entity erstmal als Objekt baut, und dann mit der Add Methode hinzufügt und SaveChangesAsync() aufruft.
Ich hab es mal etwas einfacher gebaut und der Fehler ist der gleiche:

C#:
var products = ctx.Products.ToList();

foreach (JToken j in jObjects)
{
    Product product = j.ToObject<Product>();
    products.Add(product);
}
count++;

ctx.Products.AddRange(products);
ctx.SaveChanges();
 
Zuletzt bearbeitet:
Die einfachste Variante ist die Entities nach Id zu holen mit EF Core und diese Entities dann an das neue Product anheften. Das sind halt zusätzliche Queries, dafür bekommt man einfacher mit wenn die referenzierten Entities nicht existieren.

Ansonsten kann man die Many-to-Many Einträge auch direkt erstellen, wenn sie als explizite Entity in EF Core konfiguriert sind. Das würde etwas so aussehen:

Code:
product.Manufacturers = newProduct.Manufacturers
        .Select(manufacturerId => new ProductManufacturer { Product = product, ManufacturerId = manufacturerId })
        .ToList();

newProduct ist hier die Benutzereingabe, und product das Objekt das später in die Datenbank soll. Geht vermutlich auch wenn es das gleiche Objekt ist, hab ich aber noch nicht ausprobiert.

Und dein einfaches Beispiel fordert alle Produkte in der Datenbank an, das ist so nicht beabsichtigt denke ich. Du kannst die neuen Produkte in einer leeren Liste sammeln oder sie einfach einzeln an den DbContext adden.
 
Dalek schrieb:
Die einfachste Variante ist die Entities nach Id zu holen mit EF Core und diese Entities dann an das neue Product anheften. Das sind halt zusätzliche Queries, dafür bekommt man einfacher mit wenn die referenzierten Entities nicht existieren.
Wie würde das konkret aussehen?
Das Product-Objekt kommt ja schon mit einem Manufactor-Objekt, letztlich soll dieses ja nur dann getrackt werden, wenn es noch nicht existiert.

Die Many-to-Many-Variante finde ich nicht so charmant, da jedes Product ja nur ein Manufactor haben kann.

Grüße
 
Du müsstest die Ids nehmen, die Manufacturer mit EF Core anforden und dann ähnlich wie in meinem Beispiel an das Product dranhängen.

Karbe schrieb:
Die Many-to-Many-Variante finde ich nicht so charmant, da jedes Product ja nur ein Manufactor haben kann.
Mein Beispiel ist mit mehreren, das ist Code aus einer echten Anwendung auf deine Entities umbenannt. Und da können mehrere Entities dranhängen.

Du kannst vermutlich auch mit "Attach" arbeiten, damit kannst du Entities an den Kontext hängen und sie als "unmodified" markieren. Ich glaube aber das du dann per Hand verhindern müsstest die gleiche Entity mehrmals dranzuhängen. Sie die Dokumentation hier von Microsoft

https://docs.microsoft.com/en-us/ef...explicit-tracking#attaching-existing-entities
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Karbe
Ich würde dir empfehlen, deinen Code etwas mehr nach dem Schichtmodell zu strukturieren. Das klassische Drei-Schichtmodell besteht dabei aus den Schichten Persistenz, Geschäftslogik und Präsentation.
Deine Klassen Product und Manufacturer tanzen derzeit auf allen drei Hochzeiten und das ist mindestens eine Hochzeit zu viel. ;-)

Es bietet sich daher an, für das Einlesen der XML-Daten sogenannte Data Transfer Objects zu verwenden. Diese DTOs enthalten nur die Properties ihrer jeweilen Partnerklassen. Nach dem Einlesen können dann die DTOs in Objekte ihrer Partnerklassen konvertiert werden. Bei diesem Konvertierungsvorgang kannst du dann das passende Manufacturer-Objekt auswählen.
DTOs bieten noch weitere Vorteile wie zum Beispiel, dass deine Model-Klassen keinen Default-Konstruktor mehr benötigen oder du besser steuern kannst, welche Properties aus- bzw. eingelesen werden können sollen.

Ich verwende selbst leider nicht das EntityFramework, habe dir aber trotzdem eine beispielhafte Lösung (mit EntityFramework) skizziert:

C#:
class Product {
   
    public string ProductNumber { get; }
    public Manufacturer Manufacturer { get; }
    public string Description { get; private set; }
   
    public Product(string productNumber, Manufacturer manufacturer, string description) {
        ProductNumber = productNumber ?? throw new ArgumentNullException(nameof(productNumber));
        Manufacturer = manufacturer ?? throw new ArgumentNullException(nameof(manufacturer));
        Description = description ?? throw new ArgumentNullException(nameof(description));
    }
   
}

class Manufacturer {
   
    public string ManufacturerNumber { get; }
    public string Name { get; private set; }
   
    public Manufacturer(string manufacturerNumber, string name) {
        ManufacturerNumber = manufacturerNumber ?? throw new ArgumentNullException(nameof(manufacturerNumber));
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }
   
}

public class ProductDTO {
   
    public string ProductNumber { get; set; }
    public ManufacturerDTO Manufacturer { get; set; }
    public string Description { get; set; }
   
}

public class ManufacturerDTO {
   
    public string ManufacturerNumber { get; set; }
    public string Name { get; set; }
   
}

class ProductFactory {

    private DbContext dbContext;
   
    public ProductFactory(DbContext dbContext) {
        this.dbContext = dbContext;  
    }
   
    public Product CreateProduct(ProductDTO productDto) {
        if (dbContext == null) throw new InvalidOperationException($"{nameof(dbContext)} must not be null");
        if (productDto == null) throw new ArgumentNullException(nameof(productDto));
        if (productDto.Manufacturer == null) throw new ArgumentException($"{nameof(Product)}.{nameof(productDto.Manufacturer)} must not be null");
       
        Manufacturer manufacturer = GetManufacturer(productDto.Manufacturer);
        Product product = new Product(productDto.ProductNumber, manufacturer, productDto.Description);
        return product;
    }
   
    private Manufacturer GetManufacturer(ManufacturerDTO manufacturerDto) {
        Manufacturer manufacturer =
            ExistManufacturer(manufacturerDto.ManufacturerNumber)
            ? LoadManufacturer(manufacturerDto.ManufacturerNumber)
            : CreateManufacturer(manufacturerDto);
        return manufacturer;
    }
   
    private bool ExistManufacturer(string manufacturerNumber) {
        return dbContext.Manufacturers.Local.Any(x => x.ManufacturerNumber == manufacturerNumber)
            || dbContext.Manufacturers.Any(x => x.ManufacturerNumber == manufacturerNumber);
    }
   
    private Manufacturer LoadManufacturer(string manufacturerNumber) {
        return dbContext.Manufacturers.Local.SingleOrDefault(x => x.ManufacturerNumber == manufacturerNumber)
            ?? dbContext.Manufacturers.Single(x => x.ManufacturerNumber == manufacturerNumber);  
    }
   
    private Manufacturer CreateManufacturer(ManufacturerDTO manufacturerDto) {
        Manufacturer manufacturer = new Manufacturer(manufacturerDto.ManufacturerNumber, manufacturerDto.Name);
        dbContext.Manufacturers.Add(manufacturer);
        return manufacturer;
    }
   
}

public class Program
{
   
    public static void Main()
    {
        DbContext dbContext = new DbContext();
        ProductFactory productFactory = new ProductFactory(dbContext);
        for (int i = 0; i < 10; ++i)
        {
            ProductDTO productDto = ReadProduct(i);
            Product product = productFactory.CreateProduct(productDto);
            dbContext.Products.Add(product);
        }
        dbContext.SaveChanges();
    }
   
    private static ProductDTO ReadProduct(int i) {
        ProductDTO productDto = new ProductDTO()
        {
            ProductNumber = "P" + i.ToString("0000"),
            Manufacturer = new ManufacturerDTO()
            {
                ManufacturerNumber = "1234",
                Name = "Default Manufacturer"
            },
            Description = "Default Description for product"
        };
        return productDto;
    }
   
}
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Karbe
Das hat mir wirklich sehr geholfen, vielen Dank!
 
Karbe schrieb:
Ich hab es mal etwas einfacher gebaut und der Fehler ist der gleiche:

C#:
var products = ctx.Products.ToList();

foreach (JToken j in jObjects)
{
    Product product = j.ToObject<Product>();
    products.Add(product);
}
count++;

ctx.Products.AddRange(products);
ctx.SaveChanges();
In der ersten Zeile liegt der Fehler.
Du holst dir alle aktuellen Produkte als getrackte Liste, fügst dann Dinge hinzu und willst diese dann wieder in den Kontext speichern.

Statt am Anfang alle Products aus dem Kontext zu laden einfach eine neue Liste erstellen welche du dann hinzufügst. Das ist auch deutlich schneller.
C#:
var products = new List<Product>();

foreach (JToken j in jObjects)
{
    Product product = j.ToObject<Product>();
    products.Add(product);
}
count++;

ctx.Products.AddRange(products);
ctx.SaveChanges();
 

Ähnliche Themen

Antworten
13
Aufrufe
4.885
W
Zurück
Oben