如果 Automapper 5.2 在 Base DTO 映射中配置,则忽略 ExplicitExpansion

Automapper 5.2 ignores ExplicitExpansion if it is configured in Base DTO mapping

Automapper 5.2(目前最新)忽略 ExplicitExpansion() 配置,如果它是在基本数据传输对象的映射中配置的。但如果直接在 Derived DTO 中配置映射,它仍然可以正常工作。我有一对 DTO classes,它们在字段集和映射配置中包含如此多的重复,我试图将它隔离到公共基础 DTO class,但这个问题阻止我这样做.

下面是说明这种奇怪行为的代码。有四个测试,其中两个在断言基础 DTO 未扩展 属性 时失败。如果我将 1-1..1-4 行移动到 2.1,所有测试都会通过。

我是否遗漏了一些代码,或者这是 Automapper 中的错误,我必须将此问题报告给 Automapper 的错误跟踪器?或者可能是 "by design",但为什么呢? (Ivan Stoev 提出了一个可行的解决方案,但请允许我推迟接受答案,因为我面临的问题并不那么简单,我在下面的更新中添加了更多详细信息)。

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 已经全面回答了如何解决上面编码的问题。它工作得很好,除非我被迫使用字段名称的字符串数组而不是 MemberExpressions。这与此方法与 Value 类型的成员(如 int、int?)崩溃有关。它在下面的第一个单元测试中与崩溃堆栈跟踪一起演示。我会在另一个问题中询问它,或者更确切地说在错误跟踪器中创建一个问题,因为崩溃绝对是一个错误。

UnitTest2.cs - 根据 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:\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

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?

我怀疑是 "by design",很可能是错误或不完整的 quick-and-dirty 实现。可以看到PropertyMapclass的ApplyInheritedPropertyMap方法里面的source code,负责结合base属性和derived属性 配置。 "inherited" 当前的映射属性是:

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

而以下(基本上所有 bool 类型)属性(包括有问题的属性)不是:

  • AllowNull
  • UseDestinationValue
  • ExplicitExpansion

IMO 的问题是当前的实现无法确定 bool 属性 是否被显式设置。当然,它可以很容易地通过用显式 bool? 支持字段和默认值逻辑替换自动属性来解决(以及额外的流畅配置方法来关闭它,以防它在基础 class 配置中打开) .不幸的是,它只能在源代码中完成,所以我建议你向他们的问题跟踪器报告这个问题。

直到(如果)他们修复它,我可以建议将所有通用代码移动到自定义扩展方法作为解决方法,例如

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处理bug,跟配置无关

问题是他们试图使用 MemberInfo 实例比较来过滤投影。

第一种情况(带表达式)对于值类型失败,因为尝试从 Expression<Func<T, object>> 中提取 MemberInfo 的实现只期望 MemberExpression,但在值类型的情况下它被包装在 Expression.Convert 里面。

第二种情况(使用 属性 个名字)失败了,因为他们没有考虑到 MemberInfo 对于 属性 从基础 class编译时 提取的lambda 表达式与通过反射或运行时创建的表达式检索到的lambda 表达式不同,以下测试证明了这一点:

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

您肯定需要报告这两个问题。我想说的是,在提供表达式列表时,任何值类型 属性 以及提供名称时任何继承 属性 的功能都会受到影响。