c# AutoMapper - Defining a Map in multiple places

63 Views Asked by At

A project I'm working on (still in .NET Framework, in case that matters) uses AutoMapper for two separate projects that use the same Database and share a LOT of the same view model classes from a Common class library project, e.g.:

// in Project 1
Mapper.Initialize(cfg =>
{
    cfg.CreateMap<A, AModel>()
        .ForMember(dest => dest.Prop1, opt => opt.MapFrom(src => src.PropertyName))
        .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.OtherPropertyName));

    cfg.CreateMap<B2, BModel>()
        .ForMember(dest => dest.Prop1, opt => opt.MapFrom(src => src.PropertyName))
        .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.OtherPropertyName))
        .ForMember(dest => dest.Prop3, opt => opt.MapFrom(src => src.OtherOTHERPropertyName));
}

// in Project 2
Mapper.Initialize(cfg =>
{
    cfg.CreateMap<A, AModel>()
        .ForMember(dest => dest.Prop1, opt => opt.MapFrom(src => src.PropertyName))
        .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.OtherPropertyName));

    cfg.CreateMap<B2, BModel>()
        .ForMember(dest => dest.Prop1, opt => opt.MapFrom(src => src.PropertyName))
        .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.OtherPropertyName))
        .ForMember(dest => dest.Prop3, opt => opt.Ignore());
}

I'd like to move all of the duplicate mappings (which is most of them) into a profile or something in the Common project:

public class CommonAutoMapperProfile : Profile
{
    public CommonAutoMapperProfile()
    {
        CreateMap<A, AModel>()
            .ForMember(dest => dest.Prop1, opt => opt.MapFrom(src => src.PropertyName))
            .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.OtherPropertyName));

        CreateMap<B, BModel>()
            .ForMember(dest => dest.Prop1, opt => opt.MapFrom(src => src.PropertyName))
            .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.OtherPropertyName));
    }
}

This is no problem for the COMPLETELY identical mappings like A->AModel, but I'm struggling with ones like B->BModel that differ by even ONE ForMember call (Prop3 in this case), which Project 1 sets and Project 2 ignores. It DOES apply that rule, but it seems that when the second CreateMap is called, ALL mappings set by CommonAutoMapperProfile get thrown out entirely:

// in project 1
Mapper.Initialize(cfg =>
{
    cfg.AddProfile<CommonAutoMapperProfile>();
    cfg.CreateMap<B, BModel>()
        .ForMember(dest => dest.Prop3, opt => opt.MapFrom(src => src.OtherOTHERPropertyName));
    // BModel.Prop1 and BModel.Prop2 are still null; BModel.Prop3 is the only one being set
}

I'd LIKE it to "merge" the mappings together, i.e. retain the property mappings defined in CommonAutoMapperProfile and add the project-specific ones on top. Is that something AutoMapper just... CAN'T do?

I've also attempted creating ANOTHER Profile class that is specific to each project, with unfortunately the same results, whether I'm adding it with another cfg.AddProfile call:

public class Project1AutoMapperProfile : Profile
{
    public Project1AutoMapperProfile()
    {
        CreateMap<B, BModel>()
            .ForMember(dest => dest.Prop3, opt => opt.MapFrom(src => src.OtherOTHERPropertyName));
    }
}

// elsewhere in Project 1...
Mapper.Initialize(cfg =>
{
    cfg.AddProfile<CommonAutoMapperProfile>();
    cfg.AddProfile<Project1AutoMapperProfile>();
}

or having it inherit from CommonAutoMapperProfile:

public class Project1AutoMapperProfile : CommonAutoMapperProfile
{
    public Project1AutoMapperProfile()
    {
        CreateMap<B, BModel>()
            .ForMember(dest => dest.Prop3, opt => opt.MapFrom(src => src.OtherOTHERPropertyName));
    }
}

// elsewhere in Project 1...
Mapper.Initialize(cfg =>
{
    cfg.AddProfile<Project1AutoMapperProfile>();
}

Both cases had the exact same results as the first case: ONLY the mappings in Project1AutoMapperProfile were being applied

1

There are 1 best solutions below

2
NPras On BEST ANSWER

As mentioned in the comments, what you have is not an ideal design. But I understand that sometimes we have to work with the cards we're dealt (e.g. you have no access to modify the models).

As the name implies, calling CreateMap multiple times for the same mapping will just keep creating a new one. If you want to build up from an existing one, you have to append/modify. There's no built-in facility to get and existing map and modify it, but we can be a little sneaky and implement it ourselves.


NOTE: I still do not recommend doing this. If you can break up your models, do that and use the recommended model inheritance.


That said, the code below works. For the current version of AutoMapper. For this specific case. Make sure you have good test coverage, in case it breaks in the future, or for some of your more complex models.

public class B { public int Prop1; public int Prop2; }
public class BModel { public int PropB1; public int PropB2; }

public class CommonProfile : Profile
{
  public CommonProfile() 
  {
    // Map Prop1, ignore Prop2
    CreateMap<B, BModel>()
      .ForMember(d => d.PropB1, o => o.MapFrom(s => s.Prop1))
      .ForMember(d => d.PropB2, o => o.Ignore());
  }
  
  protected IMappingExpression<T,TT> CreateOrGetMap<T, TT>() 
  {
    // Sneaky access the un-exposed existing configs.
    var existing = ((IProfileConfiguration)this).TypeMapConfigs
      .Where(c => c.SourceType == typeof(T) && c.DestinationType == typeof(TT))
      .FirstOrDefault();
      
    // If found existing map, return it. Otherwise, create new one.
    return (existing as IMappingExpression<T,TT>) ?? CreateMap<T, TT>();
  }
}

We then create a derived profile which will modify the mapping in the Common one.

public class Proj2Profile : CommonProfile {
  public Proj2Profile() 
  {
    // Since there's already a map configured in the parent profile, 
    // this will modify it.
    CreateOrGetMap<B, BModel>()
      .ForMember(d => d.PropB2, o => o.MapFrom(s => s.Prop2));
  }
}