可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
This came up mapping from models to mvvmcross viewmodels, where you have some container classes you might use, for example:
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; }
}
I need to teach AutoMapper to map from A
to INC<B>
(and back), without specifiying A or B.
Null destination INC<>
should be created, non-null should not be re-created.
Mapping should continue such that destination.Value = Mapper.Map<A,B>(source)
.
Mapping null --> INC<T>
should result in INC<T>.Value = SomeDefaultValue
回答1:
Thanks for Lucians help in the comments.
These mappers will work for Mapper.Map<INC<String>>("test")
, but not for Mapper.Map<String, INC<String>>(null, new NC<String>("I shouldnt be here"))
, since AutoMapper won't send a null source value.
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);
}
}
This uses a slightly extended 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);
}
回答2:
Thanks for Lucians help in the comments. This mapper using expressions works, and will receive null values to map as required with a bit of help from this patch and IObjectMapperInfo.CanMapNullSource = true
.
The AM team spent a lot of effort avoiding passing expressions that evaluate to null to Mappers, and it definitely simplifies the AutoMapper.Mappers namespace, so this patch is obviously controversial. I thought about less intrusive ways to signal this like a NullSafeExpression
, or an anonymous interface moniker like interface ICanHandleNullSourceExpressions {}
to try to stem the flow, but I didn't find anything that looks better yet.
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);
}
}
回答3:
This patch allows typeof(void)
to be used in CreateMap
to express a single parameter that is not contained within a type (i.e. it will match any type) and construct the ITypeConverter
as such.
The alternative for an unpatched AutoMapper is to substitute X
and Y
with object
in the below converters. I have added comments to the lines where this would fail. Even ignoring these failings, such an attempt would require reflection and therefore be more complex and less performant.
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);
}
}
回答4:
By using ForAllMaps
, you can get the source/destination types and provide a closed, fully generic converter type. This doesn't help if you want to directly call -Map<X,IContainer<Y>
, but you shouldn't need to.
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());
With the simple converters:
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;
}
}
And I used a helper method here:
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();
}