How do I map a navigation property that is the same type as the class I'm mapping?

662 views Asked by At

I'm trying to map a class Function to another one called FunctionDTO using AutoMapper. The classes look like this:

public class Function
{
     ...
     public int MasterFunctionId { get; set; }
     public Function MasterFunction { get; set; }
     ...
}

public class FunctionDTO
{
     ...
     public int MasterFunctionId { get; set; }
     public FunctionDTO MasterFunction { get; set; }
     ...
}

The mapping works perfectly for properties such as MasterFunctionId, but MasterFunction is always null, even when the Function object has a value in that property.

The call to the mapper is done in the following way (P.S. the variable config is injected into the class):

query.ProjectTo<FunctionModel>(config)

I can't use the following because the I get an error message, probably because Mapper is not initialized:

CreateMap<FLHFunction, FunctionModel>()
    .ForMember(f => f.PRNummerMaster, opt => opt.MapFrom(src => Mapper.Map<FLHFunction, FunctionModel>(src)));

Is there any way I can configure the mapping in order to make this work? I tried similar solutions to my last piece of code, but I must be missing something.

3

There are 3 answers

2
Kemal Yıldırım On

You need to map the property classes too but since it's referencing itself it's not possible to map a recursive class on to itself.

    public class Function
{
     ...
     public int MasterFunctionId { get; set; }
     public ClassA MasterFunction { get; set; }
     ...
}

public class FunctionDTO
{
     ...
     public int MasterFunctionId { get; set; }
     public ClassB MasterFunction { get; set; }
     ...
}

you must map nested classes first CreateMap<ClassA,ClassB>()

then you can map the wrapper classes which can then use the previous mapping.

0
Leaky On

I looked into it, and unfortunately it seems that this (i.e. self-referencing, recursive mapping) will not work with ProjectTo().

A couple of GitHub issues:

https://github.com/AutoMapper/AutoMapper/issues/3195 https://github.com/AutoMapper/AutoMapper/issues/2171 https://github.com/AutoMapper/AutoMapper/issues/1149

What JBogard always seems to suggest is to use explicitly named hierarchical DTOs for each level. With that approach it's quite trivial to solve the issue (assuming that you just want e.g. 2 levels).

For example:

    public class Function
    {
         public int MasterFunctionId { get; set; }
         public Function MasterFunction { get; set; }
    }

    public class FunctionChildDTO : FunctionDTO {}
    
    public class FunctionDTO
    {
         public int MasterFunctionId { get; set; }
         public FunctionChildDTO MasterFunction { get; set; }
    }

    ...
    
    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Function, FunctionDTO>();
        cfg.CreateMap<Function, FunctionChildDTO>();
        
    });

But let's hope I'm wrong, and someone will be able to provide a proper solution. I'll just leave this answer here meanwhile for information.

0
Steve Py On

As far as I can determine, Automapper does not support direct self-referencing mapping via projections. The documentation is a bit foggy around this as it would seem that you are just missing a MaxDepth configuration option for the recursive associations but I tested that rather thoroughly and while it works for .Map(), it does not appear to work at all for .ProjectTo() even though the documentation indicates that PreserveReferences does not apply to ProjectTo() but MaxDepth does.

What does work is to structure your DTOs to reflect the hierarchy. For example, instead of:

public class FunctionDTO
{
     ...
     public int? MasterFunctionId { get; set; }
     public FunctionDTO MasterFunction { get; set; }
}

For 1 level of self reference you can use:

public class FunctionDTO
{
     ...
     public int? MasterFunctionId { get; set; }
     public MasterFunctionDTO MasterFunction { get; set; }
}

public class MasterFunctionDTO : FunctionDTO
{
    public new FunctionDTO MasterFunction 
    { 
        get{ return null;} 
    }
}  

Where your configuration maps Function to both FunctionDTO and MasterFunctionDTO. The fields in the DTOs would be essentially identical. MasterFunctionDTO's MasterFunction would not be populated, so for clarity it's configuration option should be set to Ignore().

For 2 levels of hierarchy:

public class FunctionDTO
{
     ...
     public int? MasterFunctionId { get; set; }
     public MasterFunctionDTO MasterFunction { get; set; }
}

public class MasterFunctionDTO : FunctionDTO
{
    public new MasterMasterFunctionDTO MasterFunction {get; set;}
}    

public class MasterMasterFunctionDTO : FunctionDTO
{
    public new FunctionDTO MasterFunction 
    { 
        get{ return null;} 
    }
}

With all 3 DTO's mapped back to Function and the final level's "Master" property set to Ignore. Automapper can resolve these references and project them.