riok / mapperly

A .NET source generator for generating object mappings. No runtime reflection.

Home Page:https://mapperly.riok.app

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Use existing mapping rules from an "existing target object" map on a "new instance" map

mpickers opened this issue · comments

commented

When you have a mapping for creating a new instance of the target as well as setting an existing target object you have to duplicate the mapping information/rules as 2 separate maps are generated by Mapperly rather than using the same rules.

Example:

public class MyClass
{
   public int Id { get; set; }
   public string Name { get; set; }
}

public class MyClassDto
{
   public int Id { get; set; }
   public string TheName { get; set; }
}

public static partial class MyClassMapping
{
   [MapperRequiredMapping(RequiredMappingStrategy.Target)]                  // Same rules as ToExistingDto
   [MapProperty(nameof(MyClass.Name), nameof(MyClassDto.TheName))]          // Same rules as ToExistingDto
   public static partial MyClassDto ToMyClassDto(this MyClass o);

   [MapperRequiredMapping(RequiredMappingStrategy.Target)]
   [MapProperty(nameof(MyClass.Name), nameof(MyClassDto.TheName))]
   public static partial void ToExistingDto(this MyClass o, MyClassDto dto);
}

// Generated code

    public static partial class MyClassMapping
    {
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
        public static partial global::MapperlyDebug.Entities.MyClassDto ToMyClassDto(this global::MapperlyDebug.Entities.MyClass o)
        {
            var target = new global::MapperlyDebug.Entities.MyClassDto();
            target.Id = o.Id;
            target.TheName = o.Name;
            return target;
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
        public static partial void ToExistingDto(this global::MapperlyDebug.Entities.MyClass o, global::MapperlyDebug.Entities.MyClassDto dto)
        {
            dto.Id = o.Id;
            dto.TheName = o.Name;
        }
    }

Describe the solution you'd like
When there is a target type signature specified, use that as the main mapper and make the mapper that creates a new instance, use that mapping method.

Alternately if this needs to be explicit, have an attribute to specify the mapper method to use in the "new instance" map.

[Mapper]
public static partial class MyClassMapping
{
   public static partial MyClassDto ToMyClassDto(this MyClass o);

   [MapperRequiredMapping(RequiredMappingStrategy.Target)]
   [MapProperty(nameof(MyClass.Name), nameof(MyClassDto.TheName))]
   public static partial void ToExistingDto(this MyClass o, MyClassDto dto);
}

// OR

[Mapper]
public static partial class MyClassMapping
{
   [MapUsingCopyMethod] or something like [MapUsingRulesFromCopyMethod]
   public static partial MyClassDto ToMyClassDto(this MyClass o);

   [MapperRequiredMapping(RequiredMappingStrategy.Target)]
   [MapProperty(nameof(MyClass.Name), nameof(MyClassDto.TheName))]
   public static partial void ToExistingDto(this MyClass o, MyClassDto dto);
}

Expected generated code
Either the use the rules from the other mapping definition which will result in the exact same output as above generated code OR
use the existing object mapper in the "new object" map...

    public static partial class MyClassMapping
    {
        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
        public static partial global::MapperlyDebug.Entities.MyClassDto ToMyClassDto(this global::MapperlyDebug.Entities.MyClass o)
        {
            var target = new global::MapperlyDebug.Entities.MyClassDto();
            ToMyClassDto(o, target);
            return target;
        }

        [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
        public static partial void ToExistingDto(this global::MapperlyDebug.Entities.MyClass o, global::MapperlyDebug.Entities.MyClassDto dto)
        {
            dto.Id = o.Id;
            dto.TheName = o.Name;
        }
    }
commented

Couldn't this be addressed with #513?

commented

@latonz Yes, that seems like it's the option 2 I described above.

That would do the trick!

commented

@latonz With getting to know Mapperly a bit better, and seeing that the current behavior is:

  1. You define a "main" map eg
    public static partial MyClassDto ToMyClassDto(this MyClass o);
  2. You define derivations of the main map and Mapperly will automatically use the rules of the "main" map eg
    public static partial List<MyClassDto> MapToList(this List<MyClass> o);
    public static partial IQueryable<MyClassDto> ProjectTo(this IQueryable<MyClass> o);

I'd perhaps suggest that for consistency a ToExistingDto() type mapping should also fall under this same sort of behavior. ie no attribute needed and Mapperly will automatically use the rules of the "main" map.

commented

When needing a mapping from one type to another Mapperly always uses the default mapping. For your list and queryable examples this will happen as you described as Mapperly needs a mapping from MyClass to MyClassDto down the object tree. However, a ToExistingDto mapping is a new root-level alternative mapping to the default mapping and often the user wants a different configuration for it, therefore I don't think automatically applying the same configuration is a good idea. It would also make the API less explicit and harder to debug.

commented

Closing in favor of #513.