AutoMapper 自定义值解析器
尽管 AutoMapper
覆盖了许多目标成员映射场景,但仍有大约 1%
到 5%
的目标值需要在解析时提供一些帮助。很多时候,这种自定义值解析逻辑是直接应用于我们领域模型的领域逻辑。然而,如果这段逻辑仅与映射操作相关,那么在源类型上添加这样的行为就会造成不必要的冗余。在这种情况下,AutoMapper
允许为目的地成员配置自定义值解析器。例如,我们可能希望在映射过程中有一个计算值:
public class Source
{
public int Value1 { get; set; }
public int Value2 { get; set; }
}
public class Destination
{
public int Total { get; set; }
}
无论出于何种原因,我们都希望 Total
成为源 Value
属性的和。由于某些其他原因,我们不能或不应该将此逻辑放在 Source
类型上。要提供自定义值解析器,我们首先需要创建一个实现 IValueResolver
接口的类型:
public interface IValueResolver<in TSource, in TDestination, TDestMember>
{
TDestMember Resolve(TSource source, TDestination destination, TDestMember destMember, ResolutionContext context);
}
ResolutionContext
包含了当前解析操作的所有上下文信息,如源类型、目标类型、源值等。一个示例实现如下:
public class CustomResolver : IValueResolver<Source, Destination, int>
{
public int Resolve(Source source, Destination destination, int member, ResolutionContext context)
{
return source.Value1 + source.Value2;
}
}
一旦我们有了 IValueResolver
的实现,我们就需要告诉 AutoMapper
在解析特定目标成员时使用这个自定义值解析器。我们有几种方式来指定使用哪个自定义值解析器,包括:
MapFrom<TValueResolver>
MapFrom(typeof(CustomValueResolver))
MapFrom(aValueResolverInstance)
在下面的例子中,我们将通过泛型告诉 AutoMapper
自定义解析器的类型:
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Total, opt => opt.MapFrom<CustomResolver>())
);
configuration.AssertConfigurationIsValid();
var source = new Source
{
Value1 = 5,
Value2 = 7
};
var result = mapper.Map<Source, Destination>(source);
result.Total.ShouldEqual(12);
尽管目标成员(Total)没有匹配的源成员,但指定自定义解析器使配置有效,因为解析器现在负责为目标成员提供值。
如果我们不在意值解析器中的源/目标类型,或者想要在多个映射中重用它们,我们可以直接使用 object
作为源/目标类型:
public class MultBy2Resolver : IValueResolver<object, object, int> {
public int Resolve(object source, object dest, int destMember, ResolutionContext context) {
return destMember * 2;
}
}
自定义构造函数方法
因为我们只向 AutoMapper
提供了自定义解析器的类型,所以映射引擎会使用反射来创建值解析器的实例。
如果我们不希望 AutoMapper
使用反射来创建实例,我们可以直接提供它:
var configuration = new MapperConfiguration(cfg => cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Total,
opt => opt.MapFrom(new CustomResolver())
));
AutoMapper
将使用那个特定的对象,这在解析器可能有构造函数参数或需要由 IoC
容器构建的场景中非常有用。
解析的值被映射到目标属性
请注意,您从解析器返回的值并不是简单地赋值给目标属性。任何适用的映射都将被执行,并且该映射的结果将成为最终的目标属性值。请查看 执行计划 。
自定义提供给解析器的源值
默认情况下,AutoMapper
将源对象传递给解析器。这限制了解析器的可重用性,因为解析器与源类型耦合。但是,如果我们跨多个类型提供通用解析器,我们可以配置 AutoMapper
以重定向提供给解析器的源值,并使用不同的解析器接口,以便我们的解析器可以使用源/目标成员:
var configuration = new MapperConfiguration(cfg => {
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Total,
opt => opt.MapFrom<CustomResolver, decimal>(src => src.SubTotal));
cfg.CreateMap<OtherSource, OtherDest>()
.ForMember(dest => dest.OtherTotal,
opt => opt.MapFrom<CustomResolver, decimal>(src => src.OtherSubTotal));
});
public class CustomResolver : IMemberValueResolver<object, object, decimal, decimal> {
public decimal Resolve(object source, object destination, decimal sourceMember, decimal destinationMember, ResolutionContext context) {
// 这里编写逻辑
}
}
向 Mapper 传递键值对
在调用映射时,您可以通过使用键值对并使用自定义解析器从上下文中获取对象来传递额外的对象。
mapper.Map<Source, Dest>(src, opt => opt.Items["Foo"] = "Bar");
这是为此自定义解析器设置映射的方式
cfg.CreateMap<Source, Dest>()
.ForMember(dest => dest.Foo, opt => opt.MapFrom((src, dest, destMember, context) => context.Items["Foo"]));
提示
从版本 13.0 开始,您可以使用 context.State
代替,方式类似。注意,每个 Map
调用中 State
和 Items
是互斥的 。
ForPath
类似于 ForMember,从 6.1.0 开始有了 ForPath。可以查看 测试用例 了解示例。
解析器和条件
对于每个属性映射,AutoMapper
尝试在评估条件之前解析目标值。因此,即使条件会阻止所得到的值被使用,它也需要能够在不抛出异常的情况下做到这一点。
例如,以下是使用 BuildExecutionPlan(使用 ReadableExpressions 显示)生成的针对单个属性的示例输出:
try
{
var resolvedValue =
{
try
{
return // ... 在这里尝试解析目标值
}
catch (NullReferenceException)
{
return null;
}
catch (ArgumentNullException)
{
return null;
}
};
if (condition.Invoke(src, typeMapDestination, resolvedValue))
{
typeMapDestination.WorkStatus = resolvedValue;
}
}
catch (Exception ex)
{
throw new AutoMapperMappingException(
"Error mapping types.",
ex,
AutoMapper.TypePair,
AutoMapper.TypeMap,
AutoMapper.PropertyMap);
};
默认情况下,如果没有为成员自定义映射,解析属性生成的代码通常不会有任何问题。但如果您使用自定义代码映射该属性,且在条件不满足时会崩溃,则映射将会失败,尽管存在条件。
此示例代码将会失败:
public class SourceClass
{
public string Value { get; set; }
}
public class TargetClass
{
public int ValueLength { get; set; }
}
// ...
var source = new SourceClass { Value = null };
var target = new TargetClass();
CreateMap<SourceClass, TargetClass>()
.ForMember(dest => dest.ValueLength, options => options.MapFrom(src => src.Value.Length))
.ForAllMembers(options => options.Condition((sourceObj, destinationObj, value) => value != null));
该条件阻止了 Value
属性映射到目标对象上,但在那之前,自定义成员映射将会因为调用了 Value.Length
而失败,此时 Value
为 null
。
为了避免这种情况,可以使用 前置条件 或者确保自定义成员映射代码在任何条件下都能安全完成:
.ForMember(dest => dest.ValueLength, options => options.MapFrom(src => src != null ? src.Value.Length : 0))