在实体框架核心查询的Include和ThenInclude部分中使用动态linq筛选器

wkyowqbh  于 2022-12-15  发布在  其他
关注(0)|答案(1)|浏览(226)

我想要实现的目标:

目前我正在开发一个过滤器系统,它将根据用户输入对实体应用不同的过滤器。如果你只关注根实体或多对一关系,这是一项容易的任务。但是一旦你想对集合进行过滤,它就变得有点难以表达,也很难查询。

问题:

我想筛选根实体(Organization),还想筛选Organization.Contracts或Contract. Licenses等集合。我可以选择将选择添加到IncludeThenInclude子句(请参见我的示例)。但我只能添加固定的LINQ-Querys,而不能构建动态函数来选择正确的行。

例外:

System.ArgumentException: "Expression of type 'System.Func`2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]' cannot be used for parameter of type 'System.Linq.Expressions.Expression`1[System.Func`2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]]' of method 'System.Linq.IQueryable`1[ExpressionTreeTest.MinimalTest+Contract] Where[Contract](System.Linq.IQueryable`1[ExpressionTreeTest.MinimalTest+Contract], System.Linq.Expressions.Expression`1[System.Func`2[ExpressionTreeTest.MinimalTest+Contract,System.Boolean]])' Arg_ParamName_Name"

基于我看到的异常,实体框架核心想要一个系统.链接.表达式.表达式1[System.Func 2[表达式树测试.最小值测试+合同,系统.布尔值]]但是我提供了一个系统.函数'2[表达式树测试.最小值测试+合同,系统.布尔值]。只要我将其更改为**表达式〈函数〈合同,布尔值〉〉合同过滤器表达式= c =〉c.结束日期〉日期时间.现在.添加月份(11);**intellisense报告一个错误,即无法接受。

其他遮阳篷:

我发现了许多关于查询实体框架核心和构建动态查询的不同问题。基于此,我能够为根实体(组织)构建我的动态查询。但对于嵌套的ThenInclude列表查询,我找不到任何动态示例。

我为您提供的最小测试用例:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;

namespace ExpressionTreeTest
{

public class MinimalTest
{

    public void RunTest()
    {
        AppDbContext dbContext = new AppDbContext();
        dbContext.Database.EnsureDeleted();
        dbContext.Database.EnsureCreated();

        // Setup some test data to check the query results
        SetupTestData(dbContext);

        // Pre build query with entity framework, which is working as expected
        // Expectation:
        // Get all Organizations with the Name == NameOfCompany and
        // Include all Organization.Contracts which are Created > ateTime.Now.AddMonths(12)
        // ThenInclude all Contract.Licenses which are l.Articel.Name == "TestArticelName"
        IQueryable<Organization> preBuildQuery = dbContext.Organizations.Where(o => o.Name == "NameOfCompany").
            Include(c => c.Contracts.Where(c => c.Created > DateTime.Now.AddMonths(12))).
            ThenInclude(l => l.Licenses.Where(l => l.Articel.Name == "TestArticelName"));

        // This prints 1, which is the desired result
        Console.WriteLine("Query result count: " + preBuildQuery.ToList().Count());

        // This is the dynamic filter funtion for the Include-Part of the query
        // This function gets accepted by Visual Studio but throws an error by Entity Framework
        Func<Contract, bool> ContractFilterFunction = c => c.EndDate > DateTime.Now.AddMonths(11);

        // Build the above query dynamically based on user input
        IQueryable<Organization> dynamicQuery = dbContext.Organizations.Where(BuildWhereQuery()).
            Include(c => c.Contracts.Where(ContractFilterFunction)).
            ThenInclude(l => l.Licenses.Where(l => l.Articel.Name == "TestArticelName"));

        // This is the line with the error you will find in the question
        // If i remove the ContractFilterFunction and replace it with an inline lambda
        // the query gets executed, but i am not able to dynamically set the query parameters.
        Console.WriteLine("Query result count: " + dynamicQuery.ToList().Count());
    }

    /// <summary>
    /// This method creates based on input a query with different types. In the future there
    /// should be some select based on the binaryExpression to use (Equal, Greater, etc.)
    /// At the moment this is static for testing purposes
    /// </summary>
    /// <returns>A Func<T,bool> to parse to a Linq-Entity-Framewor query</returns>
    private Expression<Func<Organization, bool>> BuildWhereQuery()
    {
        ParameterExpression rootEntity = Expression.Parameter(typeof(Organization));
        MemberExpression fieldExpression = Expression.PropertyOrField(rootEntity, "Name");
        ConstantExpression valueToCompare = Expression.Constant("NameOfCompany");
        var binaryExpression = Expression.Equal(fieldExpression, valueToCompare);
        return Expression.Lambda<Func<Organization, bool>>(binaryExpression, rootEntity);
    }

    public class AppDbContext : DbContext
    {
        public DbSet<Organization> Organizations { get; set; }

        public DbSet<Contact> Contacts { get; set; }

        public DbSet<Contract> Contracts { get; set; }

        public DbSet<License> Licenses { get; set; }

        public DbSet<Articel> Articels { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(@"Data Source=mydb.db");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }
    }

    public class BasicEntity
    {
        [Key]
        public Guid Id { get; set; }

    }

    public class Organization : BasicEntity
    {
        public string Name { get; set; }

        public List<Contract> Contracts { get; set; }

        public List<Contact> Contacts { get; set; }

    }

    public class Articel : BasicEntity
    {

        public string Name { get; set; }

    }

    public class Contact : BasicEntity
    {
        public string Name { get; set; }

        public Organization Organization { get; set; }
    }

    public class Contract : BasicEntity
    {

        public DateTime Created { get; set; }

        public DateTime EndDate { get; set; }

        public List<License> Licenses { get; set; }

        public Organization Organization { get; set; }

    }

    public class License : BasicEntity
    {

        public string LicenseNumber { get; set; }

        public Articel Articel { get; set; }

        public Contract Contract { get; set; }

    }

    private static void SetupTestData(AppDbContext dbContext)
    {
        Organization org = new Organization
        {
            Name = "NameOfCompany",
        };
        dbContext.Add(org);
        dbContext.Add(new Contact
        {
            Name = "Contact 1",
            Organization = org
        });
        dbContext.Add(new Contact
        {

            Name = "Contact 2",
            Organization = org
        });
        Articel articel = new Articel
        {
            Id = Guid.NewGuid(),
            Name = "TestArticelName"
        };
        dbContext.Add(articel);
        Contract contract = new Contract
        {
            Id = Guid.NewGuid(),
            Created = DateTime.Now,
            EndDate = DateTime.Now.AddMonths(12),
            Organization = org
        };
        dbContext.Add(contract);
        License license = new License
        {
            Id = Guid.NewGuid(),
            LicenseNumber = "12345-12345",
            Articel = articel,
            Contract = contract
        };
        dbContext.Add(license);
        dbContext.SaveChanges();
    }
}
}

注:

如果你有任何额外的提示给我,甚至是一个变通或其他解决方案,我会非常高兴。这不是一个要求这样做,但这是我找到的唯一方法。

dphi5xsq

dphi5xsq1#

解决问题的最简单方法是安装LINQKit-LinqKit.Microsoft.EntityFrameworkCore
OnConfiguring中添加WithExpressionExpanding

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlite(@"Data Source=mydb.db");
    optionsBuilder.WithExpressionExpanding();
}

使动态过滤器功能为Expression,EF Core不会将Func<>转换为SQL并使用LINQKit扩展Invoke

// This is the dynamic filter function for the Include-Part of the query
Expression<Func<Contract, bool>> ContractFilterFunction = c => c.EndDate > DateTime.Now.AddMonths(11);

IQueryable<Organization> dynamicQuery = dbContext.Organizations.Where(BuildWhereQuery())
    .Include(c => c.Contracts.Where(c => ContractFilterFunction.Invoke(c)))
    .ThenInclude(l => l.Licenses.Where(l => l.Articel.Name == "TestArticelName"));


LINQKit扩展将扩展LambdaExpressionContractFilterFunction,并在EF Core的LINQ翻译器处理之前注入到最终表达式树中。

相关问题