From beb971fb2d10985d163c835135ae817a8771fe32 Mon Sep 17 00:00:00 2001 From: "Tizian.Breuch" Date: Tue, 25 Nov 2025 11:19:06 +0100 Subject: [PATCH] bilder --- .../Services/Admin/AdminProductService.cs | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/Webshop.Application/Services/Admin/AdminProductService.cs b/Webshop.Application/Services/Admin/AdminProductService.cs index 352cc84..c27e558 100644 --- a/Webshop.Application/Services/Admin/AdminProductService.cs +++ b/Webshop.Application/Services/Admin/AdminProductService.cs @@ -116,7 +116,7 @@ namespace Webshop.Application.Services.Admin { Console.WriteLine($"---- UPDATE START: Produkt-ID {productDto.Id} ----"); - // 1. Produkt laden + // 1. Produkt laden (nur um sicherzugehen, dass es existiert und für alte Bilder) var existingProduct = await _context.Products .Include(p => p.Images) .Include(p => p.Productcategories) @@ -127,14 +127,12 @@ namespace Webshop.Application.Services.Admin return ServiceResult.Fail(ServiceResultType.NotFound, $"Produkt nicht gefunden."); } - // 2. CONCURRENCY CHECK (Manuell & Optional) - // Wenn RowVersion gesendet wird, prüfen wir sie. Wenn nicht (Workaround), überschreiben wir einfach (Last Win). + // 2. Optionaler Concurrency Check if (!string.IsNullOrEmpty(productDto.RowVersion)) { string dbRowVersion = Convert.ToBase64String(existingProduct.RowVersion ?? new byte[0]); string incomingRowVersion = productDto.RowVersion.Trim().Replace(" ", "+"); - // Einfacher String-Vergleich if (dbRowVersion != incomingRowVersion) { Console.WriteLine("!!! KONFLIKT: Versionen unterschiedlich !!!"); @@ -143,25 +141,39 @@ namespace Webshop.Application.Services.Admin } // ----------------------------------------------------------------------- - // SCHRITT A: BILDER UPDATE (ISOLIERT) + // SCHRITT A: BILDER UPDATE (DIREKT AM CONTEXT) // ----------------------------------------------------------------------- bool imagesChanged = false; - // A1. Bilder zum Löschen markieren + // A1. Bilder löschen if (productDto.ImagesToDelete != null && productDto.ImagesToDelete.Any()) { + // Wir suchen die zu löschenden Bilder direkt im Context var imagesToRemove = existingProduct.Images.Where(img => productDto.ImagesToDelete.Contains(img.Id)).ToList(); if (imagesToRemove.Any()) { - // Wir entfernen sie direkt aus dem Context, nicht nur aus der Liste _context.ProductImages.RemoveRange(imagesToRemove); imagesChanged = true; } } + // Hilfsfunktion um saubere neue Bilder zu erstellen + ProductImage CreateNewImage(string url, bool isMain, int order) + { + return new ProductImage + { + Id = Guid.NewGuid(), // WICHTIG: Neue ID client-seitig generieren + ProductId = existingProduct.Id, // WICHTIG: Explizite Zuordnung zum Produkt + Url = url, + IsMainImage = isMain, + DisplayOrder = order + }; + } + // A2. Hauptbild if (productDto.MainImageFile != null) { + // Altes Hauptbild finden und löschen var existingMainImage = existingProduct.Images.FirstOrDefault(img => img.IsMainImage); if (existingMainImage != null) { @@ -171,53 +183,65 @@ namespace Webshop.Application.Services.Admin await using var stream = productDto.MainImageFile.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, productDto.MainImageFile.FileName, productDto.MainImageFile.ContentType); - // WICHTIG: Wir fügen das Bild der Collection hinzu. - existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = true, DisplayOrder = 1 }); + // NEU: Direktes Hinzufügen zum Context (Umgeht Collection-Tracking Probleme) + var newMainImage = CreateNewImage(url, true, 1); + _context.ProductImages.Add(newMainImage); + imagesChanged = true; } // A3. Zusatzbilder if (productDto.AdditionalImageFiles != null && productDto.AdditionalImageFiles.Any()) { - int displayOrder = (existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0) + 1; + // Höchste DisplayOrder ermitteln (sicher gegen NullReference) + int currentMaxOrder = existingProduct.Images.Any() ? existingProduct.Images.Max(i => i.DisplayOrder) : 0; + int displayOrder = currentMaxOrder + 1; + foreach (var file in productDto.AdditionalImageFiles) { await using var stream = file.OpenReadStream(); var url = await _fileStorageService.SaveFileAsync(stream, file.FileName, file.ContentType); - existingProduct.Images.Add(new ProductImage { Url = url, IsMainImage = false, DisplayOrder = displayOrder++ }); + + // NEU: Direktes Hinzufügen zum Context + var newAdditionalImage = CreateNewImage(url, false, displayOrder++); + _context.ProductImages.Add(newAdditionalImage); } imagesChanged = true; } // ----------------------------------------------------------------------- - // SCHRITT A: SPEICHERN (NUR BILDER!) + // SCHRITT A SPEICHERN // ----------------------------------------------------------------------- if (imagesChanged) { try { - // WICHTIG: Wir ändern HIER KEINE Eigenschaften am 'existingProduct' (wie RowVersion oder LastModified). - // Dadurch generiert EF Core nur SQL für die Tabelle "ProductImages", aber NICHT für "Products". - // Das verhindert den Concurrency-Fehler auf der Product-Tabelle. - Console.WriteLine("---- SPEICHERE BILDER ----"); + Console.WriteLine("---- SPEICHERE BILDER (Context Add) ----"); await _context.SaveChangesAsync(); - // Da sich durch Trigger oder DB-Logik die Version des Produkts geändert haben KÖNNTE, - // laden wir das Produkt jetzt neu, um für Schritt B bereit zu sein. + // WICHTIG: State neu laden für Schritt B + // Wir müssen dem Context sagen, dass existingProduct neu geladen werden soll, + // damit die Navigations-Properties (Images) wieder aktuell sind. await _context.Entry(existingProduct).ReloadAsync(); + + // Da wir die Collection umgangen haben, müssen wir sie explizit neu laden + await _context.Entry(existingProduct).Collection(p => p.Images).LoadAsync(); } catch (Exception ex) { Console.WriteLine($"Fehler Bild-Save: {ex.Message}"); + // Inner Exception loggen, falls vorhanden + if (ex.InnerException != null) Console.WriteLine($"Inner: {ex.InnerException.Message}"); + return ServiceResult.Fail(ServiceResultType.Failure, "Fehler beim Speichern der Bilder."); } } // ----------------------------------------------------------------------- - // SCHRITT B: PRODUKT EIGENSCHAFTEN UPDATE + // SCHRITT B: PRODUKT EIGENSCHAFTEN // ----------------------------------------------------------------------- - // Kategorien + // Kategorien update if (productDto.CategorieIds != null) { var currentIds = existingProduct.Productcategories.Select(pc => pc.categorieId).ToList(); @@ -231,7 +255,7 @@ namespace Webshop.Application.Services.Admin } } - // Mapping der einfachen Felder + // Mapping existingProduct.Name = productDto.Name; existingProduct.Description = productDto.Description; existingProduct.SKU = productDto.SKU; @@ -246,7 +270,7 @@ namespace Webshop.Application.Services.Admin existingProduct.IsFeatured = productDto.IsFeatured; existingProduct.FeaturedDisplayOrder = productDto.FeaturedDisplayOrder; - // JETZT aktualisieren wir die Metadaten des Produkts + // Metadaten existingProduct.LastModifiedDate = DateTimeOffset.UtcNow; existingProduct.RowVersion = Guid.NewGuid().ToByteArray(); @@ -257,8 +281,6 @@ namespace Webshop.Application.Services.Admin } catch (DbUpdateConcurrencyException) { - // Da wir oben ReloadAsync gemacht haben, sollte das hier nur passieren, - // wenn in der Millisekunde zwischen Bild-Upload und Text-Update jemand anders gespeichert hat. return ServiceResult.Fail(ServiceResultType.Conflict, "Konflikt beim Speichern der Produktdaten."); }