Немного о нетрадиционном маппинге

    Многие знают про отличную библиотеку 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>>();
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 9
    • 0

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

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

          с чего бы?
          • 0
            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: принят такой стандарт.
            • 0
              Мне показалось речь о том, что при маппинге массива из n «корней агрегации»(мн.ч.) мы получим n*m запросов для одноуровневого агрегата.
    • 0
      Это никуда не годится! Давайте пойдём дальше и вынесем это поведение в (де)сериалайзер, чтобы вместо бестолковых DTO к нам в action methods сразу приходили готовые сущности (сэкономили кучу времени — DTO писать не нужно). А чтобы совсем симметрично было, давайте сделаем кастомный parameter binding: зачем читать "/{id}" как int/long/string, если в итоге нужна сущность? Давайте сразу в готовые сущность такие параметры резолвить! :-)
      • 0
        Покажите ваш вариант, который «годится» и мы сможем обсудить все недостатки данной реализации. Пока в вашем комментарии много критики и не единого указания на конкретные проблемы.
      • +1
        Совет Джефри Рихтера: используйте ToUpperInvariant()

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.