我如何教 Automapper 将 X 映射到 IContainer<Y>?

How do I teach Automapper to map X to IContainer<Y>?

这是从模型到 mvvmcross 视图模型的映射,您可以在其中使用一些容器 类,例如:

namespace MvvmCross.Binding
{
  interface INC<T>
  {
    T Value { get; set; }
  }
}
class model
{
  String name { get; set; }
  DateTime when { get; set; }
  othermodel data { get; set; }
}
class viewmodel
{
  INC<String> Name { get; set; }
  INC<String> When { get; set; }
  INC<otherviewmodel> Data { get; set; }
}

感谢 Lucians 在评论中的帮助。

这些映射器适用于 Mapper.Map<INC<String>>("test"),但不适用于 Mapper.Map<String, INC<String>>(null, new NC<String>("I shouldnt be here")),因为 AutoMapper 不会发送空源值。

Mapper.Initialize(c =>
{
    c.Mappers.Insert(0, new DestinationObjectContainerMapper());
    c.Mappers.Insert(0, new SourceObjectContainerMapper());
});
Mapper.AssertConfigurationIsValid();

public class DestinationObjectContainerMapper : BlackBoxObjectMapper
{
    bool InterfaceMatch(Type x) => x.IsGenericType && typeof(INC<>).IsAssignableFrom(x.GetGenericTypeDefinition());
    Type CType(Type cdt) => cdt.GetInterfaces().Concat(new[] { cdt }).Where(InterfaceMatch).Select(x => x.GenericTypeArguments[0]).FirstOrDefault();
    public override bool IsMatch(TypePair context) => CType(context.DestinationType) != null;

    public override object Map(object source, Type sourceType, object destination, Type destinationType, ResolutionContext context)
    {
        var dcType = CType(destinationType);

        // Create a container if destination is null
        if (destination == null)
            destination = Activator.CreateInstance(typeof(NC<>).MakeGenericType(dcType));

        // This may also fail because we need the source type
        var setter = typeof(INC<>).MakeGenericType(dcType).GetProperty("Value").GetSetMethod();
        var mappedSource = context.Mapper.Map(source, sourceType, dcType);

        // set the value
        setter.Invoke(destination, new[] { mappedSource });
        return destination;
    }
}
public class SourceObjectContainerMapper : BlackBoxObjectMapper
{
    bool InterfaceMatch(Type x) => x.IsGenericType && typeof(INC<>).IsAssignableFrom(x.GetGenericTypeDefinition());
    Type CType(Type cdt) => cdt.GetInterfaces().Concat(new[] { cdt }).Where(InterfaceMatch).Select(x => x.GenericTypeArguments[0]).FirstOrDefault();
    public override bool IsMatch(TypePair context) => CType(context.SourceType) != null;

    public override object Map(object source, Type sourceType, object destination, Type destinationType, ResolutionContext context)
    {
        // this is only obtainable if destination is not null - so this will not work.
        var scType = CType(sourceType);

        // destination could also be null, this is another anavoidable throw
        object sourceContainedValue = null;
        if (source != null)
        {
            var getter = typeof(INC<>).MakeGenericType(scType).GetProperty("Value").GetGetMethod();
            sourceContainedValue = getter.Invoke(source, new Object[0]);
        }

        // map and return
        return context.Mapper.Map(sourceContainedValue, scType, destinationType);
    }
}

这使用了稍微扩展的 ObjectMapper:

public abstract class BlackBoxObjectMapper : IObjectMapper
{
    private static readonly MethodInfo MapMethod = typeof(BlackBoxObjectMapper).GetTypeInfo().GetDeclaredMethod("Map");

    public abstract bool IsMatch(TypePair context);

    public abstract object Map(object source, Type sourceType, object destination, Type destinationType, ResolutionContext context);

    public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap,
        PropertyMap propertyMap, Expression sourceExpression, Expression destExpression,
        Expression contextExpression) =>
        Expression.Call(
            Expression.Constant(this),
            MapMethod,
            sourceExpression,
            Expression.Constant(sourceExpression.Type),
            destExpression,
            Expression.Constant(destExpression.Type),
            contextExpression);
}

感谢 Lucians 在评论中的帮助。这个使用表达式的映射器可以工作,并且将在 this patchIObjectMapperInfo.CanMapNullSource = true.

的帮助下根据需要接收空值进行映射

AM 团队花了很大力气避免将计算为 null 的表达式传递给 Mappers,而且它确实简化了 AutoMapper.Mappers 命名空间,所以这个补丁显然是有争议的。我想过一些侵入性较小的方法来发出信号,比如 NullSafeExpression,或者像 interface ICanHandleNullSourceExpressions {} 这样的匿名接口名字来试图阻止流量,但我还没有找到任何看起来更好的方法。

public class ContainerDestinationMapper : BaseContainerMapper, IObjectMapperInfo
{

    readonly Func<Type, Expression> createContainer;

    public bool CanMapNullSource => true;

    public ContainerDestinationMapper(Type GenericContainerTypeDefinition, String ContainerPropertyName, Func<Type, Expression> createContainer)
        : base(GenericContainerTypeDefinition, ContainerPropertyName)
    {
        this.createContainer = createContainer;
    }
    public bool IsMatch(TypePair context) => ContainerDef(context.DestinationType) != null;

    public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap,
        PropertyMap propertyMap, Expression sourceExpression, Expression destExpression,
        Expression contextExpression)
    {

        var dparam = DigParameter(destExpression);
        var dVal = Expression.Property(dparam, dparam.Type.GetTypeInfo().GetDeclaredProperty("Value"));
        var cdt = ContainerDef(destExpression.Type);
        var tp = new TypePair(sourceExpression.Type, cdt);
        var ret = Expression.Block
                    (
                        // make destination not null
                        Expression.IfThen
                        (
                            Expression.Equal(dparam, Expression.Constant(null)),
                            Expression.Assign(dparam, createContainer(cdt))
                        ),
                        // Assign to the destination
                        Expression.Assign
                        (
                            dVal,
                            ExpressionBuilder.MapExpression // what you get if you map source to dest.Value
                            (
                                configurationProvider,
                                profileMap,
                                tp,
                                sourceExpression,
                                contextExpression,
                                null,
                                dVal,
                                true
                            )
                        ),
                        destExpression
                    // But we need to return the destination type!
                    // Sadly it will go on to assign destExpression to destExpression.
                    );
        return ret;
    }
    public TypePair GetAssociatedTypes(TypePair initialTypes)
    {
        return new TypePair(initialTypes.SourceType, ContainerDef(initialTypes.DestinationType));
    }
}

public class ContainerSourceMapper : BaseContainerMapper, IObjectMapperInfo
{
    public bool CanMapNullSource => true;

    public ContainerSourceMapper(Type GenericContainerTypeDefinition, String ContainerPropertyName)
        : base(GenericContainerTypeDefinition, ContainerPropertyName) { }

    public bool IsMatch(TypePair context) => ContainerDef(context.SourceType) != null;

    public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap,
        PropertyMap propertyMap, Expression sourceExpression, Expression destExpression,
        Expression contextExpression)
    {
        var dstParam = DigParameter(destExpression);
        return Expression.Block(
            Expression.IfThenElse
            (
                Expression.Equal(sourceExpression, Expression.Constant(null)),
                Expression.Assign(dstParam, Expression.Default(destExpression.Type)),
                Expression.Assign(dstParam,
                    ExpressionBuilder.MapExpression(configurationProvider, profileMap,
                        new TypePair(ContainerDef(sourceExpression.Type), destExpression.Type),
                        Expression.Property(sourceExpression, sourceExpression.Type.GetTypeInfo().GetDeclaredProperty(containerPropertyName)),
                        contextExpression,
                        propertyMap,
                        destExpression
                    )
                )
            ),
            dstParam
        );
    }

    public TypePair GetAssociatedTypes(TypePair initialTypes)
    {
        return new TypePair(ContainerDef(initialTypes.SourceType), initialTypes.DestinationType);
    }
}
public class BaseContainerMapper
{
    protected readonly Type genericContainerTypeDefinition;
    protected readonly String containerPropertyName;
    public BaseContainerMapper(Type GenericContainerTypeDefinition, String ContainerPropertyName)
    {
        genericContainerTypeDefinition = GenericContainerTypeDefinition;
        containerPropertyName = ContainerPropertyName;
    }
    protected ParameterExpression DigParameter(Expression e)
    {
        if (e is ParameterExpression pe) return pe;
        if (e is UnaryExpression ue) return DigParameter(ue.Operand);
        throw new ArgumentException("Couldn't find parameter");
    }
    public static Type ContainerDef(Type gen, Type to)
    {
        return new[] { to }.Concat(to.GetInterfaces())
                            .Where(x => x.IsGenericType)
                            .Where(x => gen.IsAssignableFrom(x.GetGenericTypeDefinition()))
                            .Select(x => x.GenericTypeArguments.Single())
                            .FirstOrDefault(); // Hopefully not overloaded!

    }
    protected Type ContainerDef(Type to)
    {
        return ContainerDef(genericContainerTypeDefinition, to);
    }
    protected PropertyInfo Of(Expression expr)
    {
        return expr.Type.GetTypeInfo().GetDeclaredProperty(containerPropertyName);
    }
}

This patch 允许在 CreateMap 中使用 typeof(void) 来表达不包含在类型中的单个参数(即它将匹配任何类型)并构造 ITypeConverter 就这样。

未打补丁的 AutoMapper 的替代方法是在以下转换器中用 object 替换 XY。我已经在这会失败的行中添加了注释。即使忽略这些失败,这样的尝试也需要反思,因此更复杂,性能更差。

Mapper.Initialise(cfg =>
{
    cfg.CreateMap(typeof(void), typeof(IContainer<>)).ConvertUsing(typeof(TCf<,>));
    cfg.CreateMap(typeof(IContainer<>), typeof(void)).ConvertUsing(typeof(TCb<,>));
});

class TCf<X, Y> : ITypeConverter<X, IContainer<Y>>
{
    public IContainer<Y> Convert(X source, IContainer<Y> destination, ResolutionContext context)
    {
        if (destination == null)
        {
            // if Y was object we could not create the correct container type
            destination = new Container<Y>(); 
            destination.Configure();
        }
        // if Y was object and source was null, we could not map to the correct type
        destination.Value = context.Mapper.Map<Y>(source);
        return destination;
    }
}

class TCb<X, Y> : ITypeConverter<IContainer<X>, Y>
{
    public Y Convert(IContainer<X> source, Y destination, ResolutionContext context)
    {
        // if X was object and source was null, we could not choose an appropriate default
        var use = source == null ? GetSomeDefault<X>() : source.Value;
        // if Y was object and destination was null, we could not map to the correct type
        return context.Mapper.Map<Y>(use);
    }
}

通过使用 ForAllMaps,您可以获得 source/destination 类型并提供封闭的、完全通用的转换器类型。如果您想直接调用 -Map<X,IContainer<Y>,这没有帮助,但您不需要这样做。

Mapper.Initialize(c =>
{
    c.CreateMap<model, viewmodel>().ReverseMap();
    c.ForAllMaps((p, mc) =>
    {
        Type st = p.SourceType, sct = GetContained(st);
        Type dt = p.DestinationType, dct = GetContained(dt);
        if (sct != null) mc.ConvertUsing(typeof(TCReverse<,>).MakeGenericType(sct, dt));
        if (dct != null) mc.ConvertUsing(typeof(TCForward<,>).MakeGenericType(st, dct));
    });
});
Mapper.AssertConfigurationIsValid();
Mapper.Map<viewmodel>(new model());
Mapper.Map<model>(new viewmodel());

使用简单的转换器:

public class TCReverse<X,Y> : ITypeConverter<IContainer<X>, Y>
{
    public Y Convert(IContainer<X> source, Y destination, ResolutionContext context)
    {
        var use = source == null ? default(X) : source.Value;
        return context.Mapper.Map(use, destination);
    }
}

public class TCForward<X,Y> : ITypeConverter<X, IContainer<Y>>
{
    public IContainer<Y> Convert(X source, IContainer<Y> destination, ResolutionContext context)
    {
        if (destination == null)
            destination = new Container<Y>();
        destination.Value = context.Mapper.Map(source, destination.Value);
        return destination;
    }
}

我在这里使用了一个辅助方法:

Type GetContained(Type t)
{
    return t.GetInterfaces()
            .Concat(new[] { t })
            .Where(x => x.IsGenericType)
            .Where(x => typeof(IContainer<>).IsAssignableFrom(x.GetGenericTypeDefinition()))
            .Select(x => x.GenericTypeArguments[0])
            .FirstOrDefault();
}