Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Automapper to map self referencing relationship

I have a self-referencing relationship involving .NET Core's ApplicationUser:

public class Network
{
    public ApplicationUser ApplicationUser { get; set; }
    public string ApplicationUserId { get; set; }
    public ApplicationUser Follower { get; set; }
    public string FollowerId { get; set; }
}

This makes it possible to keep a list of 'followers' and 'following' in the user model:

public class ApplicationUser : IdentityUser
{
    //...
    public ICollection<Network> Following { get; set; }
    public ICollection<Network> Followers { get; set; }
}

I use Automapper to map the followers and followed lists to a viewmodel. Here are the viewmodels:

public class UserProfileViewModel
{
    //...
    public IEnumerable<FollowerViewModel> Followers { get; set; }
    public IEnumerable<NetworkUserViewModel> Following { get; set; }
}

public class NetworkUserViewModel
{
    public string UserName { get; set; }
    public string ProfileImage { get; set; }
    public bool IsFollowing { get; set; } = true;
    public bool IsOwnProfile { get; set; }
}

public class FollowerViewModel
{
    public string UserName { get; set; }
    public string ProfileImage { get; set; }
    public bool IsFollowing { get; set; } = true;
    public bool IsOwnProfile { get; set; }
}

To take account of the different ways that followers and followed are mapped, I have had to create two identical classes: NetworkUserViewModel and FollowerViewModel so that the Automapper mapping logic can distinguish how to map followers and how to map followed. Here is the mapping profile:

         CreateMap<Network, NetworkUserViewModel>()
         .ForMember(x => x.UserName, y => y.MapFrom(x => x.ApplicationUser.UserName))
         .ForMember(x => x.ProfileImage, y => y.MapFrom(x => x.ApplicationUser.ProfileImage))
         .ReverseMap();

        CreateMap<Network, FollowerViewModel>()
        .ForMember(x => x.UserName, y => y.MapFrom(x => x.Follower.UserName))
        .ForMember(x => x.ProfileImage, y => y.MapFrom(x => x.Follower.ProfileImage))
        .ReverseMap();

        CreateMap<ApplicationUser, UserProfileViewModel>()
          .ForMember(x => x.UserName, y => y.MapFrom(x => x.UserName))
          .ForMember(x => x.Followers, y => y.MapFrom(x => x.Followers))
          .ForMember(x => x.Following, y => y.MapFrom(x => x.Following))
          .ReverseMap();

You can see that followers are mapped like .MapFrom(x => x.Follower.UserName)) while following users are mapped like .MapFrom(x => x.ApplicationUser.UserName)).

My question is: can I define the different ways of mapping followers and followed users without having to define duplicate classes? I would prefer to use the NetworkUserViewModel for followed and followers; so that I do not need the duplicate FollowerViewModel, if possible. Is there a way to do it?

Update

I have implemented the suggestion by @Matthijs (see first comment) to use inheritence to avoid the unnecessary duplication of properties. My viewmodels are now these:

public class UserProfileViewModel
{
    //...
    public IEnumerable<FollowerViewModel> Followers { get; set; }
    public IEnumerable<FollowingViewModel> Following { get; set; }
}

public class NetworkUserViewModel
{
    public string UserName { get; set; }
    public string ProfileImage { get; set; }
    public bool IsFollowing { get; set; }
    public bool IsOwnProfile { get; set; }
}

public class FollowingViewModel : NetworkUserViewModel
{
}

public class FollowerViewModel : NetworkUserViewModel
{
}

With the following change to the Automapper logic:

         CreateMap<Network, FollowingViewModel>()
         .ForMember(x => x.UserName, y => y.MapFrom(x => x.ApplicationUser.UserName))
         .ForMember(x => x.ProfileImage, y => y.MapFrom(x => x.ApplicationUser.ProfileImage))
         .ReverseMap();

        CreateMap<Network, FollowerViewModel>()
        .ForMember(x => x.UserName, y => y.MapFrom(x => x.Follower.UserName))
        .ForMember(x => x.ProfileImage, y => y.MapFrom(x => x.Follower.ProfileImage))
        .ReverseMap();

        CreateMap<ApplicationUser, UserProfileViewModel>()
          .ForMember(x => x.UserName, y => y.MapFrom(x => x.UserName))
          .ForMember(x => x.Followers, y => y.MapFrom(x => x.Followers))
          .ForMember(x => x.Following, y => y.MapFrom(x => x.Following))
          .ReverseMap();

This refactor reduces the duplication and makes the mapping logic easier to follow.

I'm leaving this open for a few days just in case there is a solution relying purely on the Automapper logic...

like image 952
Winthorpe Avatar asked Apr 13 '19 20:04

Winthorpe


People also ask

When should you not use AutoMapper?

If you have to do complex mapping behavior, it might be better to avoid using AutoMapper for that scenario. Reverse mapping can get very complicated very quickly, and unless it's very simple, you can have business logic showing up in mapping configuration.

Can AutoMapper map enums?

The built-in enum mapper is not configurable, it can only be replaced. Alternatively, AutoMapper supports convention based mapping of enum values in a separate package AutoMapper.


1 Answers

The reason you ran into problems is because the destination object is the same, but Automapper cannot infer it. Automapper simply uses reflection to find variables with the same name.

For complicated mappings, next time, you can consider writing a custom mapper. This gives you great flexibility and it simplifies your mapping configuration. You can create a custom mapping resolver to return the object you need from any source, even if you specify multiple sources. You map the source to your own destination member. I found this to be really nice and it might help you in the future. It works like this:

Resolver:

public class MappingResolver: IValueResolver<object, object, MyResponseObject>
{
    // Automapper needs parameterless constructor
    public MappingResolver()
    {
    }

    // Resolve dependencies here if needed
    public MappingResolver(IWhateverINeed whateverINeed)
    {
}

    public MyResponseObject Resolve(
        object source, object destination, MyResponseObject> destMember, ResolutionContext context)
    {
        if (source.GetType() == typeof(NetworkUserViewModel) 
        {
          // Specific logic for source object, while destination remains the same response
          var castedObject = source as NetworkUserViewModel;
          return MyResponseObject;
      }

}

And add it like this to Mapper config:

CreateMap<SourceObjectA, MyResponseObject>()
 .ForMember(dest => dest.ObjectA, src => src.MapFrom<MappingResolver>());

CreateMap<SourceObjectB, MyResponseObject>()
 .ForMember(dest => dest.ObjectB, src => src.MapFrom<MappingResolver>());
like image 194
Jeffrey Verpoort Avatar answered Oct 01 '22 22:10

Jeffrey Verpoort