Stian
Stian

Reputation: 1602

Cloning an object and saving the clone to db causes the original object to lose relational data

When I'm trying to clone a Product-object and save the clone to the database, the original object loses all it's relational data, such as ProductPropertyOptionForProducts, IdentifierForProducts and InCategories.

This is the Product model:

public class Product
{
    public int Id { get; set; }
    public int ProductGroupId { get; set; }
    public int ProductGroupSortOrder { get; set; }

    [Required, MaxLength(30), MinLength(4)]     public string Title { get; set; }
    [MaxLength(200)]                            public string Info { get; set; }
    [MaxLength(4000)]                           public string LongInfo { get; set; }
    [Required, DataType(DataType.Currency)]     public decimal Price { get; set; }
                                                public int Weight { get; set; }
                                                public int ProductTypeId { get; set; }
    public ICollection<ProductImage> Images { get; set; }

    // Selected property options for this product
    public ICollection<PropertyOptionForProduct> ProductPropertyOptionForProducts { get; set; }

    // A product can have multiple identifiers (EAN, ISBN, product number, etc.)
    public ICollection<IdentifierForProduct> IdentifierForProducts { get; set; }

    public ProductType Type { get; set; }
    public ICollection<FrontPageProduct> InFrontPages { get; set; }
    public ICollection<ProductInCategory> InCategories { get; set; }
}

Some of the related models:

public class ProductInCategory
// A linking table for which products belongs to which categories
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public int ProductCategoryId { get; set; }
    public int SortOrder { get; set; }

    // Nav.props.:
    public Product Product { get; set; }
    public ProductCategory ProductCategory { get; set; }
}

public class PropertyOptionForProduct
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public int ProductPropertyId { get; set; }
    public int ProductPropertyOptionId { get; set; }

    // Nav.props.
    public Product Product { get; set; }
    public ProductPropertyOption ProductPropertyOption { get; set; }
}

public class IdentifierForProduct
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public int ProductIdentifierId { get; set; }
    [StringLength(30), MaxLength(30)]
    public string Value { get; set; }

    public ProductIdentifier ProductIdentifier { get; set; }
    public Product Product { get; set; }
}

The original is loaded like this:

public async Task<Product> GetProduct(int Id)
{
    Product DbM = await _context.Products
        .Include(ic => ic.InCategories)
            .ThenInclude(pc => pc.ProductCategory)
        .Include(t => t.Type)
            .ThenInclude(iit => iit.Identifiers) //ProductIdentifiersInTypes
                .ThenInclude(i => i.Identifier) // ProductIdentifiers
                    .ThenInclude(ifp => ifp.ProductIdentifiers) // IdentifiersForProducts
        .Include(t => t.Type)
            .ThenInclude(pit => pit.Properties) // ProductPropertiesInTypes
                .ThenInclude(p => p.Property) // ProductProperties
                    .ThenInclude(po => po.Options) // ProductPropertyOptions
        .Include(p => p.ProductPropertyOptionForProducts)
        .Where(p => p.Id == Id)
        .SingleOrDefaultAsync();
    return DbM;
}

This is the clone-method:

private async Task<Product> MakeClone(Product Original)
{
    Product Clone = new Product
    {
        ProductGroupId = Original.ProductGroupId,
        ProductGroupSortOrder = Original.ProductGroupSortOrder + 1,
        IdentifierForProducts = Original.IdentifierForProducts,
        Images = Original.Images,
        InCategories = Original.InCategories,
        Info = Original.Info,
        InFrontPages = Original.InFrontPages,
        LongInfo = Original.LongInfo,
        Price = Original.Price,
        ProductPropertyOptionForProducts = Original.ProductPropertyOptionForProducts,
        ProductTypeId = Original.ProductTypeId,
        Title = Original.Title,
        Type = Original.Type,
        Weight = Original.Weight
    };
    _context.Add(Clone);
    await _context.SaveChangesAsync();
    return Clone; // and go to the Edit-view.
}

Now, the clone has all the properties of the original product, but the original has been stripped of all of it's relational data. In the database, it looks like the clone's relational data has replaced the original's ones.

UPDATE

In accordance to Georg's answer, I changed my MakeClone()-method to this:

private Product MakeClone(Product Original)
{
    List<IdentifierForProduct> identifiers = Original
            .IdentifierForProducts
            .Select(i => CloneIdentifierForProduct(i))
            .ToList();
    List<PropertyOptionForProduct> propertyOptions = Original
            .ProductPropertyOptionForProducts
            .Select(o => ClonePropertyOptionForProduct(o))
            .ToList();
    List<ProductInCategory> inCategories = Original
            .InCategories
            .Select(c => CloneProductInCategory(c))
            .ToList();
    List<FrontPageProduct> inFrontPages = Original
            .InFrontPages
            .Select(f => CloneFrontPageProduct(f))
            .ToList();
    List<ProductImage> images = Original.Images.Select(i => CloneProductImage(i)).ToList();
    Product Clone = new Product
    {
        ProductGroupId = Original.ProductGroupId,
        ProductGroupSortOrder = Original.ProductGroupSortOrder + 1,
        Info = Original.Info,
        LongInfo = Original.LongInfo,
        Price = Original.Price,
        ProductTypeId = Original.ProductTypeId,
        Title = Original.Title,
        Type = Original.Type,
        Weight = Original.Weight,
        IdentifierForProducts = identifiers,
        ProductPropertyOptionForProducts = propertyOptions,
        InCategories = inCategories,
        InFrontPages = inFrontPages,
        Images = images
    };
    _context.Add(Clone);
    // fix FKs
    foreach (var ifp in Clone.IdentifierForProducts) ifp.ProductId = Clone.Id;
    foreach (var ofp in Clone.ProductPropertyOptionForProducts) ofp.ProductId = Clone.Id;
    foreach (var pic in Clone.InCategories) pic.ProductId = Clone.Id;
    foreach (var fpp in Clone.InFrontPages) fpp.ProductId = Clone.Id;
    foreach (var pi in Clone.Images) pi.ProductId = Clone.Id;
    // Lagre klonen i databasen:
    _context.SaveChangesAsync();
    return Clone;
}

... and added separate methods for cloning each of the linked data (I don't think I need to show all five methods):

private IdentifierForProduct CloneIdentifierForProduct(IdentifierForProduct ifp)
{
    IdentifierForProduct IFP = new IdentifierForProduct
    {
        Product = ifp.Product,
        ProductId = ifp.ProductId,
        ProductIdentifier = ifp.ProductIdentifier,
        ProductIdentifierId = ifp.ProductIdentifierId,
        Value = ifp.Value
    };
    return IFP;
}

Now I'm getting ArgumentNullException on the creation of the child Lists.

Could it have something to do with the fact that for example IdentifierForProduct also has a child property (which I also want to clone)?

Upvotes: 1

Views: 572

Answers (1)

Georg Patscheider
Georg Patscheider

Reputation: 9463

As Steve noted, you are just setting a reference to the collection properties of the original, instead of cloning the collection. If the relationship of the collection is not defined as many:many, this removes the related entities from the original and adds them to the clone.

For example, to clone the IdentifierForProducts collection, you have to clone each element and then add them to the collection of the clone.

Product clone = new Product {
    IdentifierForProducts = Original.IdentifierForProducts.Select(ifp => MakeClone(ifp)).ToList(),
    // other properties ....
};

// fix FKs after cloning
foreach (var ifp in clone.IdentifierForProducts) {
    ifp.ProductId = clone.Id;
}

with MakeClone<IdentifierForProduct>(IdentifierForProduct original) analogous to MakeClone<Product>(Product original).

Upvotes: 4

Related Questions