首页 > 解决方案 > AutoMapper:直接映射到子对象目标字段未按预期工作

问题描述

我看到 AutoMapper 的一些奇怪行为,当目标字段位于子对象中时,我无法直接映射源字段和目标字段。相反,我需要将源字段包装在方法调用中,该方法调用检查该字段是否为空。如果不为null,则返回该值,否则返回null。不得不这样做似乎是不对的。特别是因为映射到根对象上的目标字段不需要这种黑客攻击。

公平地说,我不确定问题出在 AutoMapper 上。可能问题出在 EntityFramework Core 上。但是,从表面上看,它看起来像是 AutoMapper 的问题。

由于对知识产权的担忧,如果发现问题,我无法分享代码。因此,我编写了一个工作示例,它尽可能接近原始代码,并且表现出相同的问题。它可以在https://github.com/BurikkuDeibu/BrickApi找到。主分支有我认为应该的代码。UseMagicMethods分支的代码包含使事情正常运行所需的 hack UseMagicMethods分支中真正感兴趣的文件是https://github.com/BurikkuDeibu/BrickApi/blob/UseMagicMethods/src/WebApi/Models/ElementDetailsMapper.cs

从主分支(抛出异常):

    public class ElementDetailsMapper
    {
        public class ElementDetailsProfile : Profile
        {
            public ElementDetailsProfile()
            {
                CreateMap<ElementDetailEntity, RGBDetail>()
                    .ForMember(dest => dest.R, opts => opts.MapFrom(src => src.Red))
                    .ForMember(dest => dest.G, opts => opts.MapFrom(src => src.Green))
                    .ForMember(dest => dest.B, opts => opts.MapFrom(src => src.Blue));

                CreateMap<ElementDetailEntity, ColorDetail>()
                    .ForMember(dest => dest.RGB, opts => opts.MapFrom(src => src))
                    .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.ColorId))
                    .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Color))
                    .ForMember(dest => dest.IsTranparent, opts => opts.MapFrom(src => src.Transparent))
                    .ForMember(dest => dest.IsMetaliic, opts => opts.MapFrom(src => src.Metallic));

                CreateMap<ElementDetailEntity, DesignDetail>()
                    .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.DesignId))
                    .ForMember(dest => dest.Name, opts => opts.MapFrom(src => src.Design));

                CreateMap<ElementDetailEntity, ElementDetails>()
                    .ForMember(dest => dest.Color, opts => opts.MapFrom(src => src))
                    .ForMember(dest => dest.Design, opts => opts.MapFrom(src => src))
                    .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
                    .ForMember(dest => dest.ManufactureStartDate, opts => opts.MapFrom(src => src.ManufactureStartDate))
                    .ForMember(dest => dest.ManufactureEndDate, opts => opts.MapFrom(src => src.ManufactureEndDate));
            }
        }
    }

从 UseMagicMethods 分支(作品):

        public class ElementDetailsProfile : Profile
        {
            public ElementDetailsProfile()
            {
                CreateMap<ElementDetailEntity, RGBDetail>()
                    .ForMember(dest => dest.R, opts => opts.MapFrom(src => ByteMagic(src.Red)))
                    .ForMember(dest => dest.G, opts => opts.MapFrom(src => ByteMagic(src.Green)))
                    .ForMember(dest => dest.B, opts => opts.MapFrom(src => ByteMagic(src.Blue)));

                CreateMap<ElementDetailEntity, ColorDetail>()
                    .ForMember(dest => dest.RGB, opts => opts.MapFrom(src => src))
                    .ForMember(dest => dest.Id, opts => opts.MapFrom(src => ShortMagic(src.ColorId)))
                    .ForMember(dest => dest.Name, opts => opts.MapFrom(src => StringMagic(src.Color)))
                    .ForMember(dest => dest.IsTranparent, opts => opts.MapFrom(src => BooleanMagic(src.Transparent)))
                    .ForMember(dest => dest.IsMetaliic, opts => opts.MapFrom(src => BooleanMagic(src.Metallic)));

                CreateMap<ElementDetailEntity, DesignDetail>()
                    .ForMember(dest => dest.Id, opts => opts.MapFrom(src => StringMagic(src.DesignId)))
                    .ForMember(dest => dest.Name, opts => opts.MapFrom(src => StringMagic(src.Design)));

                CreateMap<ElementDetailEntity, ElementDetails>()
                    .ForMember(dest => dest.Color, opts => opts.MapFrom(src => src))
                    .ForMember(dest => dest.Design, opts => opts.MapFrom(src => src))
                    .ForMember(dest => dest.Id, opts => opts.MapFrom(src => src.Id))
                    .ForMember(dest => dest.ManufactureStartDate, opts => opts.MapFrom(src => src.ManufactureStartDate))
                    .ForMember(dest => dest.ManufactureEndDate, opts => opts.MapFrom(src => src.ManufactureEndDate));
            }

            public static bool? BooleanMagic(bool? input)
            {
                return input.HasValue ? input.Value : (bool?)null;
            }

            public static byte? ByteMagic(byte? input)
            {
                return input.HasValue ? input.Value : (byte?)null;
            }

            public static short? ShortMagic(short? input)
            {
                return input.HasValue ? input.Value : (short?)null;
            }

            public static string StringMagic(string input)
            {
                return input ?? null;
            }
        }
    }

您会注意到,在UseMagicMethods分支的代码文件中,我将每个映射到目标子对象字段的源字段包装在方法调用中。目标和源字段的数据类型完全匹配,所以我想我可以直接映射它们。但是,如果我尝试使用以下堆栈跟踪得到 NullReference 异常:

   at Microsoft.EntityFrameworkCore.Storage.TypedRelationalValueBufferFactoryFactory.CacheKey.<>c.<GetHashCode>b__6_0(Int32 t, TypeMaterializationInfo v)
   at System.Linq.Enumerable.Aggregate[TSource,TAccumulate](IEnumerable`1 source, TAccumulate seed, Func`3 func)
   at Microsoft.EntityFrameworkCore.Storage.TypedRelationalValueBufferFactoryFactory.CacheKey.GetHashCode()
   at System.Collections.Generic.ObjectEqualityComparer`1.GetHashCode(T obj)
   at System.Collections.Concurrent.ConcurrentDictionary`2.TryGetValue(TKey key, TValue& value)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Storage.TypedRelationalValueBufferFactoryFactory.Create(IReadOnlyList`1 types)
   at Microsoft.EntityFrameworkCore.Query.Sql.Internal.FromSqlNonComposedQuerySqlGenerator.CreateValueBufferFactory(IRelationalValueBufferFactoryFactory relationalValueBufferFactoryFactory, DbDataReader dataReader)
   at Microsoft.EntityFrameworkCore.Query.Internal.ShaperCommandContext.<NotifyReaderCreated>b__14_0(FactoryAndReader s)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Query.Internal.ShaperCommandContext.NotifyReaderCreated(DbDataReader dataReader)
   at Microsoft.EntityFrameworkCore.Query.Internal.AsyncQueryingEnumerable`1.AsyncEnumerator.<BufferlessMoveNext>d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.<ExecuteAsync>d__7`2.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.EntityFrameworkCore.Query.Internal.AsyncQueryingEnumerable`1.AsyncEnumerator.<MoveNext>d__11.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.EntityFrameworkCore.Query.Internal.AsyncLinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.<MoveNext>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Linq.AsyncEnumerable.<Aggregate_>d__6`3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at WebApi.Controllers.ElementController.<Get>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at lambda_method(Closure , Object )
   at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()
   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.AwaitableObjectResultExecutor.<Execute>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextActionFilterAsync>d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeInnerFilterAsync>d__13.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeNextResourceFilter>d__23.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeFilterPipelineAsync>d__18.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeAsync>d__16.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.<Invoke>d__7.MoveNext()

您会在堆栈跟踪中注意到对 EntityFramework Core 的各种引用,这就是为什么我想知道问题是否真的存在。

那么,我做错了什么,还是 AutoMapper 或 EntityFramework Core 有问题?

标签: c#entity-framework-coreautomapper

解决方案


简短的回答是,因为我使用的是ProjectTo方法,所以在翻译查询方面,我依赖于 EntityFramework Core 支持的内容。在这一点上,它不支持我试图做的事情。

...

根据上面的评论,我尝试了一些事情。

我尝试从ProjectTo切换到Map,这似乎工作得很好。这是我们决定采用的方法,以便我们可以消除魔法方法。

我还尝试使用视图而不是存储过程。这也有效,但它让我们的 DBA 非常不高兴。

我还尝试对存储过程结果使用查询类型而不是实体类型。这没什么区别。如果没有神奇的方法,我仍然会得到 NullReference 异常。


推荐阅读