Automapper 5.2 ignores ExplicitExpansion if it is configured in a basic DTO mapping

Automapper 5.2 (the latter at the moment) ignores the ExplicitExpansion () configuration if it is configured when mapping the underlying data transfer object. But it still works correctly if the mapping is configured directly in the Derived DTO. I have a couple of DTO classes that contain so many duplicates in field settings and mappings that I'm trying to isolate in a common DTO base class, but this problem is stopping me from doing this.

The code below illustrates this strange behavior. There are four tests, two of which cannot claim a non-extended DTO base property. If I move lines 1-1..1-4 to place 2.1, all tests pass.

Am I missing a piece of code or is this an error in Automapper and should I report this issue to the Automapper error answering machine? Or it is possible "by design", but why? (Ivan Stoev suggested a working fix, but let me postpone accepting the answer, because the problem I am facing is not so simple, and I added more detailed information about the update below).

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 . Ivan Stoev comprehensively answered how to fix the problem encoded above. It works very well if I am not forced to use string arrays of field names instead of MemberExpressions. This is due to the fact that this approach falls with members of type Value (for example, int, int?). It is demonstrated in the first unit test below, along with a crash stack trace. I will ask about this in another question or, rather, will create a problem in the tracker with an error, since the crash is definitely an error.

UnitTest2.cs - with the correction of the answer of Ivan 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. The updated issue above definitely cannot be fixed outside, see the comment according to the accepted answer. This is a problem for AutoMapper itself. If you can’t wait to fix the updated problem, you can make your AutoMapper patch using the following simple (but not minor) differences: https://github.com/moudrick/AutoMapper/commit/65005429609bb568a9373d7f3ae0a535833a1729

+7
inheritance c # automapper
source share
1 answer

I missed some piece of code

You have not missed anything.

or is this a bug in Automapper and should I report this issue to an autopilot answering machine? Or it is possible "by design", but why?

I doubt that this is “by design,” most likely a mistake or an incomplete quick and dirty implementation. This can be seen inside the source code of the ApplyInheritedPropertyMap method of the PropertyMap class, which is responsible for combining the base property and derived properties. “Inherited” display properties currently:

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

while the following (basically all bool types) (including this question) are not:

  • AllowNull
  • UseDestinationValue
  • ExplicitExpansion

The IMO problem is that the current implementation cannot determine if the bool property is set explicitly or not. Of course, it can be easily fixed by replacing the automatic properties with the explicit bool? field bool? and the logic of the default values ​​(and an additional free way to configure it to disable it if it is enabled inside the base class configuration). Unfortunately, this can only be done in the source code, so I would advise you to report this problem to your tracker.

Before (and if) they fix this, I could suggest, as a workaround, moving all the common code to custom extension methods, like

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

and use them from the main configuration code:

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

Edit: Concerning additional issues. Both are more serious non-configuration AM processing errors.

The problem is that they are trying to use MemberInfo instance comparison to filter projection.

The first case (with an expression) fails for value types because an implementation that tries to retrieve MemberInfo from Expression<Func<T, object>> expects only MemberExpression , but in the case of value types, it terminates inside Expression.Convert .

The second case (with property names) fails because they do not take into account the fact that MemberInfo for a property inherited from the base class extracted from the compilation-time lambda expression is different from that obtained by reflecting or executing the created expression, which demonstrated with the following test:

 // 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 

You definitely need to report all problems. I would say that a function is compromised for any property of type value when delivering a list of expressions and for any inherited property when delivering names.

+5
source share

All Articles