2017-01-30 1 views
7

Automapper 5.2 (последний на данный момент) игнорирует конфигурацию ExplicitExpansion(), если она настроена при сопоставлении базового объекта передачи данных. Но он по-прежнему работает правильно, если отображение настроено непосредственно в Derived DTO. У меня есть пара классов DTO, которые содержат так много дубликатов в настройках полей и сопоставления, которые я пытаюсь изолировать в общем базовом классе DTO, но эта проблема мешает мне это делать.Automapper 5.2 игнорирует ExplicitExpansion, если он настроен в сопоставлении базового DTO

Ниже приведен код, иллюстрирующий это странное поведение. Есть четыре теста, два из которых не могут утверждать не расширенное свойство базы DTO. Если я переместил строки 1-1..1-4 на место 2.1, все тесты пройдут.

Я пропустил какой-то фрагмент кода или это ошибка в Automapper, и я должен сообщить об этой проблеме автозагрузчику Automapper? Или это возможно «по дизайну», но почему? (Иван Стоев предложил исправление, но позвольте мне отложить принятие ответа, потому что проблема, с которой я сталкиваюсь, не так проста, и я добавил более подробные сведения об обновлении ниже).

UnitTest1.cs:

using System.Collections.Generic; 
using System.Linq; 
using AutoMapper; 
using AutoMapper.QueryableExtensions; 
using Microsoft.VisualStudio.TestTools.UnitTesting; 

namespace AutoMapperIssue 
{ 
    public class Source { public string Name; public string Desc; } 
    public class DtoBase    { public string Name { get; set; } } 
    public class DtoDerived : DtoBase { public string Desc { get; set; } } 
    [TestClass] public class UnitTest1 
    { 
     [AssemblyInitialize] public static void AssemblyInit(TestContext context) 
     { 
      Mapper.Initialize(cfg => 
      { 
       cfg.CreateMap<Source, DtoBase>() 
        .ForMember(dto => dto.Name, conf => { // line 1-1 
         conf.MapFrom(src => src.Name); // line 1-2 
         conf.ExplicitExpansion();   // line 1-3 
        })         // line 1-4 
        .Include<Source, DtoDerived>(); 
       cfg.CreateMap<Source, DtoDerived>() 
        // place 2.1 
        .ForMember(dto => dto.Desc, conf => { 
         conf.MapFrom(src => src.Desc); 
         conf.ExplicitExpansion(); 
        }); 
      }); 
      Mapper.Configuration.CompileMappings(); 
      Mapper.AssertConfigurationIsValid(); 
     } 

     private readonly IQueryable<Source> _iq = new List<Source> { 
      new Source() { Name = "Name1", Desc = "Descr",}, 
     } .AsQueryable(); 

     [TestMethod] public void ProjectAll_Success() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(_ => _.Name, _ => _.Desc); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNotNull(first.Desc); Assert.AreEqual("Descr", first.Desc); 
      Assert.IsNotNull(first.Name); Assert.AreEqual("Name1", first.Name); 
     } 
     [TestMethod] public void SkipDerived_Success() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(_ => _.Name); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNotNull(first.Name); Assert.AreEqual("Name1", first.Name); 
      Assert.IsNull(first.Desc, "Should not be expanded."); 
     } 
     [TestMethod] public void SkipBase_Fail() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(_ => _.Desc); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNotNull(first.Desc); Assert.AreEqual("Descr", first.Desc); 
      Assert.IsNull(first.Name, "Should not be expanded. Fails here. Why?"); 
     } 
     [TestMethod] public void SkipAll_Fail() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNull(first.Desc, "Should not be expanded."); 
      Assert.IsNull(first.Name, "Should not be expanded. Fails here. Why?"); 
     } 
    } 
} 

packages.config:

<package id="AutoMapper" version="5.2.0" targetFramework="net452" /> 

UPD. Иван Стоев всесторонне ответил, как исправить проблему, закодированную выше. Он работает очень хорошо, если я не вынужден использовать строковые массивы имен полей вместо MemberExpressions. Это связано с тем, что этот подход падает с членами типа Value (например, int, int?). Это продемонстрировано в первом блочном тесте ниже вместе с трассировкой стека аварий. Я спрошу об этом в другом вопросе или, скорее, создаст проблему в трекере с ошибкой, так как крах определенно является ошибкой.

UnitTest2.cs - с исправлением от ответа Ивана Stoev в

using System; 
using System.Collections.Generic; 
using System.Linq; 
using AutoMapper; 
using AutoMapper.QueryableExtensions; 
using Microsoft.VisualStudio.TestTools.UnitTesting; 

namespace AutoMapperIssue.StringPropertyNames 
{ /* int? (or any ValueType) instead of string - .ProjectTo<> crashes on using MemberExpressions in projction */ 
    using NameSourceType = Nullable<int> /* String */; using NameDtoType = Nullable<int> /* String */; 
    using DescSourceType = Nullable<int> /* String */; using DescDtoType = Nullable<int> /* String*/; 

    public class Source 
    { 
     public NameSourceType Name { get; set; } 
     public DescSourceType Desc { get; set; } 
    } 

    public class DtoBase    { public NameDtoType Name { get; set; } } 
    public class DtoDerived : DtoBase { public DescDtoType Desc { get; set; } } 

    static class MyMappers 
    { 
     public static IMappingExpression<TSource, TDestination> Configure<TSource, TDestination>(this IMappingExpression<TSource, TDestination> target) 
      where TSource : Source 
      where TDestination : DtoBase 
     { 
      return target.ForMember(dto => dto.Name, conf => 
       { 
        conf.MapFrom(src => src.Name); 
        conf.ExplicitExpansion(); 
       }); 
     } 
    } 

    [TestClass] public class UnitTest2 
    { 
     [ClassInitialize] public static void ClassInit(TestContext context) 
     { 
      Mapper.Initialize(cfg => 
      { 
       cfg.CreateMap<Source, DtoBase>() 
        .Configure() 
        .Include<Source, DtoDerived>(); 
       cfg.CreateMap<Source, DtoDerived>() 
        .Configure() 
        .ForMember(dto => dto.Desc, conf => { 
         conf.MapFrom(src => src.Desc); 
         conf.ExplicitExpansion(); 
        }) 
       ; 
      }); 
      Mapper.Configuration.CompileMappings(); 
      Mapper.AssertConfigurationIsValid(); 
     } 

     private static readonly IQueryable<Source> _iq = new List<Source> { 
      new Source() { Name = -25 /* "Name1" */, Desc = -12 /* "Descr" */, }, 
     } .AsQueryable(); 

     private static readonly Source _iqf = _iq.First(); 

     [TestMethod] public void ProjectAllWithMemberExpression_Exception() 
     { 
      _iq.ProjectTo<DtoDerived>(_ => _.Name, _ => _.Desc); // Exception here, no way to use Expressions with current release 
//Test method AutoMapperIssue.StringPropertyNames.UnitTest2.ProjectAllWithMemberExpression_Exception threw exception: 
//System.NullReferenceException: Object reference not set to an instance of an object. 
// 
// at System.Linq.Enumerable.<SelectManyIterator>d__16`2.MoveNext() 
// at System.Linq.Enumerable.<DistinctIterator>d__63`1.MoveNext() 
// at System.Linq.Buffer`1..ctor(IEnumerable`1 source) 
// at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source) 
// at AutoMapper.QueryableExtensions.ProjectionExpression.To[TResult](IDictionary`2 parameters, IEnumerable`1 memberPathsToExpand) 
// at AutoMapper.QueryableExtensions.ProjectionExpression.To[TResult](Object parameters, Expression`1[] membersToExpand) 
// at AutoMapper.QueryableExtensions.Extensions.ProjectTo[TDestination](IQueryable source, IConfigurationProvider configuration, Object parameters, Expression`1[] membersToExpand) 
// at AutoMapper.QueryableExtensions.Extensions.ProjectTo[TDestination](IQueryable source, Expression`1[] membersToExpand) 
// at AutoMapperIssue.StringPropertyNames.UnitTest2.ProjectAllWithMemberExpression_Exception() in D:\01\AutoMapperIssue\UnitTest2.cs:line 84 
     } 
#pragma warning disable 649 
     private DtoDerived d; 
#pragma warning restore 649 
     [TestMethod] public void ProjectAll_Fail() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(null, new string[] { nameof(d.Name), nameof(d.Desc) } /* _ => _.Name, _ => _.Desc */); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNotNull(first.Desc, "Should be expanded.");     Assert.AreEqual(_iqf.Desc, first.Desc); 
      Assert.IsNotNull(first.Name, "Should be expanded. Fails here, why?"); Assert.AreEqual(_iqf.Name, first.Name); 
     } 
     [TestMethod] public void BaseOnly_Fail() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(null, new string[] { nameof(d.Name) } /* _ => _.Name */); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNull(first.Desc, "Should NOT be expanded."); 
      Assert.IsNotNull(first.Name, "Should be expanded. Fails here, why?"); Assert.AreEqual(_iqf.Name, first.Name); 

     } 
     [TestMethod] public void DerivedOnly_Success() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(null, new string[] { nameof(d.Desc) } /* _ => _.Desc */); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNotNull(first.Desc, "Should be expanded."); Assert.AreEqual(_iqf.Desc, first.Desc); 
      Assert.IsNull(first.Name, "Should NOT be expanded."); 
     } 
     [TestMethod] public void SkipAll_Success() 
     { 
      var projectTo = _iq.ProjectTo<DtoDerived>(null, new string[] { }); 
      Assert.AreEqual(1, projectTo.Count()); var first = projectTo.First(); 
      Assert.IsNull(first.Desc, "Should NOT be expanded."); 
      Assert.IsNull(first.Name, "Should NOT be expanded."); 
     } 
    } 
} 

UPD2. Обновленный вопрос выше определенно не может быть установлен за пределами, см. Комментарий в соответствии с принятым ответом. Это проблема самого AutoMapper. Если вы не можете ждать, чтобы исправить обновленный вопрос, вы можете сделать свой патч AutoMapper используя следующие простые (но не незначительные) различия: https://github.com/moudrick/AutoMapper/commit/65005429609bb568a9373d7f3ae0a535833a1729

ответ

5

я пропустил некоторый кусок кода

Вы ничего не пропустили.

или это ошибка в Automapper, и я должен сообщить об этой проблеме в отладчик ошибок Automapper? Или это возможно «по дизайну», но почему?

Я сомневаюсь, что это «по дизайну», скорее всего, это ошибка или неполная быстрая и грязная реализация. Это можно увидеть внутри source code метода ApplyInheritedPropertyMap класса PropertyMap, который отвечает за объединение базовых свойств и конфигураций полученных свойств.В "унаследованных" свойства картографирования в настоящее время являются:

  • CustomExpression
  • CustomResolver
  • Condition
  • PreCondition
  • NullSubstitute
  • MappingOrder
  • ValueResolverConfig

в то время как следующие (в основном все bool типа) свойств (в том числе один в вопросе) не являются:

  • AllowNull
  • UseDestinationValue
  • ExplicitExpansion

Проблема ИМО что текущая реализация не может определить, установлено ли свойство bool явно или нет. Конечно, его можно легко устранить, заменив свойства auto явным bool? базовым полем и логикой значений по умолчанию (и дополнительным плавным способом настройки, чтобы отключить его, если он включен внутри конфигурации базового класса). К сожалению, это может быть сделано только в исходном коде, поэтому я предлагаю вам сообщить об этом проблеме на свой трекер.

До (и если) они это исправить, я мог бы предложить в качестве обходного пути перемещения всех общего кода до методов пользовательских расширений как

static class MyMappers 
{ 
    public static IMappingExpression<TSource, TDestination> Configure<TSource, TDestination>(this IMappingExpression<TSource, TDestination> target) 
     where TSource : Source 
     where TDestination : DtoBase 
    { 
     return target 
      .ForMember(dto => dto.Name, conf => 
      { 
       conf.MapFrom(src => src.Name); 
       conf.ExplicitExpansion(); 
      }); 
    } 
} 

и использовать их из основного кода конфигурации:

Mapper.Initialize(cfg => 
{ 
    cfg.CreateMap<Source, DtoBase>() 
     .Configure(); 

    cfg.CreateMap<Source, DtoDerived>() 
     .Configure() 
     .ForMember(dto => dto.Desc, conf => { 
      conf.MapFrom(src => src.Desc); 
      conf.ExplicitExpansion(); 
     }); 
}); 

Редактировать: Относительно дополнительных вопросов. Оба являются более серьезными ошибками обработки AM, не связанными с конфигурацией.

Проблема заключается в том, что они пытаются использовать сравнение экземпляров MemberInfo для фильтрации проекции.

Первый случай (с выражениями) не для типов значений, поскольку реализация, которая пытается извлечь из MemberInfoExpression<Func<T, object>> ожидает только MemberExpression, но в случае типов значений это обернуто внутри Expression.Convert.

Второй случай (с именами свойств) не потому, что они не считают тот факт, что MemberInfo для собственности, унаследованный от базового класса извлеченной из время компиляции лямбда-выражения отличается от того же получены путем отражения или которое продемонстрировано со следующим тестом:

// From reflection 
var nameA = typeof(DtoDerived).GetMember(nameof(DtoDerived.Name)).Single(); 
// Same as 
//var nameA = typeof(DtoDerived).GetProperty(nameof(DtoDerived.Name)); 

// From compile time expression 
Expression<Func<DtoDerived, NameDtoType>> compileTimeExpr = _ => _.Name; 
var nameB = ((MemberExpression)compileTimeExpr.Body).Member; 

// From runtime expression 
var runTimeExpr = Expression.PropertyOrField(Expression.Parameter(typeof(DtoDerived)), nameof(DtoDerived.Name)); 
var nameC = runTimeExpr.Member; 

Assert.AreEqual(nameA, nameC); // Success 
Assert.AreEqual(nameA, nameB); // Fail 

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

+0

Иван, спасибо за всесторонний ответ! Я готов принять ваш ответ незадолго до окончания периода выплаты, если никто не ответил на обновленную проблему, но позвольте мне оставить щедрость открытой, надеясь получить разрешение на фактическую проблему, с которой я столкнулся. Кроме того, не могли бы вы ответить на обновленную проблему? – moudrick

+0

Конечно, я посмотрю. –

+1

@moudrick К сожалению, дополнительные проблемы не могут быть решены извне. Фактически, выражение с выражениями может быть легко разрешено, но для этого не требуется использовать другое имя метода, чем 'ProjectTo', чтобы не столкнуться с AM' QueryableExtensions'. Дайте знать, если вас это заинтересовало. Печально то, что на уровне исходного кода исправление довольно простое. –

Смежные вопросы