8

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

8
  • where is you DbCOntextFIle and Entities . With EF you can do this insert which you want to do . Commented Jan 25, 2017 at 14:43
  • my DBContext file is declared in the Startup.cs (EF Core) and my Entities are in the Models folder as classes Commented Jan 25, 2017 at 14:48
  • Can you show the code you assign an Image to Brand and to Product, and the call to your Add method? Commented Jan 25, 2017 at 15:34
  • basically in the GenericRepository, I need to do something like Context.Entry(Image).State == EntityState.Unchanged; however I do not know how to do it with Generics Commented Jan 25, 2017 at 15:34
  • @Alisson I pasted the code I am doing to call the Generic Add Method? Commented Jan 25, 2017 at 15:37

2 Answers 2

7

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

Sign up to request clarification or add additional context in comments.

12 Comments

That still won't work. Until it's tracked by the context, it will be treated as separate image instances, regardless of being same C# class instance.
I tested in a console application, it worked (one single image)....I can paste my full code here...
One single image as in one single row in a database table?
@ChrisPratt yep, I edited my answer with full code for my tests. Did I do something wrong?
@Alisson I still have a problem if the Image already exists in the db. For example lets say a new Brand with an old Image
|
1

You must save the image first, and then save the entities that reference it. Otherwise, Entity Framework sees each instance as a new instance that should be saved separately.

2 Comments

Hi Chris, I have already tried to save the image first, and then the Brand, and then the product in my Integration Test, however, when I try to save the Brand, its getting the same instance of the Image, and telling me that it cannot insert the same Image in the Image Table
I am getting the following error when creating the Image first and then the Brand System.Data.SqlClient.SqlException: Cannot insert explicit value for identity column in table 'Images' when IDENTITY_INSERT is set to OFF.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.