Использование Automapper для сопоставления отношений, ссылающихся на себя

#c# #asp.net-core #automapper

#c# #asp.net-ядро #automapper

Вопрос:

У меня есть отношения с самоссылками, связанные с .NET Core ApplicationUser :

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

Это позволяет вести список «подписчиков» и «подписчиков» в пользовательской модели:

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

Я использую Automapper для сопоставления списков подписчиков и подписчиков с viewmodel. Вот модели представления:

 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; }
}
  

Чтобы учесть различные способы сопоставления подписчиков и последователей, мне пришлось создать два идентичных класса: NetworkUserViewModel и FollowerViewModel чтобы логика сопоставления Automapper могла различать, как сопоставлять подписчиков и как сопоставлять последователей. Вот профиль сопоставления:

          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();
  

Вы можете видеть, что подписчики отображаются как .MapFrom(x => x.Follower.UserName)) , а следующие пользователи отображаются как .MapFrom(x => x.ApplicationUser.UserName)) .

Мой вопрос: могу ли я определить различные способы сопоставления подписчиков и подписанных пользователей без необходимости определять повторяющиеся классы? Я бы предпочел использовать NetworkUserViewModel for followed и followers; так что мне не нужен дубликат FollowerViewModel , если это возможно. Есть ли способ это сделать?

Обновить

Я реализовал предложение @Matthijs (см. Первый комментарий) использовать наследование, чтобы избежать ненужного дублирования свойств. Мои viewmodels теперь такие:

 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
{
}
  

Со следующим изменением логики Automapper:

          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();
  

Этот рефакторинг уменьшает дублирование и упрощает логику сопоставления.

Я оставляю это открытым на несколько дней на случай, если есть решение, основанное исключительно на логике Automapper…

Комментарии:

1. Возможно, вы могли бы (на данный момент) создать базовый класс, содержащий общие свойства, и пусть NetworkUserViewModel и FollowerViewModel производные от него.

2. Спасибо @Matthijs. Я обновил свой ответ вашим предложением, что означает, что я могу избежать дублирования свойств и улучшает логику отображения / читаемость. Я оставляю открытым на всякий случай, если есть решение, основанное на Automapper mogic (я подозреваю, что такого решения нет). Еще раз спасибо

Ответ №1:

Причина, по которой вы столкнулись с проблемами, заключается в том, что целевой объект тот же, но Automapper не может его определить. Automapper просто использует отражение для поиска переменных с тем же именем.

В следующий раз для сложных сопоставлений вы можете подумать о написании пользовательского mapper. Это дает вам большую гибкость и упрощает настройку сопоставления. Вы можете создать пользовательский преобразователь сопоставления, чтобы возвращать нужный вам объект из любого источника, даже если вы укажете несколько источников. Вы сопоставляете источник своему собственному члену назначения. Я нашел это действительно приятным, и это может помочь вам в будущем. Это работает следующим образом:

Распознаватель:

 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;
      }
  

}

И добавьте это так в конфигурацию Mapper:

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

CreateMap<SourceObjectB, MyResponseObject>()
 .ForMember(dest => dest.ObjectB, src => src.MapFrom<MappingResolver>());