Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework adding multiple related entities with single insert

I have a GenericService Add method like so:-

    public bool Add(T entity, Expression<Func<T, bool>> filter = null)
    {
        try
        {
            _genericRepository.Add(entity, filter);
        }
        catch (Exception e)
        {
            return false;
        }
        return true;
    }

and a GenericRepository Add method like so:-

    public void Add(T entity, Expression<Func<T, bool>> filter = null)
    {
        var existing = Get<T>(filter);
        if (existing.Result != null) return;
        Context.Add(entity);
        Save();
    }

This is the call I am doing in the ProductsController:-

    [HttpPost]
    public IActionResult Create([FromBody] Product product)
    {
        if (product == null)
            return BadRequest();

        var result = _productsService.Add(product, m => m.Name == product.Name);
        if (result)
        {
            return CreatedAtRoute("GetProducts", new { id = product.Id }, product);

        }
        return BadRequest("Item not added");
    }

I am creating this by means of an integration test as follows :-

        testBrand = new Brand { Name = "testBrand" };
        testImage = new Image { Name = "testImage", Url = "/Brands/adidas_logo_test.png" };
        testBrand.Image = testImage;

        testCategory = new Category {Name = "testCategory"};

        testProduct = new Product
        {
            Category = testCategory,
            Name = "testProduct",
            Brand = testBrand,
            BrandId = testBrand.Id,
            CategoryId = testCategory.Id,
            Deal = false,
            Description = "testDescription",
            Discount = "50% Discount",
            Image = testImage,
            ImageId = testImage.Id,
            Price = new decimal(50.00),
            Stock = 5
        };

    [Test]
    public async Task Create_CreateAProduct_NewBrandNewCategoryNewImageProductsController()
    {
        //Arrange 

        //Act
        //create new image
        var requestImage = "api/Images/";
        var postResponseImage = await _client.PostAsJsonAsync(requestImage, testImage);
        var created = await postResponseImage.Content.ReadAsStringAsync();
        var createdImage = JsonConvert.DeserializeObject<Image>(created);

        //Act
        testBrand.Image = createdImage;
        testBrand.ImageId = createdImage.Id;
        testImage.Id = createdImage.Id;

        var postResponseProduct = await _client.PostAsJsonAsync(requestProduct, testProduct);
        var createdProduct = await postResponseProduct.Content.ReadAsStringAsync();
        var createdProductObj = JsonConvert.DeserializeObject<Product>(createdProduct);

        var getResponse = await _client.GetAsync(requestProduct + "Get/" + createdProductObj.Id);
        var fetched = await getResponse.Content.ReadAsStringAsync();
        var fetchedProduct = JsonConvert.DeserializeObject<Product>(fetched);

        // Assert
        Assert.IsTrue(postResponseProduct.IsSuccessStatusCode);
        Assert.IsTrue(getResponse.IsSuccessStatusCode);

        Assert.AreEqual(testProduct.Name, createdProductObj.Name);
        Assert.AreEqual(testProduct.Name, fetchedProduct.Name);

        Assert.AreNotEqual(Guid.Empty, createdProductObj.Id);
        Assert.AreEqual(createdProductObj.Id, fetchedProduct.Id);
    }

Everything works fine, until I try to insert an entity that has multiple related entities. Let me give an example.

Lets say I have a Product, which has an FK ImageId, a FK for BrandId, and a FK for CategoryId. The Brands entity has already a FK ImageId for the Image entity.

Now when I try to insert a new product, its inserting 2 images, one which comes with the Brand, and the image for the Product itself. So in the Images Table, I get 2 entries, when I only want 1 new entry for the Product Image. Also, this is causing a problem when I want to use an existing image for a new product.

So I was thinking of creating a new Service/Repository for the Product to Inherit from the Generic Service/Repository, and add some more logic to it. However is there a better way to do this?

Thanks for your help and time

like image 245
JMon Avatar asked Jan 25 '17 14:01

JMon


People also ask

How do I insert multiple rows in Entity Framework?

You can add multiple records or multiple objects using the AddRange method of DbSet as shown in the following code. The code creates a list of department objects and inserts two new departments to the list. We add the list to the context using the AddRange method.

Which method can be used to add collection of entities in one go?

Entity Framework 6 introduced methods to add and remove a collection of entities in one go. The DbSet. AddRange() method attaches a collection of entities to the context with Added state, which will execute the INSERT command in the database for all entities on SaveChanges() . In the same way, the DbSet.

How to add multiple records in Entity Framework Core?

Add Record / Multiple Records In Entity Framework Core. In this tutorial let us look at how to add a record, add multiple records to the database. Before inserting records into the database, we must add the entities to the context first. To do that we use the Add & AddRange methods. Once the records are added to the context, ...

What happens when you add a new entity to a context?

If you create several new related entities, adding one of them to the context will cause the others to be added too. In the following example, the blog and three related posts are all inserted into the database. The posts are found and added, because they are reachable via the Blog.Posts navigation property.

How to count related entities without loading them in Entity Framework?

Using Query to count related entities without loading them Entity Framework supports three ways to load related data - eager loading, lazy loading and explicit loading. The techniques shown in this topic apply equally to models created with Code First and the EF Designer.

How does Entity Framework save changes work?

Once the records are added to the context, we can call the SaveChanges method, which sends the insert query to the database. The EF also takes care of updating identity values generated in the database in the entity. We also show you how to add related entities or data.


1 Answers

Now I understood.

When using a client for testing purposes, mvc receives your request with json data, and creates your models correctly.

However, mvc doesn't know you want the same Image for product and brand, it'll create one instance for each one, like this (I simplified for example purposes):

var product = new Product();
var brand = new Brand();
product.Image = new Image();
product.Brand = brand;
brand.Image = new Image(); // new image with same info...

Likewise, entity framework will assume they are two different images with same data. Just let it know it's the same, by doing something like this in your actions (of course you would create a better code, this is just a quick sample):

[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    if (product == null)
        return BadRequest();

    // If the image already exists...nullify image so EF won't try to insert a new one...
    if (product.ImageId > 0)
        product.Image = null;
    // If the image already exists...and the brand doesn't have an existing image, use the same image and nullify the brand's image as well...
    if (product.ImageId > 0 && product.Brand != null && !(product.Brand.ImageId > 0))
    {
        product.Brand.ImageId = product.ImageId;
        product.Brand = null;
    }
    // If product is reveiving a new image...and the brand doesn't have an existing image, use the same new image...
    if (product.Image != null && product.Brand != null && !(product.Brand.ImageId > 0))
        product.Brand.Image = product.Image;

    var result = _productsService.Add(product, m => m.Name == product.Name);
    if (result)
    {
        return CreatedAtRoute("GetProducts", new { id = product.Id }, product);

    }
    return BadRequest("Item not added");
}

Just to test in a console application I reproduced it like below. Some classes:

public class Brand
{
    public int Id { get; set; }
    public virtual Image Image { get; set; }
    public int ImageId { get; set; }

}

public class Image
{
    public int Id { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public virtual Image Image { get; set; }
    public virtual Brand Brand { get; set; }
    public int ImageId { get; set; }
    public int BrandId { get; set; }
}

the DbContext with configurations:

public class MyDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    public DbSet<Brand> Brands { get; set; }

    public DbSet<Image> Images { get; set; }

    public MyDbContext()
        : base("name=MyDbContext")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {

        modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
        modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

        modelBuilder.Properties<int>().Where(p => p.Name == "Id").Configure(p => p.IsKey());
        modelBuilder.Entity<Product>().HasRequired(p => p.Brand).WithMany().HasForeignKey(p => p.BrandId);
        modelBuilder.Entity<Product>().HasRequired(p => p.Image).WithMany().HasForeignKey(p => p.ImageId);
        modelBuilder.Entity<Brand>().HasRequired(p => p.Image).WithMany().HasForeignKey(p => p.ImageId);

    }
}

Then finally, the code itself.

This first case I use the same instance:

class Program
{
    static void Main(string[] args)
    {

        using (var db = new MyDbContext())
        {

            var image = new Image();
            var product = new Product();
            var brand = new Brand();
            product.Image = image;
            product.Brand = brand;
            brand.Image = image; // same instance

            db.Products.Add(product);

            db.SaveChanges();

        }

    }
}

My result was:

first case

Then I ran again, now using a new instance:

class Program
{
    static void Main(string[] args)
    {

        using (var db = new MyDbContext())
        {

            var image = new Image();
            var product = new Product();
            var brand = new Brand();
            product.Image = image;
            product.Brand = brand;
            brand.Image = new Image();

            db.Products.Add(product);

            db.SaveChanges();

        }

    }
}

and now we have two new images:

second case

like image 199
Alisson Reinaldo Silva Avatar answered Sep 27 '22 18:09

Alisson Reinaldo Silva