如何在Linq-To-SQL查询中为可导航对象添加动态生成的where表达式? 对其进行测试

如何解决如何在Linq-To-SQL查询中为可导航对象添加动态生成的where表达式? 对其进行测试

背景

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

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

我和我的公司以前从未尝试做过这样的事情,因此要由我的团队提出可行的解决方案。这是经过一周左右的研究工作而得出的结果。

有效方法:第1部分

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

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

问题/解决我的问题

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

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.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)作为我正在读取的实体。

解决方法

如果您可以要求客户端为属性提供完整的点符号表达式。例如"Organization.City";

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

如果我收到您的问题说明,则希望能够向上导航属性链。

如果确实如此,那么真正的挑战就是从EF获取导航关系。这就是EntityTypeExtensions派上用场的地方。尤其是GetNavigations()

您可以在浏览时递归地浏览导航属性并构建属性访问器表达式:

private static IEnumerable<Tuple<IProperty,Expression>> GetPropertyAccessors(this IEntityType model,Expression param)
        {
            var result = new List<Tuple<IProperty,Expression>>();

            result.AddRange(model.GetProperties()
                                        .Where(p => !p.IsShadowProperty()) // this is your chance to ensure property is actually declared on the type before you attempt building Expression
                                        .Select(p => new Tuple<IProperty,Expression>(p,Expression.Property(param,p.Name)))); // Tuple is a bit clunky but hopefully conveys the idea
            
            foreach (var nav in model.GetNavigations().Where(p => p is Navigation))
            {
                var parentAccessor = Expression.Property(param,nav.Name); // define a starting point so following properties would hang off there
                result.AddRange(GetPropertyAccessors(nav.ForeignKey.PrincipalEntityType,parentAccessor)); //recursively call ourselves to travel up the navigation hierarchy
            }

            return result;
        }

然后可以简化您的BuildExpression方法。注意,我添加了DbContext作为参数:

        public static IQueryable<T> BuildExpression<T>(this IQueryable<T> source,DbContext context,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
            {
                var model = context.Model.FindEntityType(typeof(T)); // start with our own entity
                var props = model.GetPropertyAccessors(param); // get all available field names including navigations
                var reference = props.FirstOrDefault(p => RelationalPropertyExtensions.GetColumnName(p.Item1) == columnName); // find the filtered column - you might need to handle cases where column does not exist

                dataField = reference.Item2 as MemberExpression; // we happen to already have correct property accessors in our Tuples
            }
            catch (ArgumentException)
            {
                throw new NotImplementedException("I think you shouldn't be getting these anymore");
            }

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

GetClassroomsAsync看起来像这样:

public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(IEnumerable<QueryableFilter> queryableFilters = null)
{
    IQueryable<ClassroomEntity> classroomQuery = ClassroomEntity.All().AsNoTracking();
    
    // Loop through the supplied queryable filters (if any) to construct a dynamic LINQ-to-SQL queryable
    foreach (var filter in queryableFilters ?? new List<QueryableFilter>())
    {
        try
        {
            classroomQuery = classroomQuery.BuildExpression(_context,filter.Name,filter.Value,filter.Compare);
        }
        catch (ArgumentException ex)
        {
            // you probably should look at catching different exceptions now as joining is not required
        }
    }

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

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

对其进行测试

由于您没有提供实体层次结构,因此我尝试了自己的一种:

public class Entity
{
    public int Id { get; set; }
}
class Company: Entity
{
    public string CompanyName { get; set; }
}

class Team: Entity
{
    public string TeamName { get; set; }
    public Company Company { get; set; }
}

class Employee: Entity
{
    public string EmployeeName { get; set; }
    public Team Team { get; set; }
}
// then i've got a test harness method as GetClassroomsAsync won't compile wothout your entities
class DynamicFilters<T> where T : Entity
{
    private readonly DbContext _context;

    public DynamicFilters(DbContext context)
    {
        _context = context;
    }

    public IEnumerable<T> Filter(IEnumerable<QueryableFilter> queryableFilters = null)
    {
        IQueryable<T> mainQuery = _context.Set<T>().AsQueryable().AsNoTracking();
        // Loop through the supplied queryable filters (if any) to construct a dynamic LINQ-to-SQL queryable
        foreach (var filter in queryableFilters ?? new List<QueryableFilter>())
        {
            mainQuery = mainQuery.BuildExpression(_context,filter.Compare);
        }

        mainQuery = mainQuery.OrderBy(x => x.Id);

        return  mainQuery.ToList();
    }
}
// --- DbContext
class MyDbContext : DbContext
{
    public DbSet<Company> Companies{ get; set; }
    public DbSet<Team> Teams { get; set; }
    public DbSet<Employee> Employees { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=.\\SQLEXPRESS;Database=test;Trusted_Connection=true");
        base.OnConfiguring(optionsBuilder);
    }
}
// ---
static void Main(string[] args)
{
    var context = new MyDbContext();
    var someTableData = new DynamicFilters<Employee>(context).Filter(new 
    List<QueryableFilter> {new QueryableFilter {Name = "CompanyName",Value = "Microsoft" }});
}

有了上述内容,过滤器CompanyName = "Microsoft" EF Core 3.1为我生成了以下SQL:

SELECT [e].[Id],[e].[EmployeeName],[e].[TeamId]
FROM [Employees] AS [e]
LEFT JOIN [Teams] AS [t] ON [e].[TeamId] = [t].[Id]
LEFT JOIN [Companies] AS [c] ON [t].[CompanyId] = [c].[Id]
WHERE [c].[CompanyName] = N'Microsoft'
ORDER BY [e].[Id]

这种方法似乎产生了预期的结果,但是有一个问题:列名称在所有实体中必须唯一。可能可以解决这个问题,但是由于我不了解您的数据模型的具体细节,因此我将其交给您。

,

(免责声明:我已经编写了与此类似的代码,但是我实际上尚未测试此答案中的代码。)

您的BuildExpression接受一个查询(以IQueryable<T>的形式)并返回另一个查询。当您实际上想将其中的某些过滤器应用于参数的属性x.ClassroomId时,这会限制将所有过滤器应用于参数的属性x.Organization.City。 / p>

我建议使用一种GetFilterExpression方法,该方法会根据一些任意的基本表达式生成过滤器表达式:

private static Expression GetFilterExpression(Expression baseExpr,QueryableFilterCompareEnum? compare = QueryableFilterCompareEnum.Equal) {
    MemberExpression dataField;
    try {
        dataField = Expression.Property(baseExpr,columnName);
    } catch (ArgumentException ex) {
        if (ex.ParamName == "propertyName") {
            throw new ArgumentException($"Base expression type does not have a \"{propertyName}\" field.",ex.ParamName);
        } else {
            throw new ArgumentException(ex.Message);
        }
    }

    if (!string.IsNullOrWhiteSpace(value)) {
        value = value.Trim();
    }
    ConstantExpression constant = Expression.Constant(value,typeof(string));

    BinaryExpression binary = GetBinaryExpression(dataField,compare);
    return binary;
}

GetClassroomsAsync内,您可以通过传递不同的表达式来针对原始ClassroomEntity参数或根据参数上Organization属性的返回值来构建过滤器表达式:

public async Task<IEnumerable<IExportClassroom>> GetClassroomsAsync(IEnumerable<QueryableFilter> queryableFilters = null) {
    var filters = queryableFilters?.ToList();
    var param = Expression.Parameter(typeof(ClassroomEntity));
    var orgExpr = Expression.Property(param,"Organization"); // equivalent of x.Organization

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

    if (filters is {}) {
        // Map the filters to expressions,applied to the `x` or to the `x.Organization` as appropriate
        var filterExpressions = filters.Select(filter => {
            try {
                return GetFilterExpression(param,filter.Compare);
            } catch (ArgumentException ex) {
                if (ex.ParamName == "propertyName") {
                    return GetFilterExpression(orgExpr,filter.Compare);
                } else {
                    throw new ArgumentException(ex.Message);
                }
            }
        });

        // LogicalCombined is shown later in the answer
        query = query.Where(
            Expression.Lambda<Func<ClassroomEntity,bool>>(LogicalCombined(filters))
        );
    }

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

LogicalCombined接受多个返回bool的表达式,并将它们组合为一个表达式:

private static Expression LogicalCombined(IEnumerable<Expression> exprs,ExpressionType expressionType = ExpressionType.AndAlso) {
    // ensure the expression type is a boolean operator
    switch (expressionType) {
        case ExpressionType.And:
        case ExpressionType.AndAlso:
        case ExpressionType.Or:
        case ExpressionType.OrElse:
        case ExpressionType.ExclusiveOr:
            break;
        default:
            throw new ArgumentException("Invalid expression type for logically combining expressions.");
    }
    Expression? final = null;
    foreach (var expr in exprs) {
        if (final is null) {
            final = expr;
            continue;
        }
        final = Expression.MakeBinary(expressionType,final,expr);
    }
    return final;
}

一些建议:

在我撰写本文时,GetFilterExpression是一种static方法。由于所有参数(基本表达式除外)都来自QueryableFilter,因此您可以考虑使其成为QueryableFilter的实例方法。


我还建议更改GetBinaryExpression以使用字典将QueryableFilterCompareEnum映射到内置ExpressionType。然后,GetBinaryExpression的实现只是内置Expression.MakeBinary方法的包装:

private static Dictionary<QueryableFilterCompareEnum,ExpressionType> comparisonMapping = new  Dictionary<QueryableFilterCompareEnum,ExpressionType> {
    [QueryableFilterCompareEnum.NotEqual] = ExpressionType.NotEqual,[QueryableFilterCompareEnum.GreaterThan] = ExpressionType.GreaterThan,[QueryableFilterCompareEnum.GreaterThanOrEqual] = ExpressionType.GreaterThanOrEqual,[QueryableFilterCompareEnum.LessThan] = ExpressionType.LessThan,[QueryableFilterCompareEnum.LessThanOrEqual] = ExpressionType.LessThanOrEqual,[QueryableFilterCompareEnum.Equal] = ExpressionType.Equal
}

private static Expression GetBinaryExpression(MemberExpression member,ConstantExpression constant,QueryableFilterCompareEnum? comparisonOperation) {
    comparisonOperation = comparisonOperation ?? QueryableFilterCompareEnum.Equal;
    var expressionType = comparisonMapping[comparisonOperation];
    return Expression.MakeBinary(
        expressionType,member,constant
    );
}

GetFilterExpressionGetClassroomsAsync都通过构造成员访问表达式来处理ClassroomEntityOrganizationEntity上都不存在指定属性的可能性。处理引发的异常。

使用反射来测试属性是否存在于任何一种类型上可能更清楚。

更多,您可以考虑使用所有有效的字段名称存储静态HashSet<string>,并进行检查。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-