在相同属性上将一个对象映射到另一个对象的表达式
Expression to mapping one object to another on same-properties
我正在尝试通过以下代码使用 Expression
创建一个简单的映射器:
public static class MyUtility {
public static Action<TSource, TTarget> BuildMapAction<TSource, TTarget>(IEnumerable<PropertyMap> properties) {
var sourceInstance = Expression.Parameter(typeof(TSource), "source");
var targetInstance = Expression.Parameter(typeof(TTarget), "target");
var statements = BuildPropertyGettersSetters(sourceInstance, targetInstance, properties);
Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);
if (blockExp.CanReduce)
blockExp = blockExp.ReduceAndCheck();
blockExp = blockExp.ReduceExtensions();
var lambda = Expression.Lambda<Action<TSource, TTarget>>(blockExp, sourceInstance, targetInstance);
return lambda.Compile();
}
private static IEnumerable<Expression> BuildPropertyGettersSetters(
ParameterExpression sourceInstance,
ParameterExpression targetInstance,
IEnumerable<PropertyMap> properties) {
var statements = new List<Expression>();
foreach (var property in properties) {
// value-getter
var sourceGetterCall = Expression.Call(sourceInstance, property.SourceProperty.GetGetMethod());
var sourcePropExp = Expression.TypeAs(sourceGetterCall, typeof(object));
// value-setter
var targetSetterCall =
Expression.Call(
targetInstance,
property.TargetProperty.GetSetMethod(),
Expression.Convert(sourceGetterCall, property.TargetProperty.PropertyType)
);
var refNotNullExp = Expression.ReferenceNotEqual(sourceInstance, Expression.Constant(null));
var propNotNullExp = Expression.ReferenceNotEqual(sourcePropExp, Expression.Constant(null));
var notNullExp = Expression.And(refNotNullExp, propNotNullExp);
var ifExp = Expression.IfThen(notNullExp, targetSetterCall);
statements.Add(ifExp);
}
return statements;
}
}
我觉得一切正常,但是当我尝试对其进行测试时,我只是遇到空引用异常。测试对象及方法:
public class UserEntity {
public string Name { get; set; }
public string Family { get; set; }
public int Age { get; set; }
public string Nickname { get; set; }
}
public class UserModel {
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Nickname { get; set; }
}
public static class CallTest {
public static void Call() {
var entity = new UserEntity {
Name="Javad",
Family="Amiry",
Age = 25,
Nickname = "my nickname is here",
};
var model = new UserModel();
var map1 = new PropertyMap {
SourceProperty = entity.GetType().GetProperty("Age"),
TargetProperty = model.GetType().GetProperty("Age"),
};
var map2 = new PropertyMap {
SourceProperty = entity.GetType().GetProperty("Nickname"),
TargetProperty = model.GetType().GetProperty("Nickname"),
};
var action = MyUtility.BuildMapAction<UserEntity, UserModel>(new[] {map1, map2});
action(entity, model); // here I get the error System.NullReferenceException: 'Object reference not set to an instance of an object.'
}
}
你知道那里发生了什么吗?我错过了什么?
注意:我不能使用第三方映射器(如 AutoMapper)
问题是由这一行引起的:
Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);
使用的 Expression.Block
重载的第一个参数表示块的 局部变量 。通过在那里传递 lambda 参数,您只需定义 2 个本地未分配变量,因此在执行时定义 NRE。您可以通过检查 VS locals/watch window 中的 lambda 表达式 DebugView
看到这一点,在您的示例调用中它看起来像这样:
.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.Block(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.If (
$source != null & .Call $source.get_Age() .As System.Object != null
) {
.Call $target.set_Age((System.Int32).Call $source.get_Age())
} .Else {
.Default(System.Void)
};
.If (
$source != null & .Call $source.get_Nickname() .As System.Object != null
) {
.Call $target.set_Nickname((System.String).Call $source.get_Nickname())
} .Else {
.Default(System.Void)
}
}
}
注意块内source
和target
的重新定义。
使用正确的重载后:
Expression blockExp = Expression.Block(statements);
现在的视图是这样的:
.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.Block() {
.If (
$source != null & .Call $source.get_Age() .As System.Object != null
) {
.Call $target.set_Age((System.Int32).Call $source.get_Age())
} .Else {
.Default(System.Void)
};
.If (
$source != null & .Call $source.get_Nickname() .As System.Object != null
) {
.Call $target.set_Nickname((System.String).Call $source.get_Nickname())
} .Else {
.Default(System.Void)
}
}
}
NRE 消失了。
那是关于原始问题的。但是生成的代码看起来很丑陋而且不是最理想的。源对象空检查可以围绕整个块进行,类型转换和值空检查可以只在需要时进行。作为奖励,我会这样写:
public static Action<TSource, TTarget> BuildMapAction<TSource, TTarget>(IEnumerable<PropertyMap> properties)
{
var source = Expression.Parameter(typeof(TSource), "source");
var target = Expression.Parameter(typeof(TTarget), "target");
var statements = new List<Expression>();
foreach (var propertyInfo in properties)
{
var sourceProperty = Expression.Property(source, propertyInfo.SourceProperty);
var targetProperty = Expression.Property(target, propertyInfo.TargetProperty);
Expression value = sourceProperty;
if (value.Type != targetProperty.Type)
value = Expression.Convert(value, targetProperty.Type);
Expression statement = Expression.Assign(targetProperty, value);
// for class/interface or nullable type
if (!sourceProperty.Type.IsValueType || Nullable.GetUnderlyingType(sourceProperty.Type) != null)
{
var valueNotNull = Expression.NotEqual(sourceProperty, Expression.Constant(null, sourceProperty.Type));
statement = Expression.IfThen(valueNotNull, statement);
}
statements.Add(statement);
}
var body = statements.Count == 1 ? statements[0] : Expression.Block(statements);
// for class.interface type
if (!source.Type.IsValueType)
{
var sourceNotNull = Expression.NotEqual(source, Expression.Constant(null, source.Type));
body = Expression.IfThen(sourceNotNull, body);
}
// not sure about the need of this
if (body.CanReduce)
body = body.ReduceAndCheck();
body = body.ReduceExtensions();
var lambda = Expression.Lambda<Action<TSource, TTarget>>(body, source, target);
return lambda.Compile();
}
生成更像 C# 的代码:
.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.If ($source != null) {
.Block() {
$target.Age = $source.Age;
.If ($source.Nickname != null) {
$target.Nickname = $source.Nickname
} .Else {
.Default(System.Void)
}
}
} .Else {
.Default(System.Void)
}
}
我正在尝试通过以下代码使用 Expression
创建一个简单的映射器:
public static class MyUtility {
public static Action<TSource, TTarget> BuildMapAction<TSource, TTarget>(IEnumerable<PropertyMap> properties) {
var sourceInstance = Expression.Parameter(typeof(TSource), "source");
var targetInstance = Expression.Parameter(typeof(TTarget), "target");
var statements = BuildPropertyGettersSetters(sourceInstance, targetInstance, properties);
Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);
if (blockExp.CanReduce)
blockExp = blockExp.ReduceAndCheck();
blockExp = blockExp.ReduceExtensions();
var lambda = Expression.Lambda<Action<TSource, TTarget>>(blockExp, sourceInstance, targetInstance);
return lambda.Compile();
}
private static IEnumerable<Expression> BuildPropertyGettersSetters(
ParameterExpression sourceInstance,
ParameterExpression targetInstance,
IEnumerable<PropertyMap> properties) {
var statements = new List<Expression>();
foreach (var property in properties) {
// value-getter
var sourceGetterCall = Expression.Call(sourceInstance, property.SourceProperty.GetGetMethod());
var sourcePropExp = Expression.TypeAs(sourceGetterCall, typeof(object));
// value-setter
var targetSetterCall =
Expression.Call(
targetInstance,
property.TargetProperty.GetSetMethod(),
Expression.Convert(sourceGetterCall, property.TargetProperty.PropertyType)
);
var refNotNullExp = Expression.ReferenceNotEqual(sourceInstance, Expression.Constant(null));
var propNotNullExp = Expression.ReferenceNotEqual(sourcePropExp, Expression.Constant(null));
var notNullExp = Expression.And(refNotNullExp, propNotNullExp);
var ifExp = Expression.IfThen(notNullExp, targetSetterCall);
statements.Add(ifExp);
}
return statements;
}
}
我觉得一切正常,但是当我尝试对其进行测试时,我只是遇到空引用异常。测试对象及方法:
public class UserEntity {
public string Name { get; set; }
public string Family { get; set; }
public int Age { get; set; }
public string Nickname { get; set; }
}
public class UserModel {
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string Nickname { get; set; }
}
public static class CallTest {
public static void Call() {
var entity = new UserEntity {
Name="Javad",
Family="Amiry",
Age = 25,
Nickname = "my nickname is here",
};
var model = new UserModel();
var map1 = new PropertyMap {
SourceProperty = entity.GetType().GetProperty("Age"),
TargetProperty = model.GetType().GetProperty("Age"),
};
var map2 = new PropertyMap {
SourceProperty = entity.GetType().GetProperty("Nickname"),
TargetProperty = model.GetType().GetProperty("Nickname"),
};
var action = MyUtility.BuildMapAction<UserEntity, UserModel>(new[] {map1, map2});
action(entity, model); // here I get the error System.NullReferenceException: 'Object reference not set to an instance of an object.'
}
}
你知道那里发生了什么吗?我错过了什么?
注意:我不能使用第三方映射器(如 AutoMapper)
问题是由这一行引起的:
Expression blockExp = Expression.Block(new[] { sourceInstance, targetInstance }, statements);
使用的 Expression.Block
重载的第一个参数表示块的 局部变量 。通过在那里传递 lambda 参数,您只需定义 2 个本地未分配变量,因此在执行时定义 NRE。您可以通过检查 VS locals/watch window 中的 lambda 表达式 DebugView
看到这一点,在您的示例调用中它看起来像这样:
.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.Block(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.If (
$source != null & .Call $source.get_Age() .As System.Object != null
) {
.Call $target.set_Age((System.Int32).Call $source.get_Age())
} .Else {
.Default(System.Void)
};
.If (
$source != null & .Call $source.get_Nickname() .As System.Object != null
) {
.Call $target.set_Nickname((System.String).Call $source.get_Nickname())
} .Else {
.Default(System.Void)
}
}
}
注意块内source
和target
的重新定义。
使用正确的重载后:
Expression blockExp = Expression.Block(statements);
现在的视图是这样的:
.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.Block() {
.If (
$source != null & .Call $source.get_Age() .As System.Object != null
) {
.Call $target.set_Age((System.Int32).Call $source.get_Age())
} .Else {
.Default(System.Void)
};
.If (
$source != null & .Call $source.get_Nickname() .As System.Object != null
) {
.Call $target.set_Nickname((System.String).Call $source.get_Nickname())
} .Else {
.Default(System.Void)
}
}
}
NRE 消失了。
那是关于原始问题的。但是生成的代码看起来很丑陋而且不是最理想的。源对象空检查可以围绕整个块进行,类型转换和值空检查可以只在需要时进行。作为奖励,我会这样写:
public static Action<TSource, TTarget> BuildMapAction<TSource, TTarget>(IEnumerable<PropertyMap> properties)
{
var source = Expression.Parameter(typeof(TSource), "source");
var target = Expression.Parameter(typeof(TTarget), "target");
var statements = new List<Expression>();
foreach (var propertyInfo in properties)
{
var sourceProperty = Expression.Property(source, propertyInfo.SourceProperty);
var targetProperty = Expression.Property(target, propertyInfo.TargetProperty);
Expression value = sourceProperty;
if (value.Type != targetProperty.Type)
value = Expression.Convert(value, targetProperty.Type);
Expression statement = Expression.Assign(targetProperty, value);
// for class/interface or nullable type
if (!sourceProperty.Type.IsValueType || Nullable.GetUnderlyingType(sourceProperty.Type) != null)
{
var valueNotNull = Expression.NotEqual(sourceProperty, Expression.Constant(null, sourceProperty.Type));
statement = Expression.IfThen(valueNotNull, statement);
}
statements.Add(statement);
}
var body = statements.Count == 1 ? statements[0] : Expression.Block(statements);
// for class.interface type
if (!source.Type.IsValueType)
{
var sourceNotNull = Expression.NotEqual(source, Expression.Constant(null, source.Type));
body = Expression.IfThen(sourceNotNull, body);
}
// not sure about the need of this
if (body.CanReduce)
body = body.ReduceAndCheck();
body = body.ReduceExtensions();
var lambda = Expression.Lambda<Action<TSource, TTarget>>(body, source, target);
return lambda.Compile();
}
生成更像 C# 的代码:
.Lambda #Lambda1<System.Action`2[ConsoleApp3.UserEntity,ConsoleApp3.UserModel]>(
ConsoleApp3.UserEntity $source,
ConsoleApp3.UserModel $target) {
.If ($source != null) {
.Block() {
$target.Age = $source.Age;
.If ($source.Nickname != null) {
$target.Nickname = $source.Nickname
} .Else {
.Default(System.Void)
}
}
} .Else {
.Default(System.Void)
}
}