Automapper 5.2 (latest by the moment) ignores ExplicitExpansion() configuration if it is configured in the mapping of Base Data Transfer Object. But it still works correctly if mapping is configured directly in Derived DTO. I've got a pair of DTO classes that contain so many duplications in field set and mapping configuration that I am trying to isolate it to common base DTO class, but this issue prevents me from doing it.
Below is the code that illustrates this weird behavior. There are four tests, two of them fail on asserting not expanded property of base DTO. If I move the lines 1-1..1-4 to place 2.1, all the tests pass.
Have I missed some piece of code or is this a bug in Automapper and I have to report this issue to Automapper's bug tracker? Or is it probably "by design", but why? (Ivan Stoev has suggested a working fix, but let me please postpone accepting the answer, because the issue I am facing is not so simple and I added more details in 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 has comprehensively answered how to fix the issue coded above. It works pretty well unless I am forced to use string arrays of field names instead of MemberExpressions. This is related to the fact that this approach crashes with members of Value type (Such as int, int?). It is demonstrated in the first unit test below along with the crash stack trace. I'll ask about it in another question, or rather create an issue in the bug tracker since crash is definitely a bug.
UnitTest2.cs - with fix from Ivan Stoev's answer
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 under the accepted answer. This is an issue of AutoMapper itself. If you can't wait to fix the updated issue, you can make your patch of AutoMapper using the following simple (but not minor) diffs: https://github.com/moudrick/AutoMapper/commit/65005429609bb568a9373d7f3ae0a535833a1729