Многие знают про отличную библиотеку Automapper. С преобразованием Entity -> Dto проблем обычно не возникает. Но как обрабатывать обратный маппинг в случае, когда в API приходит корень агрегации? Хорошо, если read и write — контексты разделены и писать можно из Dto. Чаще, однако, нужно выбрать соответствующие сущности из ORM по Id и сохранить агрегат целиком. Занятие это муторное, однако зачастую поддающееся автоматизации.

Объявляем такой TypeConverter:

public class EntityTypeConverter<TDto, TEntity> : ITypeConverter<TDto, TEntity>
        where TEntity: PersistentObject, new()
    {
        public TEntity Convert(ResolutionContext context)
        {
            // Забираем из контейнера DbContext
            // Да, ServiceLocator это плохо, но о том как прикрутить IOC вы сможете и сами прочесть по ссылке
            // http://stackoverflow.com/questions/4204664/automapper-together-with-dependency-injection
            var dc = ApplicationContext.Current.Container.Resolve<IDbContext>();
            var sourceId = (context.SourceValue as IEntity)?.Id;

            var dest = context.DestinationValue as TEntity
                        ?? (sourceId.HasValue && sourceId.Value != 0 ? dc.Get<TEntity>(sourceId.Value) : new TEntity());

            // Да, reflection, да медленно и может привести к ошибкам в рантайме.
            // Можете написать Expression Trees, скомпилировать и закешировать для производительности
            // И анализатор для проверки корректности Dto на этапе компиляции
            var sp = typeof(TDto)
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(x => x.CanRead && x.CanWrite)
                .ToDictionary(x => x.Name.ToUpper(), x => x);

            var dp = typeof(TEntity)
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(x => x.CanRead && x.CanWrite)
                .ToArray();

            // проходимся по всем свойствам целевого объекта
            foreach (var propertyInfo in dp)
            {
                var key = propertyInfo.PropertyType.InheritsOrImplements(typeof(PersistentObject))
                    ? propertyInfo.Name.ToUpper() + "ID"
                    : propertyInfo.Name.ToUpper();

                if (sp.ContainsKey(key))
                {
                    // маппим один к одному примитивы, связанные сущности тащим из контекста
                    propertyInfo.SetValue(dest, key.EndsWith("ID")
                        && propertyInfo.PropertyType.InheritsOrImplements(typeof(PersistentObject))
                            ? dc.Get(propertyInfo.PropertyType, sp[key].GetValue(context.SourceValue))
                            : sp[key].GetValue(context.SourceValue));
                }
            }  

            return dest;
        }
    }

И создаем маппинг:

AutoMapper.Mapper
                .CreateMap<TDto, TEntity>()
                .ConvertUsing<EntityTypeConverter<TDto, TEntity>>();
Поделиться с друзьями
-->

Комментарии (9)


  1. kemsky
    09.08.2016 00:27

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


    1. marshinov
      09.08.2016 09:10

      TypeConverter by design лишает вас валидации маппинга, хотя можно написать вручную. В данном случае я предполагаю, что Dto может использоваться и для частичного update существующей сущности, соответственно необходимо обновить только часть полей. Ситуация с обработкой ошибок не совсем понятна, потому что зависит от того как вы обрабатываете ошибки в своем приложении: это не тема данного материала. По поводу «лезет в базу и вытаскивает по одной»: вы все-равно будете лезть в базу и вытаскивать по одной для корней агрегации (на то они и корни). Исключением может быть наличие нескольких связей с другой сущностью, но это очень редкий случай.


      1. areht
        09.08.2016 20:36

        > вы все-равно будете лезть в базу и вытаскивать по одной для корней агрегации

        с чего бы?


        1. marshinov
          09.08.2016 21:41

          public class Ar  : Entity
          {
               public Foo Foo { get; set; }
          
               public Bar Bar { get; set; }
          }
          
          public class ArDto
          {
               public int FooId { get; set; }
          
               public int BarId { get; set; }
          }
          


          Покажите пожалуйста, как вы для сохранения Ar в БД получите инстансы Foo и Bar одним запросом? В первом абзаце я явно указал, что CQRS не используется, писать из Dto сразу в БД нельзя, нужно создавать «правильную» entity и сохранять ее с помощью ORM. У Ar нет FooId и BarId: принят такой стандарт.


          1. areht
            09.08.2016 22:15

            Мне показалось речь о том, что при маппинге массива из n «корней агрегации»(мн.ч.) мы получим n*m запросов для одноуровневого агрегата.


            1. marshinov
              10.08.2016 10:51
              -2

              Это не так.


  1. js605451
    09.08.2016 06:29

    Это никуда не годится! Давайте пойдём дальше и вынесем это поведение в (де)сериалайзер, чтобы вместо бестолковых DTO к нам в action methods сразу приходили готовые сущности (сэкономили кучу времени — DTO писать не нужно). А чтобы совсем симметрично было, давайте сделаем кастомный parameter binding: зачем читать "/{id}" как int/long/string, если в итоге нужна сущность? Давайте сразу в готовые сущность такие параметры резолвить! :-)


    1. marshinov
      09.08.2016 09:04

      Покажите ваш вариант, который «годится» и мы сможем обсудить все недостатки данной реализации. Пока в вашем комментарии много критики и не единого указания на конкретные проблемы.


  1. halex2005
    09.08.2016 20:19
    +1

    Совет Джефри Рихтера: используйте ToUpperInvariant()