【问题标题】:AutoMapper: Direct Mapping to Sub-Object Destination Fields Not Working as ExpectedAutoMapper:直接映射到子对象目标字段未按预期工作
【发布时间】:2019-08-16 03:25:38
【问题描述】:

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

公平地说,我不确定问题出在 AutoMapper 上。可能问题出在 EntityFramework Core 上。但是,从表面上看,这似乎是 AutoMapper 的问题。

出于对知识产权的担忧,一旦发现问题,我无法分享代码。因此,我编写了一个工作示例,它尽可能接近原始代码,并且表现出相同的问题。可以在https://github.com/BurikkuDeibu/BrickApi 找到它。 master 分支有我认为应该的代码。 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 有问题?

【问题讨论】:

  • FromSqlNonComposedQuerySqlGenerator 表示查询正在使用客户端评估,因此ProjectTo(AM 生成的Select)没有太大影响。避免FromSql,尤其是non composable SQL
  • @IvanStoev 那么,请帮助我理解......为什么将源字段包装在方法调用中会导致代码开始工作?我认为,如果问题确实出在客户端评估上,那么添加更多代码......我认为这将在客户端进行评估......不会解决问题。但是,确实如此。为什么?
  • 客户端评估与服务器评估完全不同。例如,没有对象,因此 SQL 中没有空引用异常。但是当表达式在本地求值时,它们当然会在真实对象上求值,因此适用空对象规则。我并不是说这是你的错,EF Core 客户端 eval 不稳定(有错误)并且会导致很多问题,因此避免它是实用的方法。 AM ProjectTo(相对于Map)的主要思想是生成服务器可评估查询。
  • @IvanStoev 谢谢,虽然我不完全理解发生了什么,但你的解释让我比我问这个问题时有了更多的理解。虽然不是一个真正的解决方案,但如果您想发布一个答案来重新说明您已经说过的话,我会接受它。此外,如果您可以删除您的“糟糕...”评论,那将清理线程。
  • 不客气,很高兴能帮上忙 :) 虽然我不太愿意撰写答案,但如果您愿意,请随时发布自己的答案。

标签: c# entity-framework-core automapper


【解决方案1】:

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

...

基于上面的cmets,我尝试了一些东西。

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

我还尝试使用视图而不是存储过程。这也行得通,但它让我们的 DBA 很不高兴。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-06-19
    • 1970-01-01
    • 1970-01-01
    • 2019-12-25
    • 1970-01-01
    • 1970-01-01
    • 2017-02-18
    • 1970-01-01
    相关资源
    最近更新 更多