使用子项处理空值构建表达式

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.idparentobject.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));
}

如果有人想看到修复工作,我已经更新了示例存储库。