使用子项处理空值构建表达式
Handling null values building expressions with child items
我们正在使用表达式来构建一个精炼的 select 语句(由 GraphQL .Net 使用),它只查询所要求的属性。
我们解析原始查询并使用递归方法构建一个表达式,用于生成我们的 select(通过 entity framework)
我们的代码很大程度上基于@Svyatoslav Danyliv 提供的 答案。
我们的表达式构建器 class 看起来像这样:
public class ExpressionBuilder
{
public Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>(string members)
{
return BuildSelector<TSource, TTarget>(members.Split(',').Select(m => m.Trim()));
}
public Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>(IEnumerable<string> members)
{
var parameter = Expression.Parameter(typeof(TSource), "e");
var body = NewObject(typeof(TTarget), parameter, members.Select(m => m.Split('.')));
return Expression.Lambda<Func<TSource, TTarget>>(body, parameter);
}
private Expression NewObject(Type targetType, Expression source, IEnumerable<string[]> memberPaths, int depth = 0)
{
var bindings = new List<MemberBinding>();
var target = Expression.Constant(null, targetType);
foreach (var memberGroup in memberPaths.GroupBy(path => path[depth]))
{
var memberName = memberGroup.Key;
var targetMember = Expression.PropertyOrField(target, memberName);
var sourceMember = Expression.PropertyOrField(source, memberName);
var childMembers = memberGroup.Where(path => depth + 1 < path.Length).ToList();
Expression targetValue = null;
if (!childMembers.Any())
{
targetValue = sourceMember;
}
else
{
if (IsEnumerableType(targetMember.Type, out var sourceElementType) &&
IsEnumerableType(targetMember.Type, out var targetElementType))
{
var sourceElementParam = Expression.Parameter(sourceElementType, "e");
targetValue = NewObject(targetElementType, sourceElementParam, childMembers, depth + 1);
targetValue = Expression.Call(typeof(Enumerable), nameof(Enumerable.Select),
new[] { sourceElementType, targetElementType }, sourceMember,
Expression.Lambda(targetValue, sourceElementParam));
targetValue = CorrectEnumerableResult(targetValue, targetElementType, targetMember.Type);
}
else
{
targetValue = NewObject(targetMember.Type, sourceMember, childMembers, depth + 1);
}
}
bindings.Add(Expression.Bind(targetMember.Member, targetValue));
}
return Expression.MemberInit(Expression.New(targetType), bindings);
}
private bool IsEnumerableType(Type type, out Type elementType)
{
foreach (var intf in type.GetInterfaces())
{
if (intf.IsGenericType && intf.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
elementType = intf.GetGenericArguments()[0];
return true;
}
}
elementType = null;
return false;
}
private bool IsSameCollectionType(Type type, Type genericType, Type elementType)
{
var result = genericType.MakeGenericType(elementType).IsAssignableFrom(type);
return result;
}
private Expression CorrectEnumerableResult(Expression enumerable, Type elementType, Type memberType)
{
if (memberType == enumerable.Type)
return enumerable;
if (memberType.IsArray)
return Expression.Call(typeof(Enumerable), nameof(Enumerable.ToArray), new[] { elementType }, enumerable);
if (IsSameCollectionType(memberType, typeof(List<>), elementType)
|| IsSameCollectionType(memberType, typeof(ICollection<>), elementType)
|| IsSameCollectionType(memberType, typeof(IReadOnlyList<>), elementType)
|| IsSameCollectionType(memberType, typeof(IReadOnlyCollection<>), elementType))
return Expression.Call(typeof(Enumerable), nameof(Enumerable.ToList), new[] { elementType }, enumerable);
throw new NotImplementedException($"Not implemented transformation for type '{memberType.Name}'");
}
}
用法如下:
var builder = new ExpressionBuilder();
var statement = builder.BuildSelector<Payslip, Payslip>(selectStatement);
上面的 select 语句看起来像 "id,enrollment.id"
我们正在 selecting parentobject.id
和 parentobject.enrollment.id
此语句变量然后在 EntityQueryable.Select(statement)
中用于过滤数据库集合,我们基本上完成了查询的 'where' 方面。
如果正常工作,生成的 sql 只是请求的字段,而且效果很好。但是在这种情况下,它试图访问空值的 .Id 属性 并且不断失败。
当前失败,因为未设置 parentObject 的“注册”属性,因此为空。
我们得到以下错误
System.InvalidOperationException: Nullable object must have a value.
at System.Nullable`1.get_Value()
at lambda_method2198(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
我尝试实现一个 try catch 表达式,但一直失败,并以完全相同的错误结束。此时,我们没有访问数据库,所以我们不知道该值是否为空(如果它不为空则工作正常)。
我绝不是表达式方面的专家,我真的很难找到我应该在哪里包装这个值,甚至如何。理想情况下,我只想 return null,但最坏的情况是可以在客户端检测到默认的 enrollment
对象并可能处理正常。
** 更新!! **
读到这里,我认为这是 this 问题的结果。解决方法包括在表达式构建器中构建类似于此空检查的内容。
.Select(r => new Something()
{
X1 = r.X1,
X2 = r.X2,
X = new X()
{
Name= r.X.Name
},
XX = r.XXNullableId == null ? null : new XX()
{
XNumber = r.XX.XNumber
}
})
我创建了一个非常简单的 EF Core 应用程序 here,它创建了一个简单的三 table 数据库并重现了问题。
我相信上面提到的解决方法会奏效,只需弄清楚如何将其添加到表达式构建器即可。
所以我通过特殊外壳 属性 解决了这个问题。
由于我们模型的性质,这种情况只会发生在少数几个地方,为它们添加一些变通方法更容易,让表达式构建器对每个 属性 做一些奇怪的空检查。
我创建了一个简单的属性,我将其放在 'Object' 属性 上并让它指向 属性 我需要检查是否为 null。
看起来像这样:
[System.AttributeUsage(System.AttributeTargets.Property, Inherited = true, AllowMultiple = true)]
public class ExpressionBuilderNullCheckForPropertyAttribute : Attribute
{
public ExpressionBuilderNullCheckForPropertyAttribute(string propertyName)
{
this.PropertyName = propertyName;
}
public string PropertyName { get; set; }
}
现在,我的 'Person' Class(来自上面链接的示例存储库)现在看起来像这样。
public class Person
{
[Required]
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public Nullable<Guid> VetId { get; set; }
[ExpressionBuilderNullCheckForProperty(nameof(VetId))]
public Vet Vet { get; set; }
public List<Pet> Pets { get; set; }
}
然后我在 ExpressionBuilder
class 中添加了一个检查以检查该属性,如果它存在,查找它提到的 属性,然后执行空检查。如果 属性 为空,则 return 该类型的默认对象。在这种情况下,它是空的。
if (targetMember.Member.GetCustomAttributesData().Any(x => x.AttributeType == typeof(ExpressionBuilderNullCheckForPropertyAttribute)))
{
var att = (ExpressionBuilderNullCheckForPropertyAttribute)targetMember.Member.GetCustomAttributes(true).First(x => x.GetType() == typeof(ExpressionBuilderNullCheckForPropertyAttribute));
var propertyToCheck = att.PropertyName;
var valueToCheck = Expression.PropertyOrField(source, propertyToCheck);
var nullCheck = Expression.Equal(valueToCheck, Expression.Constant(null, targetMember.Type));
var checkForNull = Expression.Condition(nullCheck, Expression.Default(targetMember.Type), targetValue);
bindings.Add(Expression.Bind(targetMember.Member, checkForNull));
}
else
{
bindings.Add(Expression.Bind(targetMember.Member, targetValue));
}
如果有人想看到修复工作,我已经更新了示例存储库。
我们正在使用表达式来构建一个精炼的 select 语句(由 GraphQL .Net 使用),它只查询所要求的属性。
我们解析原始查询并使用递归方法构建一个表达式,用于生成我们的 select(通过 entity framework)
我们的代码很大程度上基于@Svyatoslav Danyliv 提供的
我们的表达式构建器 class 看起来像这样:
public class ExpressionBuilder
{
public Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>(string members)
{
return BuildSelector<TSource, TTarget>(members.Split(',').Select(m => m.Trim()));
}
public Expression<Func<TSource, TTarget>> BuildSelector<TSource, TTarget>(IEnumerable<string> members)
{
var parameter = Expression.Parameter(typeof(TSource), "e");
var body = NewObject(typeof(TTarget), parameter, members.Select(m => m.Split('.')));
return Expression.Lambda<Func<TSource, TTarget>>(body, parameter);
}
private Expression NewObject(Type targetType, Expression source, IEnumerable<string[]> memberPaths, int depth = 0)
{
var bindings = new List<MemberBinding>();
var target = Expression.Constant(null, targetType);
foreach (var memberGroup in memberPaths.GroupBy(path => path[depth]))
{
var memberName = memberGroup.Key;
var targetMember = Expression.PropertyOrField(target, memberName);
var sourceMember = Expression.PropertyOrField(source, memberName);
var childMembers = memberGroup.Where(path => depth + 1 < path.Length).ToList();
Expression targetValue = null;
if (!childMembers.Any())
{
targetValue = sourceMember;
}
else
{
if (IsEnumerableType(targetMember.Type, out var sourceElementType) &&
IsEnumerableType(targetMember.Type, out var targetElementType))
{
var sourceElementParam = Expression.Parameter(sourceElementType, "e");
targetValue = NewObject(targetElementType, sourceElementParam, childMembers, depth + 1);
targetValue = Expression.Call(typeof(Enumerable), nameof(Enumerable.Select),
new[] { sourceElementType, targetElementType }, sourceMember,
Expression.Lambda(targetValue, sourceElementParam));
targetValue = CorrectEnumerableResult(targetValue, targetElementType, targetMember.Type);
}
else
{
targetValue = NewObject(targetMember.Type, sourceMember, childMembers, depth + 1);
}
}
bindings.Add(Expression.Bind(targetMember.Member, targetValue));
}
return Expression.MemberInit(Expression.New(targetType), bindings);
}
private bool IsEnumerableType(Type type, out Type elementType)
{
foreach (var intf in type.GetInterfaces())
{
if (intf.IsGenericType && intf.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
elementType = intf.GetGenericArguments()[0];
return true;
}
}
elementType = null;
return false;
}
private bool IsSameCollectionType(Type type, Type genericType, Type elementType)
{
var result = genericType.MakeGenericType(elementType).IsAssignableFrom(type);
return result;
}
private Expression CorrectEnumerableResult(Expression enumerable, Type elementType, Type memberType)
{
if (memberType == enumerable.Type)
return enumerable;
if (memberType.IsArray)
return Expression.Call(typeof(Enumerable), nameof(Enumerable.ToArray), new[] { elementType }, enumerable);
if (IsSameCollectionType(memberType, typeof(List<>), elementType)
|| IsSameCollectionType(memberType, typeof(ICollection<>), elementType)
|| IsSameCollectionType(memberType, typeof(IReadOnlyList<>), elementType)
|| IsSameCollectionType(memberType, typeof(IReadOnlyCollection<>), elementType))
return Expression.Call(typeof(Enumerable), nameof(Enumerable.ToList), new[] { elementType }, enumerable);
throw new NotImplementedException($"Not implemented transformation for type '{memberType.Name}'");
}
}
用法如下:
var builder = new ExpressionBuilder();
var statement = builder.BuildSelector<Payslip, Payslip>(selectStatement);
上面的 select 语句看起来像 "id,enrollment.id"
我们正在 selecting parentobject.id
和 parentobject.enrollment.id
此语句变量然后在 EntityQueryable.Select(statement)
中用于过滤数据库集合,我们基本上完成了查询的 'where' 方面。
如果正常工作,生成的 sql 只是请求的字段,而且效果很好。但是在这种情况下,它试图访问空值的 .Id 属性 并且不断失败。
当前失败,因为未设置 parentObject 的“注册”属性,因此为空。
我们得到以下错误
System.InvalidOperationException: Nullable object must have a value.
at System.Nullable`1.get_Value()
at lambda_method2198(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
我尝试实现一个 try catch 表达式,但一直失败,并以完全相同的错误结束。此时,我们没有访问数据库,所以我们不知道该值是否为空(如果它不为空则工作正常)。
我绝不是表达式方面的专家,我真的很难找到我应该在哪里包装这个值,甚至如何。理想情况下,我只想 return null,但最坏的情况是可以在客户端检测到默认的 enrollment
对象并可能处理正常。
** 更新!! ** 读到这里,我认为这是 this 问题的结果。解决方法包括在表达式构建器中构建类似于此空检查的内容。
.Select(r => new Something()
{
X1 = r.X1,
X2 = r.X2,
X = new X()
{
Name= r.X.Name
},
XX = r.XXNullableId == null ? null : new XX()
{
XNumber = r.XX.XNumber
}
})
我创建了一个非常简单的 EF Core 应用程序 here,它创建了一个简单的三 table 数据库并重现了问题。
我相信上面提到的解决方法会奏效,只需弄清楚如何将其添加到表达式构建器即可。
所以我通过特殊外壳 属性 解决了这个问题。
由于我们模型的性质,这种情况只会发生在少数几个地方,为它们添加一些变通方法更容易,让表达式构建器对每个 属性 做一些奇怪的空检查。
我创建了一个简单的属性,我将其放在 'Object' 属性 上并让它指向 属性 我需要检查是否为 null。
看起来像这样:
[System.AttributeUsage(System.AttributeTargets.Property, Inherited = true, AllowMultiple = true)]
public class ExpressionBuilderNullCheckForPropertyAttribute : Attribute
{
public ExpressionBuilderNullCheckForPropertyAttribute(string propertyName)
{
this.PropertyName = propertyName;
}
public string PropertyName { get; set; }
}
现在,我的 'Person' Class(来自上面链接的示例存储库)现在看起来像这样。
public class Person
{
[Required]
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public Nullable<Guid> VetId { get; set; }
[ExpressionBuilderNullCheckForProperty(nameof(VetId))]
public Vet Vet { get; set; }
public List<Pet> Pets { get; set; }
}
然后我在 ExpressionBuilder
class 中添加了一个检查以检查该属性,如果它存在,查找它提到的 属性,然后执行空检查。如果 属性 为空,则 return 该类型的默认对象。在这种情况下,它是空的。
if (targetMember.Member.GetCustomAttributesData().Any(x => x.AttributeType == typeof(ExpressionBuilderNullCheckForPropertyAttribute)))
{
var att = (ExpressionBuilderNullCheckForPropertyAttribute)targetMember.Member.GetCustomAttributes(true).First(x => x.GetType() == typeof(ExpressionBuilderNullCheckForPropertyAttribute));
var propertyToCheck = att.PropertyName;
var valueToCheck = Expression.PropertyOrField(source, propertyToCheck);
var nullCheck = Expression.Equal(valueToCheck, Expression.Constant(null, targetMember.Type));
var checkForNull = Expression.Condition(nullCheck, Expression.Default(targetMember.Type), targetValue);
bindings.Add(Expression.Bind(targetMember.Member, checkForNull));
}
else
{
bindings.Add(Expression.Bind(targetMember.Member, targetValue));
}
如果有人想看到修复工作,我已经更新了示例存储库。