下面的代码说明了这种奇怪的行为.有四个测试,其中两个未能断言未扩展基础DTO的属性.如果我将1-1..1-4行移动到2.1,则所有测试都通过.
我是否错过了一些代码或者这是Automapper中的错误,我必须向Automapper的错误跟踪器报告此问题?或者它可能是“按设计”,但为什么呢? (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,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?)崩溃有关.在下面的第一个单元测试中演示了崩溃堆栈跟踪.我会在另一个问题中询问它,或者更确切地说是在bug跟踪器中创建一个问题,因为崩溃肯定是一个bug.
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>() .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 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,"Should NOT be expanded."); Assert.IsNotNull(first.Name,first.Name); } [TestMethod] public void DerivedOnly_Success() { var projectTo = _iq.ProjectTo<DtoDerived>(null,new string[] { nameof(d.Desc) } /* _ => _.Desc */); Assert.AreEqual(1,"Should be expanded."); Assert.AreEqual(_iqf.Desc,"Should NOT be expanded."); } [TestMethod] public void SkipAll_Success() { var projectTo = _iq.ProjectTo<DtoDerived>(null,new string[] { }); Assert.AreEqual(1,"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?
我怀疑这是“按设计”,很可能是错误或不完整的快速和脏实施.可以在PropertyMap类的ApplyInheritedPropertyMap方法的source code内部看到,该方法负责组合基本属性和派生属性配置. “继承的”映射属性目前是:
> CustomExpression
> CustomResolver
>条件
>前提条件
> NullSubstitute
> MappingOrder
> ValueResolverConfig
而以下(基本上所有bool类型)属性(包括有问题的属性)不是:
> AllowNull
> UseDestinationValue
> ExplicitExpansion
IMO的问题是当前的实现无法确定是否明确设置了bool属性.当然,通过用显式bool替换auto属性可以很容易地解决它?支持字段和默认值逻辑(以及其他流畅的配置方法,以便在基类配置中打开它时将其关闭).不幸的是,它只能在源代码中完成,因此我建议您将问题报告给他们的问题跟踪器.
直到(以及如果)他们修复它,我可以建议将所有常用代码移动到自定义扩展方法,如
static class MyMappers { public static 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实例比较来过滤投影.
第一种情况(带表达式)对于值类型失败,因为尝试从Expression< Func< T,object>>中提取MemberInfo的实现我只期待MemberExpression,但是在值类型的情况下,它包含在Expression.Convert中.
第二种情况(带有属性名称)失败,因为他们没有考虑这样的事实:从编译时lambda表达式中提取的基类继承的属性的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
你肯定需要报告这两个问题.我会说,在提供表达式列表时,任何值类型属性都会泄露该功能,并且在提供名称时,任何继承的属性都会泄露.