2

我有一个简单的 EF7 代码优先模型:Countries拥有StatesStates拥有Cities。定义了关系和逆关系,以便可以双向遍历导航字段和集合。

我想对所有城市进行文本搜索:拆分搜索词以逐个搜索每个词,使此搜索针对城市名称、州名和国家名称,并使此搜索具有包容性(如果我查找 ' France Germany francisco'、来自法国的城市、来自德国的城市和旧金山将包含在搜索结果中)。

为此,我正在使用 LinqKit 构建我的搜索谓词PredicateBuilder

当我在一个简单的内存中实体集合上测试谓词构建和运行代码时,一切都按预期工作。

当我在 Entity Framework 7 上下文中针对 Cities DbSet 运行它时,我遇到了这个奇怪的异常 ( System.InvalidCastException 'System.Linq.Expressions.FieldExpression' to type 'System.Linq.Expressions.ParameterExpression')。

我最初认为它与 LinqKit 相关,但它实际上与在查找涉及 2 级关系的字段时使用 PredicateBuilder 和 EF7 相关:

// simple snippet, full code is below.
// the part that causes the exception is when accessing c.State.Country.Name
predicate = predicate.Or(c =>
    c.StateId > 0 &&
    c.State.CountryId > 0 &&
    c.State.Country.Name.Contains(word));

在谓词构建代码上,您会注意到我在 3 个级别构建它:

  • 针对城市名称进行文本搜索(可以使用内存和 DbSet 上下文)
  • 针对状态名称进行文本搜索(可以使用内存和 DbSet 上下文)
  • 将文本搜索定位在国家/地区名称(内存中的上下文可以,但在 DbSet 上下文中崩溃)。

谓词构建过程显然从一个错误的初始化开始,然后是每个单词和每个字段目标的 OR。在此处发布之前,我尝试了以下尝试,如先前与类似问题相关的问题中所述:

  • 在表达式构建的各个步骤调用 LinqKit Expand()(提示:它没有帮助!)
  • 以前将崩溃表达式存储在局部变量中,并使用该Expression变量而不是匿名 lambda(不是更好)
  • 仅使用涉及级别 2 遍历(崩溃的那个)的表达式,它仍然崩溃
  • 移除Name.Contains2级遍历的部分,但保留Id的2级遍历。在这种情况下它不会崩溃(当然文本查找没有完成)。

这是一个完整的复制代码(创建一个 localDb 数据库,为其播种,然后运行崩溃代码)。

这是一个 .Net 5 控制台包项目,您需要将以下 NuGet 包添加到 project.json 文件中:

"dependencies": {
    "LinqKit": "1.1.3.1",
    "EntityFramework.Commands": "7.0.0-rc1-final",
    "EntityFramework.Core": "7.0.0-rc1-final",
    "EntityFramework.MicrosoftSqlServer": "7.0.0-rc1-final"
}

控制台应用程序的 C# 代码:

using System;
using System.Collections.Generic;
using System.Linq;
using LinqKit;
using Microsoft.Data.Entity;

namespace LinqKitIssue
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Country : BaseEntity
    {
        public virtual ICollection<State> States { get; set; } = new List<State>();
    }

    public class State : BaseEntity
    {
        public int CountryId { get; set; }
        public virtual Country Country { get; set; }
        public virtual ICollection<City> Cities { get; set; } = new List<City>();
    }

    public class City : BaseEntity
    {
        public int StateId { get; set; }
        public virtual State State { get; set; }
        public override string ToString() => $"{Name}, {State?.Name} ({State?.Country?.Name})";
    }

    // setup DbContext
    public class MyContext : DbContext
    {
        public DbSet<City> Cities { get; set; }
        public DbSet<State> States { get; set; }
        public DbSet<Country> Countries { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=LinqKitIssue;Trusted_Connection=True;MultipleActiveResultSets=true");
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Country>().HasMany(c => c.States).WithOne(c => c.Country).HasForeignKey(s => s.CountryId);
            modelBuilder.Entity<State>().HasMany(c => c.Cities).WithOne(c => c.State).HasForeignKey(c => c.StateId);
            modelBuilder.Entity<City>().HasOne(f => f.State);
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            // Seed with all cities
            SeedData.Seed();

            // search parameters
            var searchInput = "los las british france";
            var searchWords = searchInput.Split(' ');
            // will be looking for each word, inclusively
            var predicate = PredicateBuilder.False<City>();
            // iterate to look for each word
            foreach (var word in searchWords)
            {
                // Level 0 : look for the word at each level : city (ok with in-mem and ef)
                predicate = predicate.Or(c => c.Name.Contains(word));
                // Level 1 : then state (ok with in-mem and ef)
                predicate = predicate.Or(c =>
                    c.StateId > 0 &&
                    c.State.Name.Contains(word));
                // Level 2 : then country (ok with in-mem, crashes with ef)
                predicate = predicate.Or(c =>
                    c.StateId > 0 &&
                    c.State.CountryId > 0 &&
                    c.State.Country.Name.Contains(word));
            }

            // apply
            Console.WriteLine($"Search results for : '{searchInput}'");
            using (var ctx = new MyContext())
            {
                var query = ctx.Cities.AsQueryable();
                // includes
                query = query.Include(c => c.State).ThenInclude(s => s.Country);
                // search
                var searchResults = query.Where(predicate).ToList();
                searchResults.ForEach(Console.WriteLine);
            }
        }
    }

    public static class SeedData
    {
        public static void Seed()
        {
            using (var ctx = new MyContext())
            {
                ctx.Database.EnsureDeleted();
                ctx.Database.EnsureCreated();

                // countries
                var us = new Country { Name = "united states" };
                var canada = new Country { Name = "canada" };
                ctx.Countries.AddRange(us, canada);
                ctx.SaveChanges();

                // states
                var california = new State { Name = "california", Country = us };
                var nevada = new State { Name = "nevada", Country = us };
                var quebec = new State { Name = "quebec", Country = canada };
                var bc = new State { Name = "british columbia", Country = canada };
                ctx.States.AddRange(california, nevada, quebec, bc);
                ctx.SaveChanges();

                // Cities
                var sf = new City { Name = "san francisco", State = california };
                var la = new City { Name = "los angeles", State = california };
                var lv = new City { Name = "las vegas", State = nevada };
                var mt = new City { Name = "montreal", State = quebec };
                var vc = new City { Name = "vancouver", State = bc };
                ctx.Cities.AddRange(sf, la, lv, mt, vc);
                ctx.SaveChanges();
            }
        }
    }
}

下面是完整的堆栈跟踪(抱歉是法语,但我不想翻译它,而且法语单词与英语单词相比是透明的!)。

System.InvalidCastException: Impossible d'effectuer un cast d'un objet de type 'System.Linq.Expressions.FieldExpression' en type 'System.Linq.Expressions.ParameterExpression'.
   à Microsoft.Data.Entity.Query.ExpressionVisitors.Internal.IncludeExpressionVisitor.VisitMethodCall(MethodCallExpression expression)
   à System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   à Microsoft.Data.Entity.Query.ExpressionVisitors.ExpressionVisitorBase.Visit(Expression expression)
   à System.Linq.Expressions.ExpressionVisitor.VisitArguments(IArgumentProvider nodes)
   à System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   à Microsoft.Data.Entity.Query.ExpressionVisitors.Internal.IncludeExpressionVisitor.VisitMethodCall(MethodCallExpression expression)
   à System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   à Microsoft.Data.Entity.Query.ExpressionVisitors.ExpressionVisitorBase.Visit(Expression expression)
   à Microsoft.Data.Entity.Query.RelationalQueryModelVisitor.IncludeNavigations(IncludeSpecification includeSpecification, Type resultType, LambdaExpression accessorLambda, Boolean querySourceRequiresTracking)
   à Microsoft.Data.Entity.Query.EntityQueryModelVisitor.IncludeNavigations(QueryModel queryModel, IReadOnlyCollection`1 includeSpecifications)
   à Microsoft.Data.Entity.Query.RelationalQueryModelVisitor.IncludeNavigations(QueryModel queryModel, IReadOnlyCollection`1 includeSpecifications)
   à Microsoft.Data.Entity.Query.EntityQueryModelVisitor.IncludeNavigations(QueryModel queryModel)
   à Microsoft.Data.Entity.Query.EntityQueryModelVisitor.CreateQueryExecutor[TResult](QueryModel queryModel)
   à Microsoft.Data.Entity.Storage.Database.CompileQuery[TResult](QueryModel queryModel)
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
   à Microsoft.Data.Entity.Query.Internal.QueryCompiler.<>c__DisplayClass18_0`1.<CompileQuery>b__0()
   à Microsoft.Data.Entity.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   à Microsoft.Data.Entity.Query.Internal.QueryCompiler.CompileQuery[TResult](Expression query)
   à Microsoft.Data.Entity.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   à Microsoft.Data.Entity.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   à Remotion.Linq.QueryableBase`1.GetEnumerator()
   à System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
   à System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   à LinqKitIssue.Program.Main(String[] args) dans d:\documents\visual studio 2015\Projects\LinqKitIssue\src\LinqKitIssue\Program.cs:ligne 98
--- Fin de la trace de la pile à partir de l'emplacement précédent au niveau duquel l'exception a été levée ---
   à System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   à Microsoft.Dnx.Runtime.Common.EntryPointExecutor.Execute(Assembly assembly, String[] args, IServiceProvider serviceProvider)
   à Microsoft.Dnx.ApplicationHost.Program.<>c__DisplayClass3_0.<ExecuteMain>b__0()
   à System.Threading.Tasks.Task`1.InnerInvoke()
   à System.Threading.Tasks.Task.Execute()
4

0 回答 0