I am having an issue after changing the mapping engine to mapperly and generating mappers per your documentation. The issue I am having is that since Id is a protected property, Mapperly does not generate mappings for it and so it comes across as empty. I was able to work around this in some cases by using Parameterless Contrctustor = false, but that breaks EF Core Change tracking because the mapping process creates a new entity which leaves the old entity in a detached state. I specifically saw this when we have an entity with sub-entities. The sub-entities are not able to be tracked by EF Core because they get tracked twice during the mapping process.
2 Answer(s)
-
0
- Root cause
- Mapperly only maps properties that have a setter.
- Your aggregate root’s
Idcomes fromEntity<TPrimaryKey>/AggregateRoot<TPrimaryKey>and typically has a protected set – that’s fine for reading (Entity -> DTO), but:- For
DTO -> Entity(especially updates) Mapperly is generating code that creates a new entity instance and does not (and should not) setId. - When you set
ParameterlessConstructor = false, Mapperly uses a constructor instead of the parameterless one, which again creates a new entity instance. That breaks EF Core tracking (original tracked entity vs new entity) and especially child collections.
- For
In ABP, the recommended pattern is:
- Never create a new entity instance when updating.
- Never map the
Idfrom DTO back to the entity.Idis taken from the route/method parameter and the existing entity instance, not from the DTO.
So the problem is not “Mapperly can’t map
Id”, but that the mapping is being used in a way that replaces the tracked entity instance.
- Recommended fix (ABP + Mapperly + EF Core)
a) Define separate mappers for Create and Update
Create DTO → Entity (OK to create a new entity):
using Riok.Mapperly.Abstractions; using Volo.Abp.Mapperly; [Mapper] public partial class MyEntityCreateDtoToMyEntityMapper : MapperBase<MyEntityCreateDto, MyEntity> { // For create, you usually don't have Id in DTO; EF or your code generates it. public partial MyEntity Map(MyEntityCreateDto source); }Update DTO → existing Entity (no new instance, no Id mapping):
using Riok.Mapperly.Abstractions; using Volo.Abp.Mapperly; [Mapper] public partial class MyEntityUpdateDtoToMyEntityMapper : MapperBase<MyEntityUpdateDto, MyEntity> { // Important: map *into existing* entity public partial void Map(MyEntityUpdateDto source, MyEntity destination); }If your update DTO contains
Id, explicitly ignore it:[Mapper] public partial class MyEntityUpdateDtoToMyEntityMapper : MapperBase<MyEntityUpdateDto, MyEntity> { [MapProperty(Ignore = true)] public long Id { get; set; } // or use [MapperIgnoreTarget(nameof(MyEntity.Id))] on the mapper public partial void Map(MyEntityUpdateDto source, MyEntity destination); }or with attribute on the entity side:
[Mapper] public partial class MyEntityUpdateDtoToMyEntityMapper : MapperBase<MyEntityUpdateDto, MyEntity> { [MapperIgnoreTarget(nameof(MyEntity.Id))] public partial void Map(MyEntityUpdateDto source, MyEntity destination); }This ensures
Idis never touched during update mappings.
b) Use the “map-into-existing” pattern in your AppService
Correct (preserves EF tracking and children):
public class MyEntityAppService : CrudAppService< MyEntity, MyEntityDto, Guid, PagedAndSortedResultRequestDto, MyEntityCreateDto, MyEntityUpdateDto> { public MyEntityAppService(IRepository<MyEntity, Guid> repository) : base(repository) { } protected override async Task MapToEntityAsync(MyEntityUpdateDto input, MyEntity entity) { // entity is tracked by EF; just map into it ObjectMapper.Map(input, entity); } protected override Task<MyEntity> MapToEntityAsync(MyEntityCreateDto input) { // For create, Mapperly is allowed to create a new instance var entity = ObjectMapper.Map<MyEntityCreateDto, MyEntity>(input); return Task.FromResult(entity); } }Avoid patterns like:
// BAD for updates – creates a *new* instance var entity = ObjectMapper.Map<MyEntityUpdateDto, MyEntity>(input); await _repository.UpdateAsync(entity);or relying on
ParameterlessConstructor = falseto select another constructor: both approaches replace the tracked instance and cause the detached/duplicate-tracking issues you’re seeing with sub-entities.
c) Do not try to force mapping to
IdEven if you expose
Idwith a public setter, you generally should not map it:- For create,
Idis generated by EF Core or your own code (IGuidGenerator, sequences, etc.). - For update,
Idcomes from the route or method parameter and is used to load the existing entity; mapping it from DTO is at best redundant, at worst dangerous.
Let
MapperlyignoreId(via attributes as above), and rely on the existing aggregate root instance loaded from the repository.
- About EF Core change tracking & child collections
When you:
- Load entity with
GetAsync/FirstOrDefaultAsyncinside the same UoW, - Use
ObjectMapper.Map(input, existingEntity),
EF Core:
- keeps the original instance tracked,
- tracks changes to scalar and navigation properties (including child collections),
- will properly generate updates and maintain tracking for sub-entities.
Creating a new
MyEntityduring mapping always risks:- old instance stays tracked,
- new one is added/attached separately,
- child collections get attached twice or lost.
That’s exactly what your workaround with
ParameterlessConstructor = falsecaused.
4. Summary
- Keep
Idprotected and do not try to map it with Mapperly. - For updates, always:
- load the existing entity from the repository,
- use the
Map(source, destination)pattern (no new entity), - ignore
Idin the mapping.
- For creates, use a separate mapper that returns a new aggregate instance; let EF or your domain logic set
Id.
This pattern is aligned with ABP’s Mapperly integration and EF Core best practices, and it eliminates the EF change-tracking issues you’re seeing.
- Sources
ABP – Object to Object Mapping / Mapperly integration
https://abp.io/docs/latest/framework/infrastructure/object-to-object-mappingABP – AutoMapper to Mapperly migration (Mapperly mapper patterns)
https://abp.io/docs/latest/release-info/migration-guides/AutoMapper-To-Mapperly
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
-
0
hi
Can you share your classes and mapper code?
Thanks.