c# - 如何在 Linq-To-SQL 查询中为可导航对象添加动态生成的 Where 表达式?

标签 c# linq-to-sql entity-framework-core expression-trees dynamic-expression

背景

我的客户希望有一种方法来发送字段(字符串)、值(字符串)和比较(枚举)值的数组,以便检索其数据。

public class QueryableFilter {
    public string Name { get; set; }
    public string Value { get; set; }
    public QueryableFilterCompareEnum? Compare { get; set; }
}

我和我的公司以前从未尝试过做这样的事情,所以我的团队需要拿出一个可行的解决方案。这是经过一周左右的研究来制定解决方案的结果。

什么有效:第 1 部分

我创建了一项能够从我们的表Classroom 中检索数据的服务。数据检索是通过 LINQ-to-SQL 在 Entity Framework Core 中完成的。如果过滤器中提供的字段之一对于 Classroom 不存在,但对于其相关的 Organization 确实存在(客户想要还能够在组织地址之间进行搜索)并且具有可导航属性。

public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(
    IEnumerable<QueryableFilter> queryableFilters = null) {
    var filters = queryableFilters?.ToList();

    IQueryable<ClassroomEntity> classroomQuery = ClassroomEntity.All().AsNoTracking();

    // The organization table may have filters searched against it
    // If any are, the organization table should be inner joined to all filters are used
    IQueryable<OrganizationEntity> organizationQuery = OrganizationEntity.All().AsNoTracking();
    var joinOrganizationQuery = false;

    // Loop through the supplied queryable filters (if any) to construct a dynamic LINQ-to-SQL queryable
    if (filters?.Count > 0) {
        foreach (var filter in filters) {
            try {
                classroomQuery = classroomQuery.BuildExpression(filter.Name, filter.Value, filter.Compare);
            } catch (ArgumentException ex) {
                if (ex.ParamName == "propertyName") {
                    organizationQuery = organizationQuery.BuildExpression(filter.Name, filter.Value, filter.Compare);
                    joinOrganizationQuery = true;
                } else {
                    throw new ArgumentException(ex.Message);
                }
            }
        }
    }

    // Inner join the classroom and organization queriables (if necessary)
    var query = joinOrganizationQuery
        ? classroomQuery.Join(organizationQuery, classroom => classroom.OrgId, org => org.OrgId, (classroom, org) => classroom)
        : classroomQuery;

    query = query.OrderBy(x => x.ClassroomId);

    IEnumerable<IExportClassroom> results = await query.Select(ClassroomMapper).ToListAsync();
    return results;
}

什么有效:第 2 部分

代码中存在的BuildExpression是我自己创建的(有扩展空间)。

public static IQueryable<T> BuildExpression<T>(this IQueryable<T> source, string columnName, string value, QueryableFilterCompareEnum? compare = QueryableFilterCompareEnum.Equal) {
    var param = Expression.Parameter(typeof(T));

    // Get the field/column from the Entity that matches the supplied columnName value
    // If the field/column does not exists on the Entity, throw an exception; There is nothing more that can be done
    MemberExpression dataField;
    try {
        dataField = Expression.Property(param, propertyName);
    } catch (ArgumentException ex) {
        if (ex.ParamName == "propertyName") {
            throw new ArgumentException($"Queryable selection does not have a \"{propertyName}\" field.", ex.ParamName);
        } else {
            throw new ArgumentException(ex.Message);
        }
    }

    ConstantExpression constant = !string.IsNullOrWhiteSpace(value)
        ? Expression.Constant(value.Trim(), typeof(string))
        : Expression.Constant(value, typeof(string));

    BinaryExpression binary = GetBinaryExpression(dataField, constant, compare);
    Expression<Func<T, bool>> lambda = (Expression<Func<T, bool>>)Expression.Lambda(binary, param)
    return source.Where(lambda);
}

private static Expression GetBinaryExpression(MemberExpression member, ConstantExpression constant, QueryableFilterCompareEnum? comparisonOperation) {
    switch (comparisonOperation) {
        case QueryableFilterCompareEnum.NotEqual:
            return Expression.Equal(member, constant);
        case QueryableFilterCompareEnum.GreaterThan:
            return Expression.GreaterThan(member, constant);
        case QueryableFilterCompareEnum.GreaterThanOrEqual:
            return Expression.GreaterThanOrEqual(member, constant);
        case QueryableFilterCompareEnum.LessThan:
            return Expression.LessThan(member, constant);
        case QueryableFilterCompareEnum.LessThanOrEqual:
            return Expression.LessThanOrEqual(member, constant);
        case QueryableFilterCompareEnum.Equal:
        default:
            return Expression.Equal(member, constant);
        }
    }
}

问题/解决我的问题

虽然组织上的内部联接有效,但我不想拉入第二个实体集来检查可导航的值。如果我输入城市作为我的过滤器名称,通常我会这样做:

classroomQuery = classroomQuery.Where(x => x.Organization.City == "Atlanta");

这在这里不起作用。

为了得到我想要的东西,我尝试了几种不同的方法:

  • 一个编译函数,将返回 Func,但通过 LINQ-to-SQL 进行查询时,查询不包含它。
  • 我将其更改为 Expression>,但我的返回没有按照我尝试实现它的方式返回 bool,因此不起作用。
  • 我改变了实现导航属性的方式,但我的任何函数都无法正确读取该值。

基本上,是否有某种方法可以让我能够以 Entity Framework Core 中的 LINQ-to-SQL 工作的方式实现以下功能?也欢迎其他选择。

classroomQuery = classroomQuery.Where(x => x.Organization.BuildExpression(filter.Name, filter.Value, filter.Compare));

编辑01:

当使用不带动态生成器的表达式时,如下所示:

IQueryable<ClassroomEntity>classroomQuery = ClassroomEntity.Where(x => x.ClassroomId.HasValue).Where(x => x.Organization.City == "Atlanta").AsNoTracking();

调试内容如下:

.Call Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.AsNoTracking(.Call System.Linq.Queryable.Where(
        .Call System.Linq.Queryable.Where(
            .Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[ClassroomEntity]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[ClassroomEntity]),
            '(.Lambda #Lambda1<System.Func`2[ClassroomEntity,System.Boolean]>)),
        '(.Lambda #Lambda2<System.Func`2[ClassroomEntity,System.Boolean]>)))

.Lambda #Lambda1<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $x)
{
    ($x.ClassroomId).HasValue
}

.Lambda #Lambda2<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $x)
{
    ($x.Organization).City == "Bronx"
}

我尝试使用动态构建器来获取类教师,这给了我调试:

.Lambda #Lambda3<System.Func`2[ClassroomEntity,System.Boolean]>(ClassroomEntity $var1)
{
    $var1.LeadTeacherName == "Sharon Candelariatest"
}

仍然无法弄清楚如何获取 ($var1.Organization) 作为我正在读取的实体。

最佳答案

如果您可以要求客户提供该属性的完整点符号表达式。例如“组织.城市”;

    dataField = (MemberExpression)propertyName.split(".")
        .Aggregate(
            (Expression)param,
            (result,name) => Expression.Property(result, name));

关于c# - 如何在 Linq-To-SQL 查询中为可导航对象添加动态生成的 Where 表达式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63648020/

相关文章:

c# - C# 中的 LINQ。检查一个列表是否包含另一个列表中的元素

c# - 无法将值从 datagridview 获取到文本框

c# - 将 linq 模型转换为通用列表

c# - 如何从 SQL to Linq 中的存储过程中进行选择

c# - LINQ to SQL 语句相似吗?

asp.net-core-mvc - Entity Framework Core 1.0 DbContext 未限定为 http 请求

c# - Java Web 服务使用 .Net

c# - 在主应用程序和周期性任务之间共享数据

c# - DbContextOptionsBuilder 找不到 ASP.NET Core 1.1 Web API 项目

c# - 实体 ForEach [JsonIgnore]