Search code examples
automapperprojection

AutoMapper QueryableExtensions throws NullReference when denormalizing a child object


Are there other alternatives using AutoMapper Queryable extensions to avoid null reference exception when attempting to map from a child object?

Background

When using AutoMapper Queryable extensions to project onto CustomerViewModel, mapping the FullAddress property fails with a null reference exception. I opened an issue with AutoMapper team https://github.com/AutoMapper/AutoMapper/issues/351 with a test harness to reproduce the issue. Test named can_map_AsQuerable_with_projection_this_FAILS is the failing test.

The desire is to continue using AutoMapper and the Queryable Extensions because the code is expressive and easy to read; however, calculating the FullAddress throws Null Reference Exception. I know it is the FullAddress mapping that causes the issue because if I change it to Ignore(), then the mapping succeeds. Of course, the test still fails, because I am checking to make sure the FullAddress has a value.

I came up with some alternatives, but they don't use the AutoMapper mappings. Each of these approaches are outlined in the following test cases.

**can_map_AsQuerable_with_expression**
**can_map_AsQuerable_with_custom_mapping**  

Test Fixture is below.

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(); 
        }
    }
}

Solution

  • I found a working solution that uses AutoMapper and Queryable Extensions. The problem is with using String.Format in the projection. The solution is to add all necessary properties (Street, City, and State) to CustomViewModel, then add a property (FullAddress) to do the calculation in the 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);
            }
        }
    }
    

    The updated mappings looks like this. Notice the FullAddress is ignored because this is a calculated field containing reference 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())
            ;
    

    And, with these modifications, this test now passed.

        [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));
        }