Open Closed

Mapperly Cannot Map Id properly #10282


User avatar
0
ddelamare created

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)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Root cause
    • Mapperly only maps properties that have a setter.
    • Your aggregate root’s Id comes from Entity<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) set Id.
      • 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.

    In ABP, the recommended pattern is:

    • Never create a new entity instance when updating.
    • Never map the Id from DTO back to the entity. Id is 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.


    1. 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 Id is 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 = false to 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 Id

    Even if you expose Id with a public setter, you generally should not map it:

    • For create, Id is generated by EF Core or your own code (IGuidGenerator, sequences, etc.).
    • For update, Id comes 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 Mapperly ignore Id (via attributes as above), and rely on the existing aggregate root instance loaded from the repository.


    1. About EF Core change tracking & child collections

    When you:

    • Load entity with GetAsync / FirstOrDefaultAsync inside 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 MyEntity during 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 = false caused.


    4. Summary

    • Keep Id protected 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 Id in 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.


    1. Sources
    • ABP – Object to Object Mapping / Mapperly integration
      https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping

    • ABP – 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.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Can you share your classes and mapper code?

    Thanks.

Learn More, Pay Less
33% OFF
All Trainings!
Get Your Deal
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.2.0-preview. Updated on January 07, 2026, 08:03
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.