Base solution for your next web application
Open Closed

Automapper update collection #7581


User avatar
0
fvdh created

Hi,

We have a product entity which has a collection with specifications like this:

    [Table("Masterdata.Products")]
    public class Product : FullAuditedEntity, IMustHaveTenant
    {
        public int TenantId { get; set; }

        [Required]
        public string Code { get; set; }

        public ICollection<ProductSpecificationValue> Specifications { get; set; }
    }

We want to update the product trough following DTO:

    public class ProductCreateOrEditDto : EntityDto<int?>
    {
        [Required]
        public string Code { get; set; }

        public ICollection<ProductSpecificationValueCreateOrEditDto> Specifications { get; set; }
    }

Following code we want to use to update the product:

public async Task Update(ProductCreateOrEditDto input)
        {
            var product = await _productRepository.GetAllIncluding(e => e.Specifications).Where(e => e.Id == input.Id).FirstOrDefaultAsync();
            ObjectMapper.Map(input, product);
        }

We can't update the values in the specification as the objectmapper won't sync the collection. The collection in the product gets deleted and AutoMapper creates new instances for the specification in the input object. Referring to following github topic i would assume that Automapper would sync the collections based on the EF properties. https://github.com/aspnetboilerplate/aspnetboilerplate/pull/4034

Do we need to add some configuration to make this work? Are we missing something else?


18 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team

    I recommend that you add/delete/update the Specifications collection separately.

  • User Avatar
    0
    fvdh created

    That's a way to do it but it feels to much like a workaround. In the example I simplified the entity, in the original code we have 5 more collections in the enitty. If we need to handle each collection with a separately handling we get a lot of messy code.

    The AutoMapper.Collection is specially made for supporting collections and is also implemented in Abp. So i guess it's only matter of finding out the correct configuration.

  • User Avatar
    0
    maliming created
    Support Team

    What is product framework type (.net framework or .net core)?

  • User Avatar
    0
    fvdh created

    The product type is ASP.NET CORE & Angular.

  • User Avatar
    0
    maliming created
    Support Team

    @fvdh Can you share your Automapper configuration code?

    This is my configuration and test code.

    public class Thing
    {
    	public int Id { get; set; }
    	public string Title { get; set; }
    
    	public string Code { get; set; }
    }
    
    public class ThingDto
    {
    	public int Id { get; set; }
    	public string Title { get; set; }
    }
    
    class Program
    {
    	static void Main(string[] args)
    	{
    		Mapper.Initialize(cfg =>
    		{
    			cfg.AddCollectionMappers();
    			cfg.CreateMap<ThingDto, Thing>().EqualityComparison((dto, entity) => dto.Id == entity.Id).ReverseMap();
    		});
    
    		var dtos = new List<ThingDto>
    		{
    			new ThingDto { Id = 1, Title = "test1" },
    			new ThingDto { Id = 2, Title = "test2" }
    		};
    
    
    		var entities = new List<Thing>
    		{
    			new Thing { Id = 1, Title = "" , Code = "1"},
    			new Thing { Id = 2, Title = "" , Code = "2"}
    		};
    
    		Mapper.Map(dtos, entities);
    
    		entities[0].Id.ShouldBe(1);
    		entities[0].Title.ShouldBe("test1");
    		entities[0].Code.ShouldBe("1");
    
    		entities[1].Id.ShouldBe(2);
    		entities[1].Title.ShouldBe("test2");
    		entities[1].Code.ShouldBe("2");
    	}
    }
    
  • User Avatar
    0
    fvdh created

    Sorry for the late answer, I haven't got any notice of the reply.

    The code you provided runs fine but doesn't proves anything. The problem is that AutoMapper creates new entities for the DTO's instead of updating the current DTO objects.

    I've changed your Entity and DTO classes to FullAudited and checkes for the creation time. When you run that test you will see that the CreationTime get changed.

            public class Thing : FullAuditedEntity
            {
                public string Title { get; set; }
    
                public string Code { get; set; }
            }
    
            public class ThingDto : FullAuditedEntityDto
            {
                public string Title { get; set; }
            }
    
            [Fact]
            public async Task Should_Update_Collections()
            {
                Mapper.Initialize(cfg =>
                {
                    cfg.AddCollectionMappers();
                    cfg.CreateMap<ThingDto, Thing>().EqualityComparison((dto, entity) => dto.Id == entity.Id).ReverseMap();
                });
    
                var dtos = new List<ThingDto>
                {
                    new ThingDto { Id = 1, Title = "test1" },
                    new ThingDto { Id = 2, Title = "test2" }
                };
    
                var entities = new List<Thing>
                {
                    new Thing { Id = 1, Title = "" , Code = "1"},
                    new Thing { Id = 2, Title = "" , Code = "2"}
                };
    
                DateTime tCreationTime = entities[0].CreationTime;
    
                Mapper.Map(dtos, entities);
    
                entities[0].Id.ShouldBe(1);
                entities[0].Title.ShouldBe("test1");
                entities[0].Code.ShouldBe("1");
    
                entities[0].CreationTime.ShouldBe(tCreationTime);
    
                entities[1].Id.ShouldBe(2);
                entities[1].Title.ShouldBe("test2");
                entities[1].Code.ShouldBe("2");
            }
        }
    
  • User Avatar
    0
    maliming created
    Support Team

    hi @fvdh

    The reason for the following test failure is because the CreationTime property is also available in dto and is set to the current time in the constructor.

    entities[0].CreationTime.ShouldBe(tCreationTime);
    

    You can create an example using Zero's Demo project (simply reproduce the above automapper problem), send it to me, I will view and write some code for you. : )

    Email: [email protected]

  • User Avatar
    0
    fvdh created

    hi @maliming

    I've just send you the example via WeTransfer. A new application service 'ProductAppService' is added with 2 services.

    The service that failes:

    1. Get the product and map it to a DTO
    2. Change the specifications in DTO object
    3. Maps it back to the product object

    This fails with following error:

    The instance of entity type 'ProductSpecification' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
    System.InvalidOperationException: The instance of entity type 'ProductSpecification' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode node, Boolean force)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode node, TState state, Func`3 handleNode)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.NavigationCollectionChanged(InternalEntityEntry entry, INavigation navigation, IEnumerable`1 added, IEnumerable`1 removed)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationCollectionChanged(InternalEntityEntry entry, INavigation navigation, IEnumerable`1 added, IEnumerable`1 removed)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavigationChange(InternalEntityEntry entry, INavigation navigation)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(InternalEntityEntry entry)
       at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(IStateManager stateManager)
       at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.Entries()
       at Abp.Zero.EntityFrameworkCore.AbpZeroCommonDbContext`3.SaveChangesAsync(CancellationToken cancellationToken)
       at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChangesInDbContextAsync(DbContext dbContext) in D:\Github\aspnetboilerplate\src\Abp.EntityFrameworkCore\EntityFrameworkCore\Uow\EfCoreUnitOfWork.cs:line 167
       at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.SaveChangesAsync() in D:\Github\aspnetboilerplate\src\Abp.EntityFrameworkCore\EntityFrameworkCore\Uow\EfCoreUnitOfWork.cs:line 68
       at Abp.EntityFrameworkCore.Uow.EfCoreUnitOfWork.CompleteUowAsync() in D:\Github\aspnetboilerplate\src\Abp.EntityFrameworkCore\EntityFrameworkCore\Uow\EfCoreUnitOfWork.cs:line 83
       at Abp.Domain.Uow.UnitOfWorkBase.CompleteAsync() in D:\Github\aspnetboilerplate\src\Abp\Domain\Uow\UnitOfWorkBase.cs:line 273
       at Abp.AspNetCore.Mvc.Uow.AbpUowActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) in D:\Github\aspnetboilerplate\src\Abp.AspNetCore\AspNetCore\Mvc\Uow\AbpUowActionFilter.cs:line 49
       at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
       at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
       at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
       at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
       at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync()
    

    The second service succeed without error when changing the Specifications directly in the Product object instead of mapping it to a DTO object.

    This scenario tells me that something is wrong with the mapping from ProductDTO to Product. I hope you guys can point me to the right direction.

  • User Avatar
    0
    maliming created
    Support Team

    Thanks, I will check it out.

  • User Avatar
    0
    maliming created
    Support Team

    hi @fvdh Please provide a email address, I will send you the code for ProjectDemo. It can already work with automapper. See git commit for specific changes.

  • User Avatar
    0
    fvdh created

    hi @maliming,

    You can send it to [email protected] .

    Thx.

  • User Avatar
    0
    maliming created
    Support Team

    Email has been sent.

  • User Avatar
    0
    fvdh created

    @hi maliming

    I just want to inform you that the problem is solved based on the sent project. The code you've sent was a good base to start from. I found some other issues in the code which were also responsible for not finding the right solution.

    Thanks for helping me out!

  • User Avatar
    0
    sneddo created

    @maliming / @fvdh - I have run into the same issue, can you share here what the underlying issue was or indicate where it was so others (like me) who run into the same issue can fix it and move on?

    Thanks.

  • User Avatar
    0
    maliming created
    Support Team

    hi sneddo You can create a new question and describe it in detail.

  • User Avatar
    0
    JapNolt created

    @maliming @fvdh Please share the solution to this problem with the rest of us.

  • User Avatar
    0
    maliming created
    Support Team

    hi japnolt You can create a new question and describe it in detail. Thanks : )

  • User Avatar
    0
    JapNolt created

    Since my problem is the same as the original poster, I was hoping you would be able to post the solution that you sent to him since it was just demo code anyway. I try to do my due diligence by searching for existing solutions instead of posting a new question as soon as I have a problem.