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...
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.
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.
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>());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With