AutoMapper QueryableExtensions在对子对象进行非规范化时抛出NullReference

时间:2013-06-29 02:38:17

标签: automapper projection

在尝试从子对象进行映射时,还有其他使用AutoMapper可查询扩展的替代方法来避免空引用异常吗?

背景

使用AutoMapper Queryable扩展项投影到CustomerViewModel时,映射FullAddress属性失败,并引用空引用异常。我向AutoMapper团队https://github.com/AutoMapper/AutoMapper/issues/351提出了一个问题,其中包含一个测试工具来重现该问题。名为 can_map_AsQuerable_with_projection_this_FAILS 的测试是失败的测试。

希望继续使用AutoMapper和Queryable Extensions,因为代码具有表现力且易于阅读;但是,计算FullAddress会抛出Null Reference Exception。我知道这是导致问题的FullAddress映射,因为如果我将其更改为Ignore(),则映射成功。当然,测试仍然失败,因为我正在检查以确保FullAddress具有值。

我想出了一些替代方案,但他们没有使用AutoMapper映射。以下测试用例概述了这些方法中的每一种。

**can_map_AsQuerable_with_expression**
**can_map_AsQuerable_with_custom_mapping**  

测试夹具如下。

namespace Test.AutoMapper
{
    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Address Address { get; set; }
    }

    public class Address
    {
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
    }

    public class CustomerViewModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public string FullAddress { get; set; }
    }


    [TestFixture]
    public class AutoMapperQueryableExtensionsThrowsNullReferenceExceptionSpec
    {
        protected List<Customer> Customers { get; set; }

        [SetUp]
        public void Setup()
        {
            Mapper.CreateMap<Customer, CustomerViewModel>()
                  .ForMember(x => x.FullAddress,
                             o => o.MapFrom(s => String.Format("{0}, {1} {2}", 
                                                        s.Address.Street, 
                                                        s.Address.City, 
                                                        s.Address.State))); 

            Mapper.AssertConfigurationIsValid();

            Customers = new List<Customer>()
                {
                    new Customer() {
                            FirstName = "Mickey", LastName = "Mouse", 
                            Address = new Address() { Street = "My Street", City = "My City", State = "my state" }
                        }, 

                        new Customer() {
                            FirstName = "Donald", LastName = "Duck", 
                            Address = new Address() { Street = "My Street", City = "My City", State = "my state" }
                        }
                };
        }

        [Test]
        public void can_map_single()
        {
            var vm = Mapper.Map<CustomerViewModel>(Customers[0]);
            Assert.IsNotNullOrEmpty(vm.FullAddress);
        }

        [Test]
        public void can_map_multiple()
        {
            var customerVms = Mapper.Map<List<CustomerViewModel>>(Customers);
            customerVms.ForEach(x => Assert.IsNotNullOrEmpty(x.FullAddress));
        }

        /// <summary>
        /// This does NOT work, throws NullReferenceException.
        /// </summary>
        /// <remarks>
        /// System.NullReferenceException : Object reference not set to an instance of an object.
        /// at AutoMapper.MappingEngine.CreateMapExpression(Type typeIn, Type typeOut)
        /// at AutoMapper.MappingEngine.CreateMapExpression(Type typeIn, Type typeOut)
        /// at AutoMapper.MappingEngine.<CreateMapExpression>b__9<TSource,TDestination>(TypePair tp)
        /// at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
        /// at AutoMapper.MappingEngine.CreateMapExpression()
        /// at AutoMapper.QueryableExtensions.ProjectionExpression`1.To()
        /// </remarks>
        [Test]
        public void can_map_AsQuerable_with_projection_this_FAILS()
        {
            var customerVms = Customers.AsQueryable().Project().To<CustomerViewModel>().ToList();
            customerVms.ForEach(x => Assert.IsNotNullOrEmpty(x.FullAddress));
        }

        [Test]
        public void can_map_AsQuerable_with_expression()
        {
            var customerVms = Customers.AsQueryable().Select(ToVM.ToCustomerViewModelExpression()).ToList();
            customerVms.ForEach(x => Assert.IsNotNullOrEmpty(x.FullAddress));
        }

        [Test]
        public void can_map_AsQuerable_with_custom_mapping()
        {
            var customerVms = Customers.AsQueryable().Select(ToVM.ToCustomerViewModel).ToList();
            customerVms.ForEach(x => Assert.IsNotNullOrEmpty(x.FullAddress));
        }
    }

    public static class ToVM
    {
        public static CustomerViewModel ToCustomerViewModel(this Customer source)
        {
            return new CustomerViewModel()
                {
                    FirstName = source.FirstName, 
                    LastName = source.LastName,
                    FullAddress = String.Format("{0}, {1} {2}",
                                                        source.Address.Street,
                                                        source.Address.City,
                                                        source.Address.State)
                }; 
        }

        public static Expression<Func<Customer, CustomerViewModel>> ToCustomerViewModelExpression()
        {
            return source => source.ToCustomerViewModel(); 
        }
    }
}

3 个答案:

答案 0 :(得分:1)

我找到了一个使用AutoMapper和Queryable Extensions的工作解决方案。问题是在投影中使用String.Format。解决方案是将所有必需的属性(Street,City和State)添加到CustomViewModel,然后添加一个属性(FullAddress)以在CustomerViewModel中进行计算。

public class CustomerViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string Street { get; set; }
    public string City { get; set; }
    public string State { get; set; }

    public string FullAddress
    {
        get
        {
            return String.Format("{0}, {1} {2}",
                          Street,
                          City,
                          State);
        }
    }
}

更新的映射看起来像这样。请注意,将忽略FullAddress,因为这是一个包含引用toe String.Format。

的计算字段
        Mapper.CreateMap<Customer, CustomerViewModel>()
            .ForMember(x => x.FirstName, o => o.MapFrom(s => s.FirstName))
            .ForMember(x => x.LastName, o => o.MapFrom(s => s.LastName))
            .ForMember(x => x.Street, o => o.MapFrom(s => s.Address.Street))
            .ForMember(x => x.City, o => o.MapFrom(s => s.Address.City))
            .ForMember(x => x.State, o => o.MapFrom(s => s.Address.State))
            .ForMember(x => x.FullAddress, o => o.Ignore())
        ;

而且,通过这些修改,此测试现已通过。

    [Test]
    public void can_map_AsQuerable_with_projection_this_FAILS()
    {
        var customerVms = Customers.AsQueryable().Project().To<CustomerViewModel>().ToList();
        customerVms.ForEach(x => Assert.IsNotNullOrEmpty(x.FullAddress));
    }

答案 1 :(得分:1)

我面临同样的问题 - 我以同样的方式解决了它。

我找到了“Canonical Functions”,LINQ提供程序应该实现的一组函数(http://msdn.microsoft.com/en-us/library/bb399302.aspx)。它们映射到CLR函数。 例如。如果你使用String.Concate(街道,城市,州)而不是string.Format(...)它应该工作...但不幸的是它不是都没有!

我希望Automapper团队能够很快解决这个问题。

答案 2 :(得分:1)

仅供参考,对于未来从谷歌这样做的人来说:AutoMapper团队最近有fixed this。此外,从AutoMapper 3.3.0开始,QueryableExtensions将通过内部调用ToString()自动处理目标是字符串的简单情况 - 请参阅release notes

我有一些与OP相似的问题(虽然情况比较简单),而v3.3.0中的修复工作效果很好。