在尝试从子对象进行映射时,还有其他使用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();
}
}
}
答案 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中的修复工作效果很好。