Mapster icon indicating copy to clipboard operation
Mapster copied to clipboard

[Feature Request] Expand capability for factory methods

Open pdevito3 opened this issue 3 years ago • 0 comments

So I'm seeing a limitation with the current capabilities when using a factory method.

The Problem

Background

Say I have a (simplified) entity that looks like this:

public class Author : BaseEntity
{
    public virtual string Name { get; private set; }

    public static Author Create(AuthorForCreationDto authorForCreationDto)
    {
        var config = new TypeAdapterConfig();
        config.Apply(new AuthorProfile());
        var mapper = new MapsterMapper.Mapper(config);

        return mapper.Map<Author>(authorForCreationDto);
    }

    public void Update(AuthorForUpdateDto authorForUpdateDto)
    {
        var config = new TypeAdapterConfig();
        config.Apply(new AuthorProfile());
        var mapper = new MapsterMapper.Mapper(config);
        mapper.Map(authorForUpdateDto, this);
    }

    public class AuthorForCreationDto
    {
        public string Name { get; set; }
    }

    public class AuthorForUpdateDto
    {
        public string Name { get; set; }
    }
    
    protected Author() { } // For EF + Mocking
}

Given this setup, when I set up my mapper and try to create an Author, I can't because I don't have a default constructor.

public class AuthorProfile : IRegister
{
    public void Register(TypeAdapterConfig config)
    {        
        config.NewConfig<Author, AuthorDto>()
            .TwoWays();
        config.NewConfig<AuthorForCreationDto, Author>()
            .TwoWays();
        config.NewConfig<Author, AuthorForUpdateDto>()
            .TwoWays();
    }
}

Error:

System.InvalidOperationException : No default constructor for type 'Author', please use 'ConstructUsing' or 'MapWith'

The Gap In Mapster

When I try and use 'ConstructUsing' or 'MapWith' like so:

config.NewConfig<AuthorForCreationDto, Author>()
    .ConstructUsing(x => Author.Create(x));

I get an error, presumably because it is recursively trying to use a mapping inside the creation factory and using the factory for the mapping.

Exit code is 134 (Output is too long. Showing the last 100 lines:
at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Int32, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()

This makes sense given the current setup, but seems to literally block me from leveraging my mappings in my factories. And while the Create factory has a theoretical path, the Update factory is totally blocked as i have no way to construct it.

Discussion

Something like this is doable in Automapper with something like the below (using reflection I think?).

I don't have an immediate idea or recommendation for solving this, but I wanted to present the problem to see if you had any ideas.

Working Automapper

public class Recipe : BaseEntity
{
    public virtual string Title { get; private set; }

    public static Recipe Create(RecipeForCreationDto recipeForCreationDto)
    {
        var mapper = new Mapper(new MapperConfiguration(cfg => {
            cfg.AddProfile<RecipeProfile>();
        }));
        return mapper.Map<Recipe>(recipeForCreationDto);       
    }

    public void Update(RecipeForUpdateDto recipeForUpdateDto)
    {
        var mapper = new Mapper(new MapperConfiguration(cfg => {
            cfg.AddProfile<RecipeProfile>();
        }));
        mapper.Map(recipeForUpdateDto, this);
    }
    
    protected Recipe() { } // For EF + Mocking
}

public class RecipeProfile : Profile
{
    public RecipeProfile()
    {
        CreateMap<Recipe, RecipeDto>()
            .ReverseMap();
        CreateMap<Recipe, RecipeForCreationDto>()
            .ReverseMap();
        CreateMap<Recipe, RecipeForUpdateDto>()
            .ReverseMap();
    }
}

pdevito3 avatar Aug 05 '22 01:08 pdevito3