Base solution for your next web application
Open Closed

Multiple Add or Insert Failing #1312


User avatar
0
rferrari created

Hi,

I am trying to make multiple addition to a collection at the same time, but it is failing.

Situation is:

The client send a partner (main entity) with phones (or other elements) as a collection. There are 3 cases, the phone has been deleted and it is in a deleted phones list, the phone has been modified or added. I identify the new phones because they have a 0 id so I build the following application service:

[AbpAuthorize(AppPermissions.Pages_Tenant_Partners_CreatePartner)]
        public async Task EditPartner(EditPartnerInput input)
        {
            var partner = input.MapTo<Partner>();
            var phones = input.Phones.MapTo<List<Phone>>();
            var phonesToDelete = input.DeletedPhones.MapTo<List<Phone>>();

            var query = await _partnerRepository.UpdateAsync(partner);

            foreach (var phone in phones)
            {
                if (phone.Id == 0)
                {
                    {
                        var currentPartner = _partnerRepository.Get(partner.Id);
                        currentPartner.Phones.Add(phone);
                      //  await CurrentUnitOfWork.SaveChangesAsync();
                    }
                }
                else
                {
                    var phoneToUpdate = await _phoneRepository.GetAsync(phone.Id);
                    phone.MapTo(phoneToUpdate);
                    await _phoneRepository.UpdateAsync(phoneToUpdate);
                }
            }
            foreach (var phoneToDelete in phonesToDelete)
            {
                await _phoneRepository.DeleteAsync(phoneToDelete.Id);
            }
        }

Everything is working fine:

  1. Multiple deleted phones in the client are deleted in the DB
  2. Multiple edited phones in the client are updated in the DB
  3. If the phone to add is just one it is added

BUT

if I have more than one phone to add I get the following error:

System.InvalidOperationException: Attaching an entity of type 'stake.Partners.Phone' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

This is obviously related to the fact that the first record is not yet saved when the second one is processed. The suggestion is to use Add, and I am using it!

What I tried:

  1. Used Insert, of course it did not work;
  2. Used Add and CurrentUnitOfWork.SaveChangesAsync(), it is the same;

It seems I should change the state of the item, but I did not find the way.

Please help, thanks!


15 Answer(s)
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Have you tried like this ?

    await _phoneRepository.InsertAsync(phone);
    await CurrentUnitOfWork.SaveChangesAsync();
    
  • User Avatar
    0
    rferrari created

    I would say yes, as I made so many attempts, but I try it again and I tell you.

  • User Avatar
    0
    rferrari created

    No way.... same error.

    Should I change my Dto?

    I tried both, the commented and uncommented:

    using Abp.AutoMapper;
    using Abp.Application.Services.Dto;
    using System.Collections.ObjectModel;
    
    namespace stake.Partners.Dto
    {
        [AutoMapTo(typeof(Partner))]
        public class EditPartnerInput : IInputDto
        {
            public int Id { get; set; }
    
            public string Name { get; set; }
    
            public string Surname { get; set; }
    
            //public Collection<PhoneInEditPartnerDto> Phones { get; set; }
    
            public PartnerPhoneDto[] Phones { get; set; }
    
            public PartnerPhoneDto[] DeletedPhones { get; set; }
        }
        //[AutoMapTo(typeof(Phone))]
        //public class PhoneInEditPartnerDto : EntityDto
        //{
        //    public string Name { get; set; }
    
        //    public int PartnerId { get; set; }
    
        //    public int PhoneTypeId { get; set; }
        //}
    }
    

    The phone Dto is:

    using Abp.AutoMapper;
    using Abp.Application.Services.Dto;
    
    namespace stake.Partners.Dto
    {
        [AutoMap(typeof(Phone))]
        public class PartnerPhoneDto : EntityDto
        {
            public string Name { get; set; }
    
            public int PartnerId { get; set; }
    
            public int PhoneTypeId { get; set; }
        }
    }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    No, I think it is not related to your Dtos. PartnerPhoneDto seems ok.

    Is your "EditPartner" method in an application service ? Can you share it's latest version you have tried ?

  • User Avatar
    0
    rferrari created

    Yes it is in application service and here it is last version

    [AbpAuthorize(AppPermissions.Pages_Tenant_Partners_CreatePartner)]
            public async Task EditPartner(EditPartnerInput input)
            {
                var partner = input.MapTo<Partner>();
                var phones = input.Phones.MapTo<List<Phone>>();
                var phonesToDelete = input.DeletedPhones.MapTo<List<Phone>>();
    
                var query = await _partnerRepository.UpdateAsync(partner);
    
                foreach (var phone in phones)
                {
                    if (phone.Id == 0)
                    {
                        {
                            //  var currentPartner = _partnerRepository.Get(partner.Id);
                            //  currentPartner.Phones.Add(phone);
                            ////  await CurrentUnitOfWork.SaveChangesAsync();
                            await _phoneRepository.InsertAsync(phone);
                            await CurrentUnitOfWork.SaveChangesAsync();
                        }
                    }
                    else
                    {
                        var phoneToUpdate = await _phoneRepository.GetAsync(phone.Id);
                        phone.MapTo(phoneToUpdate);
                        await _phoneRepository.UpdateAsync(phoneToUpdate);
                    }
                }
                foreach (var phoneToDelete in phonesToDelete)
                {
                    await _phoneRepository.DeleteAsync(phoneToDelete.Id);
                }
            }
    
  • User Avatar
    0
    rferrari created

    I found this, very similar to what I try to do in a classic book on Entity Framework programming, but we do not have anyway to use it in ABP, for what I know

    private static void SaveDestinationAndLodgings(
      Destination destination,
      List<Lodging> deletedLodgings)
    {
      // TODO: Ensure only Destinations & Lodgings are passed in
    
      using (var context = new BreakAwayContext())
      {
        context.Destinations.Add(destination);
    
        if (destination.DestinationId > 0)
        {
          context.Entry(destination).State = EntityState.Modified;
        }
    
        foreach (var lodging in destination.Lodgings)
        {
          if (lodging.LodgingId > 0)
          {
            context.Entry(lodging).State = EntityState.Modified;
          }
        }
    
        foreach (var lodging in deletedLodgings)
        {
          context.Entry(lodging).State = EntityState.Deleted;
        }
    
        context.SaveChanges();
      }
    }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    I have tried a similar scenario and it worked as expected for me. Can you share your Phone entity, so i can make a more realistic test.

  • User Avatar
    0
    rferrari created

    Thanks in advance for your support. Here are my entities:

    Partner:

    using System.ComponentModel.DataAnnotations.Schema;
    using System.ComponentModel.DataAnnotations;
    using Abp.Domain.Entities.Auditing;
    using System.Collections.Generic;
    
    namespace stake.Partners
    {
        [Table("stakePartners")]
        public class Partner : FullAuditedEntity
        {
            public const int MaxNameLength = 50;
            public const int MaxSurnameLength = 50;
    
            [Required]
            [MaxLength(MaxNameLength)]
            public virtual string Name { get; set; }
    
            [Required]
            [MaxLength(MaxSurnameLength)]
            public virtual string Surname { get; set; }
    
            public virtual RelationshipType RelationshipType { get; set; }
            public virtual ResponseLevel ResponseLevel { get; set; }
            public virtual InterestLevel InterestLevel { get; set; }
            public virtual InfluenceLevel InfluenceLevel { get; set; }
            public virtual InfluenceArea InfluenceArea { get; set; }
            public virtual InterestArea InterestArea { get; set; }
            public virtual ReactivityLevel ReactivityLevel { get; set; }
            public virtual LikingLevel LikingLevel { get; set; }
    
            public virtual ICollection<Phone> Phones { get; set; }
            public virtual ICollection<Social> Socials { get; set; }
            public virtual ICollection<Email> Emails { get; set; }
            public virtual ICollection<Address> Addresses { get; set; }
        }
    }
    

    Phone:

    using System.ComponentModel.DataAnnotations.Schema;
    using System.ComponentModel.DataAnnotations;
    using Abp.Domain.Entities.Auditing;
    using System.Collections.Generic;
    
    namespace stake.Partners
    {
        [Table("stakePhones")]
        public class Phone : FullAuditedEntity
        {
            public const int MaxNumberLength = 16;
    
            [ForeignKey("PartnerId")]
            public virtual Partner Partner { get; set; }
            public virtual int PartnerId { get; set; }
    
            [ForeignKey("PhoneTypeId")]
            public virtual PhoneType PhoneType { get; set; }
            public virtual int PhoneTypeId { get; set; }
    
            [Required]
            [MaxLength(MaxNumberLength)]
            public virtual string Name { get; set; }
    
        }
    }
    

    Phonetype (to complete the chain:

    using System.ComponentModel.DataAnnotations.Schema;
    using System.ComponentModel.DataAnnotations;
    using Abp.Domain.Entities.Auditing;
    using System.Collections.Generic;
    
    namespace stake.Partners
    {
        [Table("stakePhoneTypes")]
        public class PhoneType : FullAuditedEntity
        {
            public const int MaxNameLength = 50;
    
            [Required]
            [MaxLength(MaxNameLength)]
            public virtual string Name { get; set; }
    
            public virtual ICollection<Phone> Phones { get; set; }
        }
    }
    
  • User Avatar
    0
    rferrari created

    Could it be some wrong dll version?

    here my configs

    CORE

    <?xml version="1.0" encoding="utf-8"?>
    <packages>
      <package id="Abp" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.AutoMapper" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.Zero" version="0.9.4.0" targetFramework="net461" />
      <package id="Abp.Zero.Ldap" version="0.9.4.0" targetFramework="net461" />
      <package id="AutoMapper" version="4.2.1" targetFramework="net461" />
      <package id="Castle.Core" version="3.3.3" targetFramework="net451" />
      <package id="Castle.LoggingFacility" version="3.3.0" targetFramework="net451" />
      <package id="Castle.Windsor" version="3.3.0" targetFramework="net451" />
      <package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net451" />
      <package id="Newtonsoft.Json" version="8.0.3" targetFramework="net461" />
      <package id="Nito.AsyncEx" version="3.0.1" targetFramework="net451" />
      <package id="System.Collections.Immutable" version="1.1.36" targetFramework="net461" />
    </packages>
    

    EntityFramework

    <?xml version="1.0" encoding="utf-8"?>
    <packages>
      <package id="Abp" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.AutoMapper" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.EntityFramework" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.EntityFramework.Common" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.Zero" version="0.9.4.0" targetFramework="net461" />
      <package id="Abp.Zero.EntityFramework" version="0.9.4.0" targetFramework="net461" />
      <package id="AutoMapper" version="4.2.1" targetFramework="net461" />
      <package id="Castle.Core" version="3.3.3" targetFramework="net461" />
      <package id="Castle.LoggingFacility" version="3.3.0" targetFramework="net451" />
      <package id="Castle.Windsor" version="3.3.0" targetFramework="net451" />
      <package id="EntityFramework" version="6.1.3" targetFramework="net451" />
      <package id="EntityFramework.DynamicFilters" version="1.4.10.2" targetFramework="net461" />
      <package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net451" />
      <package id="Newtonsoft.Json" version="8.0.3" targetFramework="net461" />
      <package id="Nito.AsyncEx" version="3.0.1" targetFramework="net451" />
      <package id="System.Collections.Immutable" version="1.1.36" targetFramework="net461" />
    </packages>
    

    application

    <?xml version="1.0" encoding="utf-8"?>
    <packages>
      <package id="Abp" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.AutoMapper" version="0.9.5.0" targetFramework="net461" />
      <package id="Abp.Zero" version="0.9.4.0" targetFramework="net461" />
      <package id="Abp.Zero.Ldap" version="0.9.4.0" targetFramework="net461" />
      <package id="AutoMapper" version="4.2.1" targetFramework="net461" />
      <package id="Castle.Core" version="3.3.3" targetFramework="net451" />
      <package id="Castle.LoggingFacility" version="3.3.0" targetFramework="net451" />
      <package id="Castle.Windsor" version="3.3.0" targetFramework="net451" />
      <package id="EntityFramework" version="6.1.3" targetFramework="net451" />
      <package id="EPPlus" version="4.0.5" targetFramework="net452" />
      <package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net451" />
      <package id="Newtonsoft.Json" version="8.0.3" targetFramework="net461" />
      <package id="Nito.AsyncEx" version="3.0.1" targetFramework="net451" />
      <package id="SharpZipLib" version="0.86.0" targetFramework="net461" />
      <package id="System.Collections.Immutable" version="1.1.36" targetFramework="net461" />
      <package id="System.Linq.Dynamic" version="1.0.6" targetFramework="net452" />
    </packages>
    
  • User Avatar
    0
    rferrari created

    By the way is it correct to use 4.6.1 as target framework or I should stay on 4.5.1?

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    I couldn't reproduce your problem. Can you comment this line and try again ?

    await _phoneRepository.UpdateAsync(phoneToUpdate);
    
  • User Avatar
    0
    rferrari created

    I tried... same error

  • User Avatar
    0
    rferrari created

    I finally solved it!

    First I discovered that it was not a problem of this piece of code alone:

    foreach (var phone in phones)
                {
                    if (phone.Id == 0)
                    {
                        {
                            var currentPartner = _partnerRepository.Get(partner.Id);
                            currentPartner.Phones.Add(phone);
                          //  await CurrentUnitOfWork.SaveChangesAsync();
                        }
                    }
    

    but of the interference with previous partner update:

    var query = await _partnerRepository.UpdateAsync(partner);
    

    each of the actions alone worked well, but they did not work together, I do not really know why, maybe because of interference between the collection partner.Phones and the separate addition of phones.

    When I discovered that I took the following actions:

    1. Added a line to the Dto for added phones as I did for deleted ones
    2. Modified the cshtml and the js so that each added phone goes in an addedPhones list and not on the original vm.partner.phones

    In that way everything works I can add as many phones I want, delete as many I want, edit as many I want and also edit the partner name and surname on the same page and everything get updated.

    Thanks for the support!

  • User Avatar
    0
    ismcagdas created
    Support Team

    Your solution is good enough, thank you for sharing. As a side note, if you are updating an entity, first get it from database, then map your dto to it.

  • User Avatar
    0
    rferrari created

    To share what I did this is my app service now:

    [AbpAuthorize(AppPermissions.Pages_Tenant_Partners_CreatePartner)]
            public async Task EditPartner(EditPartnerInput input)
            {
                var partner = input.MapTo<Partner>();
                var phones = input.Phones.MapTo<List<Phone>>();
                var phonesToDelete = input.DeletedPhones.MapTo<List<Phone>>();
                var phonesToAdd = input.AddedPhones.MapTo<List<Phone>>();
                var query = await _partnerRepository.UpdateAsync(partner);
                await CurrentUnitOfWork.SaveChangesAsync();
    
                foreach (var phoneToAdd in phonesToAdd)
                {
                    await _phoneRepository.InsertAsync(phoneToAdd);
                    // await CurrentUnitOfWork.SaveChangesAsync();
                }
                foreach (var phone in phones)
                {
                    var phoneToUpdate = await _phoneRepository.GetAsync(phone.Id);
                    phone.MapTo(phoneToUpdate);
                    await _phoneRepository.UpdateAsync(phoneToUpdate);
                }
                foreach (var phoneToDelete in phonesToDelete)
                {
                    await _phoneRepository.DeleteAsync(phoneToDelete.Id);
                }
            }
    

    This the Dto:

    using Abp.AutoMapper;
    using Abp.Application.Services.Dto;
    
    namespace stake.Partners.Dto
    {
        [AutoMapTo(typeof(Partner))]
        public class EditPartnerInput : IInputDto
        {
            public int Id { get; set; }
    
            public string Name { get; set; }
    
            public string Surname { get; set; }
    
            public PartnerPhoneDto[] Phones { get; set; }
    
            public PartnerPhoneDto[] DeletedPhones { get; set; }
    
            public PartnerPhoneDto[] AddedPhones { get; set; }
        }
    }