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)
-
0
I recommend that you add/delete/update the Specifications collection separately.
-
0
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.
-
0
What is product framework type (.net framework or .net core)?
-
0
The product type is ASP.NET CORE & Angular.
-
0
@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"); } }
-
0
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"); } }
-
0
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]
-
0
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:
- Get the product and map it to a DTO
- Change the specifications in DTO object
- 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.
-
0
Thanks, I will check it out.
-
0
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. -
0
-
0
Email has been sent.
-
0
@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!
-
0
@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.
-
0
hi sneddo You can create a new question and describe it in detail.
-
0
@maliming @fvdh Please share the solution to this problem with the rest of us.
-
0
hi japnolt You can create a new question and describe it in detail. Thanks : )
-
0
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.