シンプルな EF7 コード ファースト モデルがあります: Countries
have States
、States
have Cities
。リレーションと逆リレーションが定義されているため、ナビゲーション フィールドとコレクションを双方向にトラバースできます。
すべての都市でテキスト検索を実行したい: 検索語を分割して各単語を次々と検索し、この検索で都市名、州名、国名を対象とし、この検索を包括的にします (「 france germany francisco'、France の都市、Germany の都市、および San Francisco が検索結果に含まれます)。
それを達成するために、LinqKit の を使用して検索述語を作成していますPredicateBuilder
。
エンティティの単純なメモリ内コレクションで述語の構築とコードの実行をテストすると、すべてが期待どおりに機能します。
Entity Framework 7 コンテキストで Cities DbSet に対して実行すると、この奇妙な例外が発生します ( System.InvalidCastException 'System.Linq.Expressions.FieldExpression' to type 'System.Linq.Expressions.ParameterExpression'
)。
最初は LinqKit に関連していると思いましたが、実際には、2 レベルの関係を含むフィールドを探すときに EF7 で PredicateBuilder を使用することに関連しています。
// 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 コンテキストでOK)
- 状態名をテキスト検索のターゲットにします(インメモリおよび DbSet コンテキストでOK)
- 国名のテキスト検索をターゲットにします(メモリ内コンテキストでは問題ありませんが、DbSet コンテキストではクラッシュします)。
述語の構築プロセスは明らかに false init から始まり、次に各単語と各フィールド ターゲットの OR になります。ここに投稿する前に、同様のものに関連する以前の質問で読んだように、次の試みを試みました。
- 式構築のさまざまなステップでLinqKit を呼び出し
Expand()
ます (ヒント: 役に立ちませんでした!) - 以前にクラッシュする式をローカル変数に保存し
Expression
、匿名ラムダの代わりにその変数を使用しました (それ以上ではありません)。 - レベル 2 トラバーサル (クラッシュするもの) を含む式のみを使用し、それでもクラッシュする
- レベル 2 トラバーサルの一部を削除し
Name.Contains
ますが、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()