#LINQ to Entities でエンティティのプロパティ名を動的に切り替える
やってみたかったこと!
エンティティに対して特定の処理を行いたい!
エンティティには特定の情報(例えばint型の付加情報など)を持たせておく必要がある。
ただし、プロパティ名は強制しない。
エンティティに特別な制限(インターフェースを実装させるなど)をかけたくない。
そのためには、プロパティを動的に解決する必要があった。
注意:ここでメモってる内容は、あくまで「趣味」の範囲です。ただの好奇心です。さらに複雑なクエリでは検証してません。
結局は標準SQLを使いましたが、つい遊び心に火がついてしまったのでメモしておきます。
コードファーストで次のようなエンティティクラスを作成します。ついでにコンテキストも用意。
public class SampleContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Character> Characters { get; set; }
}
public class User
{
public int Id { get; set; }
public int Level { get; set; }
public int Age { get; set; }
public override string ToString() => $"{Id} / Level = {Level}, Age = {Age}";
}
public class Character
{
public int Id { get; set; }
public int Power { get; set; }
public int YearsOld { get; set; }
public override string ToString() => $"{Id} / Power = {Power}, YearsOld = {YearsOld}";
}
internal sealed class SampleConfiguration : DbMigrationsConfiguration<DiconExp.SampleContext>
{
public SampleConfiguration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SampleContext context)
{
context.Users.AddOrUpdate(
p => p.Id,
new User { Id = 1, Level = 3, Age = 13 },
new User { Id = 2, Level = 19, Age = 24 },
new User { Id = 3, Level = 8, Age = 19 },
new User { Id = 4, Level = 7, Age = 33 },
new User { Id = 5, Level = 13, Age = 11 },
new User { Id = 6, Level = 20, Age = 28 }
);
context.Characters.AddOrUpdate(
p => p.Id,
new Character { Id = 1, Power = 3, YearsOld = 13 },
new Character { Id = 2, Power = 19, YearsOld = 24 },
new Character { Id = 3, Power = 8, YearsOld = 19 },
new Character { Id = 4, Power = 7, YearsOld = 33 },
new Character { Id = 5, Power = 13, YearsOld = 11 },
new Character { Id = 6, Power = 20, YearsOld = 28 }
);
context.SaveChanges();
base.Seed(context);
}
}
レッツマイグレーション!
このようなテーブルが出来ます。
詳しくはこちらが参考になります。
次のようなSQLを書くとします(実際にAgeなんてカラムは作らないと思いますが考えるの面倒なので許してください)
SELECT * FROM Users AS usr WHERE usr.Level < 10 AND usr.Age > 16;
まー、大体こんな感じで取得できます。
このSQLと同じ処理をLINQで書くと大体こんな感じです。
var query = from usr in ctx.Users where usr.Level < 10 && usr.Age > 16 select usr;
プロパティを見ても予想どおの結果が出てます。
Entity Frameworkではこのようにも書けます。
ctx.Users.Where(usr => usr.Level < 10 && usr.Age > 16);
ctx.Set<User>().Where(usr => usr.Level < 10 && usr.Age > 16);
特にDbContext.Set()を使うとエンティティクラスを動的に変更出来ます。
LINQがSQLに変換されたことを確かめるため、DbContext.Database.Logでコンソール出力出来るようにしました。
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
var query = from usr in ctx.Users where usr.Level < 10 && usr.Age > 16 select usr;
var items = query.ToList();
}
↑コンソール画面に出力されたSQL
この辺の仕組みがよくわからない人は「式ツリー」や「クエリ式」を調べてください。
式ツリーについてはこちらが参考になります。
クエリ式についてはこちらを。
さてここで、このLevelやAgeなどの名前を動的に変更する必要が出てきました。
エンティティクラスの型を意識しないようメソッドを作りたかったからです。
User.Level / Character.Power // レベルとかパワー、本人の能力を数値化したもの。
User.Age / Character.YearsOld // 年齢
上記のUserとCharacter、意図は同じでもプロパティ名が異なります。
プロパティ名に依存しない、ライブラリ的なメソッドを作りたかったわけです。
public class Program
{
// ↓こんなふうに。DbContextを引数で渡すべきかは別として。
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, int level, int age)
where TEntity : class
{
return ctx.Set<TEntity>().Where(p => p.Level < level && p.Age > age);
}
public static void Main(string[] args)
{
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
var users = GetEntities<User>(ctx, 10, 16).ToList();
var characters = GetEntities<Character>(ctx, 10, 16).ToList();
}
}
}
もちろんうまくいきません。
TEntityからはLevelやAgeなどのプロパティが認識できないのでコンパイルエラーになります。
エンティティに特定のインターフェース(Level,Ageを定義したもの)を実装させてジェネリック型に制約かければ!?
それが私情で出来ないからこの記事が出来上がったわけでして、あとPOCO/POJO的にどうなのかなーと思いまして。
そもそもCharacterではそれさえ無理があります。
引数で値取得用のデリゲート受け取ったら?
public class Program
{
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, int level, int age, Func<TEntity, int> levelGetter, Func<TEntity, int> ageGetter)
where TEntity : class
{
return ctx.Set<TEntity>().Where(p => levelGetter(p) < level && ageGetter(p) > age);
}
public static void Main(string[] args)
{
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
var users = GetEntities<User>(ctx, 10, 16, p => p.Level, p => p.Age).ToList();
var characters = GetEntities<Character>(ctx, 10, 16, p => p.Power, p => p.YearsOld).ToList();
}
}
}
LINQ的には問題ありません。
ところがEntity Frameworkで使ってしまうとエラーになってしまいます。
LINQ to Entitiesでは式に制限があって、クエリ中にラムダ式などは使えません。
この辺りが参考になります。
ToList()後に、コレクションを走査する分には(LINQ的には)問題ありませんが、
これはSQLに反映されるのではなく、一旦DBからとってきたコレクションに対して行われるだけなので無意味です。
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, int level, int age, Func<TEntity, int> levelGetter, Func<TEntity, int> ageGetter)
where TEntity : class
{
// return ctx.Set<TEntity>().Where(p => levelGetter(p) < level && ageGetter(p) > age);
return ctx.Set<TEntity>().ToList().Where(p => levelGetter(p) < level && ageGetter(p) > age);
}
動くには動くが、SQLには反映されません。
Where()部分にカーソルを充ててよく見てみましょう。
LINQ to ObjectsはIEnumerable<>なのに対して、LINQ to EntitiesはIQueryable<>になっているのがわかります。
↑LINQ to Objects
↑LINQ to Entities
##2の方法、よろしい、ならば式ツリーごと書いちゃえ!
public class Program
{
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, int level, int age, string levelName, string ageName)
where TEntity : class
{
var entityParameter = Expression.Parameter(typeof(TEntity), "p");
var exp = Expression.Lambda<Func<TEntity, bool>>(
Expression.AndAlso(
Expression.LessThan(
Expression.Property(entityParameter,levelName ),
Expression.Constant(level)
),
Expression.GreaterThan(
Expression.Property(entityParameter, ageName),
Expression.Constant(age)
)
),
entityParameter
);
return ctx.Set<TEntity>().Where(exp);
}
public static void Main(string[] args)
{
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
var users = GetEntities<User>(ctx, 10, 16, nameof(User.Level), nameof(User.Age)).ToList();
var characters = GetEntities<Character>(ctx, 10, 16, nameof(Character.Power), nameof(Character.YearsOld)).ToList();
}
}
}
引数にそれぞれのプロパティ名を渡し動的に作り出しますが、
とにかく見づらい、分かりにくい。
式ツリー自体学習難易度若干高めだし、可読性が悪く現実的ではない。
さらに複雑な式になってしまうと地獄になってしまいます。
##3の方法、とりあえずクエリを書いて、部分的に変更すればいいんや!?
コメントで簡潔な方法教えていただきました! お騒がせしましたー!
詳しくはコメント欄ごご覧ください!
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, Expression<Func<TEntity, bool>> levelFilter, Expression<Func<TEntity, bool>> ageFilter)
とすると良かったんですね。
一応のこしておきます。
アイデア的には一旦ラムダ式でクエリを書いた上で、目的の箇所だけ変更する方法です。
大雑把に解説すると、
p => DmyLevel < 10 && DmyAge > 16;
↑を
p => p.Level < 10 && p.Age > 16;
↑や(Userの時)
p => p.Power < 10 && p.YearsOld > 16;
↑に変換(Characterの時)
DmyLevelの部分をp.Levelやp.Powerに、
DmyAgeの部分をp.Ageやp.YearsOldに、
すり替えてしまえばいいよね!?
まるでテンプレートのように!
ちなみに「Dmy」は「Dummy」を略しただけです。
もちろんダミーとなる変数はあらかじめ定義しておく必要があります。
はたしてそんなことが出来るのか???
探してみました。
ExpressionVisitorというのがあるんですね!!!!?
変更はできないけど、走査して再構築って感じですね。
これをもとに、ライブラリ書きました。
public class Program
{
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, int level, int age)
where TEntity : class
{
throw new NotSupportedException();
}
public static void Main(string[] args)
{
// ダミー変数の定義
var dmyLevel = 0;
var dmyAge = 0;
// Userの設定
var usrRewriter = new DynamicEntityPropertiesRewriter<User>();
usrRewriter.Add(nameof(dmyLevel), nameof(User.Level)); // usrRewriter.Add("dmyLevel", "Level");
usrRewriter.Add(nameof(dmyAge), nameof(User.Age)); // usrRewriter.Add("dmyAge", "Age");
// Characterの設定
var chrRewriter = new DynamicEntityPropertiesRewriter<Character>();
chrRewriter.Add(nameof(dmyLevel), nameof(Character.Power)); // chrRewriter.Add("dmyLevel", "Power");
chrRewriter.Add(nameof(dmyAge), nameof(Character.YearsOld)); // chrRewriter.Add("dmyAge", "YearsOld");
// クエリのすり替え
var usrWhere = usrRewriter.Rewrite(p => dmyLevel < 10 && dmyAge > 16);
var chrWhere = chrRewriter.Rewrite(p => dmyLevel < 10 && dmyAge > 16);
// 実際にDBアクセス
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
var usrItems = ctx.Users.Where(usrWhere).ToList();
var chrItems = ctx.Characters.Where(chrWhere).ToList();
}
}
}
/// <summary>
/// dynamic entity properties? 動的にエンティティのプロパティを変更します。
/// 一見無駄なことしているように見えますが、度重なるバグ回避によりようやくたどり着いたコードだったりします。
/// ExpressionVisitor及びExpression.Property()は実行する毎にインスタンスを生成してます(インスタンス違いによるパラメータバグにつながるため)。
/// </summary>
public class DynamicEntityPropertiesRewriter<TEntity>
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
public void Add(string dmy, string rewriteName)
{
this.parameters[dmy] = rewriteName;
}
IDictionary<string, MemberExpression> createProperties(ParameterExpression pParam)
{
return parameters.ToDictionary(p => p.Key, p => Expression.Property(pParam, p.Value));
}
public Expression<Func<TEntity, bool>> Rewrite(Expression<Func<TEntity, bool>> node)
{
var visitor = new RwVisitor(this.createProperties(node.Parameters.First()));
return visitor.Visit(node) as Expression<Func<TEntity, bool>>;
}
class RwVisitor : ExpressionVisitor
{
IDictionary<string, MemberExpression> properties;
public RwVisitor(IDictionary<string, MemberExpression> properties)
{
this.properties = properties;
}
protected override Expression VisitMember(MemberExpression node)
{
var dmyName = node.Member.Name;
return this.properties.FirstOrDefault(p => p.Key == dmyName).Value ?? node;
}
}
}
うんうん、いい感じ!
注目すべき変数は、usrWhere/chrWhereと、usrItems/chrItemsです。
動的にクエリを作成し、さらにその結果を取得出来ていることが確認出来ます。
もちろんSQLもきちんと意図したとおり生成されてます。
これを修正します。
public class Program
{
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, int level, int age, string levelName, string ageName)
where TEntity : class
{
// ダミー変数の定義
var dmyLevel = 0;
var dmyAge = 0;
var rewriter = new DynamicEntityPropertiesRewriter<TEntity>();
rewriter.Add(nameof(dmyLevel), levelName);
rewriter.Add(nameof(dmyAge), ageName);
var whereQuery = rewriter.Rewrite(p => dmyLevel < 10 && dmyAge > 16);
return ctx.Set<TEntity>().Where(whereQuery);
}
public static void Main(string[] args)
{
// 実際にDBアクセス
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
var usrItems = GetEntities<User>(ctx, 10, 16, nameof(User.Level), nameof(User.Age)).ToList();
var chrItems = GetEntities<Character>(ctx, 10, 16, nameof(Character.Power), nameof(Character.YearsOld)).ToList();
}
}
}
public class DynamicEntityPropertiesRewriter<TEntity>
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
public void Add(string dmy, string rewriteName)
{
this.parameters[dmy] = rewriteName;
}
IDictionary<string, MemberExpression> createProperties(ParameterExpression pParam)
{
return parameters.ToDictionary(p => p.Key, p => Expression.Property(pParam, p.Value));
}
public Expression<Func<TEntity, bool>> Rewrite(Expression<Func<TEntity, bool>> node)
{
var visitor = new RwVisitor(this.createProperties(node.Parameters.First()));
return visitor.Visit(node) as Expression<Func<TEntity, bool>>;
}
class RwVisitor : ExpressionVisitor
{
IDictionary<string, MemberExpression> properties;
public RwVisitor(IDictionary<string, MemberExpression> properties)
{
this.properties = properties;
}
protected override Expression VisitMember(MemberExpression node)
{
var dmyName = node.Member.Name;
return this.properties.FirstOrDefault(p => p.Key == dmyName).Value ?? node;
}
}
}
public void Add(string dmy, string rewriteName)
登録時にパラメータを作成しません、一旦Dictionaryで文字列型のペアを作成します。
var visitor = new RwVisitor(this.createProperties(node.Parameters.First()));
Rewrite()を呼び出した時にExpressionVisitor派生インスタンスを生成します。
この時、すり替え用のパラメーターをもつ辞書を作成し、コンストラクタに渡します。
return parameters.ToDictionary(p => p.Key, p => Expression.Property(pParam, p.Value));
特に厄介な点は、pParamがラムダ式全体で共通したインスタンスであることです。
このことに気づかずかなり苦戦を強いられました。
return this.properties.FirstOrDefault(p => p.Key == dmyName).Value ?? node;
ダミーの変数名をキーとして、辞書の中からキーを見つけたらパラメータにすり替えます、それ以外はそのままnodeを返します。
すべてのコードを載せておきます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq.Expressions;
namespace DiconExp
{
public class SampleContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Character> Characters { get; set; }
}
public class User
{
public int Id { get; set; }
public int Level { get; set; }
public int Age { get; set; }
public override string ToString() => $"{Id} / Level = {Level}, Age = {Age}";
}
public class Character
{
public int Id { get; set; }
public int Power { get; set; }
public int YearsOld { get; set; }
public override string ToString() => $"{Id} / Power = {Power}, YearsOld = {YearsOld}";
}
internal sealed class SampleConfiguration : DbMigrationsConfiguration<DiconExp.SampleContext>
{
public SampleConfiguration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SampleContext context)
{
context.Users.AddOrUpdate(
p => p.Id,
new User { Id = 1, Level = 3, Age = 13 },
new User { Id = 2, Level = 19, Age = 24 },
new User { Id = 3, Level = 8, Age = 19 },
new User { Id = 4, Level = 7, Age = 33 },
new User { Id = 5, Level = 13, Age = 11 },
new User { Id = 6, Level = 20, Age = 28 }
);
context.Characters.AddOrUpdate(
p => p.Id,
new Character { Id = 1, Power = 3, YearsOld = 13 },
new Character { Id = 2, Power = 19, YearsOld = 24 },
new Character { Id = 3, Power = 8, YearsOld = 19 },
new Character { Id = 4, Power = 7, YearsOld = 33 },
new Character { Id = 5, Power = 13, YearsOld = 11 },
new Character { Id = 6, Power = 20, YearsOld = 28 }
);
context.SaveChanges();
base.Seed(context);
}
}
public class Program
{
public static IEnumerable<TEntity> GetEntities<TEntity>(DbContext ctx, int level, int age, string levelName, string ageName)
where TEntity : class
{
// ダミー変数の定義
var dmyLevel = 0;
var dmyAge = 0;
var rewriter = new DynamicEntityPropertiesRewriter<TEntity>();
rewriter.Add(nameof(dmyLevel), levelName);
rewriter.Add(nameof(dmyAge), ageName);
var whereQuery = rewriter.Rewrite(p => dmyLevel < 10 && dmyAge > 16);
return ctx.Set<TEntity>().Where(whereQuery);
}
public static void Main(string[] args)
{
// 実際にDBアクセス
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
var usrItems = GetEntities<User>(ctx, 10, 16, nameof(User.Level), nameof(User.Age)).ToList();
var chrItems = GetEntities<Character>(ctx, 10, 16, nameof(Character.Power), nameof(Character.YearsOld)).ToList();
}
}
}
public class DynamicEntityPropertiesRewriter<TEntity>
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
public void Add(string dmy, string rewriteName)
{
this.parameters[dmy] = rewriteName;
}
IDictionary<string, MemberExpression> createProperties(ParameterExpression pParam)
{
return parameters.ToDictionary(p => p.Key, p => Expression.Property(pParam, p.Value));
}
public Expression<Func<TEntity, bool>> Rewrite(Expression<Func<TEntity, bool>> node)
{
var visitor = new RwVisitor(this.createProperties(node.Parameters.First()));
return visitor.Visit(node) as Expression<Func<TEntity, bool>>;
}
class RwVisitor : ExpressionVisitor
{
IDictionary<string, MemberExpression> properties;
public RwVisitor(IDictionary<string, MemberExpression> properties)
{
this.properties = properties;
}
protected override Expression VisitMember(MemberExpression node)
{
var dmyName = node.Member.Name;
return this.properties.FirstOrDefault(p => p.Key == dmyName).Value ?? node;
}
}
}
}
スルー推奨! 作ってしまったものはしょうがない?
一旦没になりましたが、気まぐれな好奇心で実験しました。
ローカルにおいておくとすぐ紛失するのでメモとして残しておきます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq.Expressions;
namespace DiconExp
{
public class SampleContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Character> Characters { get; set; }
}
public class User
{
public int Id { get; set; }
public int Level { get; set; }
public int Age { get; set; }
public override string ToString() => $"{Id} / Level = {Level}, Age = {Age}";
}
public class Character
{
public int Id { get; set; }
public int Power { get; set; }
public int YearsOld { get; set; }
public override string ToString() => $"{Id} / Power = {Power}, YearsOld = {YearsOld}";
}
internal sealed class SampleConfiguration : DbMigrationsConfiguration<DiconExp.SampleContext>
{
public SampleConfiguration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SampleContext context)
{
context.Users.AddOrUpdate(
p => p.Id,
new User { Id = 1, Level = 3, Age = 13 },
new User { Id = 2, Level = 19, Age = 24 },
new User { Id = 3, Level = 8, Age = 19 },
new User { Id = 4, Level = 7, Age = 33 },
new User { Id = 5, Level = 13, Age = 11 },
new User { Id = 6, Level = 20, Age = 28 }
);
context.Characters.AddOrUpdate(
p => p.Id,
new Character { Id = 1, Power = 3, YearsOld = 13 },
new Character { Id = 2, Power = 19, YearsOld = 24 },
new Character { Id = 3, Power = 8, YearsOld = 19 },
new Character { Id = 4, Power = 7, YearsOld = 33 },
new Character { Id = 5, Power = 13, YearsOld = 11 },
new Character { Id = 6, Power = 20, YearsOld = 28 }
);
context.SaveChanges();
base.Seed(context);
}
}
public class Program
{
#region 凡庸的なクラス、プロパティ名の違いを吸収してクエリ!
public interface IEntityReader<TEntity>
{
IEnumerable<TEntity> GetEntities(params Expression<Func<TEntity, bool>>[] filters);
}
public class EntityReaderBase<TEntity> : IEntityReader<TEntity>
where TEntity : class
{
protected DbContext Context { get; private set; }
protected DynamicEntityPropertiesRewriter<TEntity> Rewriter { get; private set; }
public EntityReaderBase(DbContext ctx)
{
this.Context = ctx;
this.Rewriter = new DynamicEntityPropertiesRewriter<TEntity>();
}
// コメントを参考にさせていただきました。さらにRewriter適用バージョンです。
public IEnumerable<TEntity> GetEntities(params Expression<Func<TEntity, bool>>[] filters)
{
IQueryable<TEntity> query = Context.Set<TEntity>();
foreach (var f in filters)
{
query = query.Where(this.Rewriter.Rewrite(f));
}
return query;
}
}
#endregion
#region さらにロジック追加!
public interface IDiconEntityReader<TEntity> : IEntityReader<TEntity>
{
IEnumerable<TEntity> GetPowerful();
IEnumerable<TEntity> GetGreaterOrEqual(int level, int age);
}
public class DiconEntityReader<TEntity> : EntityReaderBase<TEntity>, IDiconEntityReader<TEntity>
where TEntity : class
{
public static readonly int DmyLevel = 0;
public static readonly int DmyAge = 0;
public DiconEntityReader(DbContext ctx, string levelName, string ageName) : base(ctx)
{
this.Rewriter.Add(nameof(DmyLevel), levelName);
this.Rewriter.Add(nameof(DmyAge), ageName);
}
// つよーい人を取得(レベル20以上、二十歳以上)
public IEnumerable<TEntity> GetPowerful()
{
return this.GetEntities(p => DmyLevel >= 20 && DmyAge >= 20);
}
// level以上、かつage以上の一覧を取得します。
public IEnumerable<TEntity> GetGreaterOrEqual(int level, int age)
{
return this.GetEntities(p => DmyLevel >= level && DmyAge >= age);
}
}
#endregion
#region Entity Frameworkの範囲外のこと、違う型を抽象的に扱うとなると別のややこしさが出てくる。
public interface IDiconAccessor<TEntity>
{
int GetLevel(TEntity entity);
void SetLevel(TEntity entity, int level);
int GetAge(TEntity entity);
void SetAge(TEntity entity, int age);
}
public class UserAccessor : IDiconAccessor<User>
{
public int GetAge(User entity) => entity.Age;
public int GetLevel(User entity) => entity.Level;
public void SetAge(User entity, int age) => entity.Age = age;
public void SetLevel(User entity, int level) => entity.Level = level;
}
public class CharacterAccessor : IDiconAccessor<Character>
{
public int GetAge(Character entity) => entity.YearsOld;
public int GetLevel(Character entity) => entity.Power;
public void SetAge(Character entity, int age) => entity.YearsOld = age;
public void SetLevel(Character entity, int level) => entity.Power = level;
}
#endregion
/// <summary>
/// つよーい人をより強く、しかも年齢もUP!
/// LINQ to Entitiesで使うわけでないのでここではデリゲートが使える。
/// 他にいい方法ありそうですけど、とりあえず実験用として用意。
/// </summary>
static void PowerUp<TEntity>(IDiconEntityReader<TEntity> reader, IDiconAccessor<TEntity> accessor)
{
foreach (var p in reader.GetPowerful())
{
var level = accessor.GetLevel(p);
var age = accessor.GetAge(p);
accessor.SetLevel(p, level + 1);
accessor.SetAge(p, age + 1);
}
}
public static void Main(string[] args)
{
using (var ctx = new SampleContext())
{
ctx.Database.Log = Console.WriteLine;
IDiconEntityReader<User> usr = new DiconEntityReader<User>(ctx, nameof(User.Level), nameof(User.Age));
IDiconEntityReader<Character> chr = new DiconEntityReader<Character>(ctx, nameof(Character.Power), nameof(Character.YearsOld));
// つぉーい人取得
var usrItems = usr.GetPowerful().ToList();
var chrItems = chr.GetPowerful().ToList();
// 引数付き、レベル、年齢共に10以上取得
var grt = chr.GetGreaterOrEqual(10, 10).ToList();
// インターフェースに拘るとダミー変数をどこに置くかが新たな課題。ローカル変数である必要は無さそう。
int DmyLevel = 0, DmyAge = 0;
var items = chr.GetEntities(p => DmyLevel > 10 && DmyAge > 10, p => p.Id >= 3).ToList();
// Userをパワーアップ
PowerUp(usr, new UserAccessor());
// Characterをパワーアップ
PowerUp(chr, new CharacterAccessor());
// パワーアップの処理を反映・・・ しないでおく。
//ctx.SaveChanges();
}
}
}
public class DynamicEntityPropertiesRewriter<TEntity>
{
Dictionary<string, string> parameters = new Dictionary<string, string>();
public void Add(string dmy, string rewriteName)
{
this.parameters[dmy] = rewriteName;
}
IDictionary<string, MemberExpression> createProperties(ParameterExpression pParam)
{
return parameters.ToDictionary(p => p.Key, p => Expression.Property(pParam, p.Value));
}
public Expression<Func<TEntity, bool>> Rewrite(Expression<Func<TEntity, bool>> node)
{
var visitor = new RwVisitor(this.createProperties(node.Parameters.First()));
return visitor.Visit(node) as Expression<Func<TEntity, bool>>;
}
class RwVisitor : ExpressionVisitor
{
IDictionary<string, MemberExpression> properties;
public RwVisitor(IDictionary<string, MemberExpression> properties)
{
this.properties = properties;
}
protected override Expression VisitMember(MemberExpression node)
{
var dmyName = node.Member.Name;
return this.properties.FirstOrDefault(p => p.Key == dmyName).Value ?? node;
}
}
}
}
今のところ、一見正しく動いているように見えます。
もう動けばいいや的に書いたコードで申し訳ないですが、欠陥が潜んでるかもしれないので遊びで使う以外はちょっと危険かもです。
IDiconEntityReader<User> usr = new DiconEntityReader<User>(ctx, nameof(User.Level), nameof(User.Age));
IDiconEntityReader<Character> chr = new DiconEntityReader<Character>(ctx, nameof(Character.Power), nameof(Character.YearsOld));
var usrItems = usr.GetPowerful().ToList();
var chrItems = chr.GetPowerful().ToList();
DiconEntityReaderでプロパティ名の違いを吸収してます。
もっといいほうほうありそうですが・・・orz
static void PowerUp<TEntity>(IDiconEntityReader<TEntity> reader, IDiconAccessor<TEntity> accessor)
内部ではエンティティの具体的な型について知る必要無し。
アクセッサについても式ツリーで簡略化できそう。
この辺が参考になりそう(まだ試してない)ですが、範囲外なのでやめます。
##いくつかハマったところです。
以下のコードはエラーになります。
// p => p.Level >= 10
var expression = Expression.Lambda<Func<User, bool>>(
Expression.GreaterThanOrEqual(
Expression.Property(
Expression.Parameter(typeof(User), "p"),
"Level"
),
Expression.Constant(10)
),
Expression.Parameter(typeof(User), "p")
);
var lambda = expression.Compile();
System.InvalidOperationException: '型 'DiconExp.User' の変数 'p' がスコープ '' から参照されましたが、これは定義されていません'
// p => p.Level >= 10
var pParam = Expression.Parameter(typeof(User), "p");
var expression = Expression.Lambda<Func<User, bool>>(
Expression.GreaterThanOrEqual(
Expression.Property(
pParam,
"Level"
),
Expression.Constant(10)
),
pParam
);
var lambda = expression.Compile();
原因は、パラメータのインスタンスが違うから?
都度パラメータを作成するのではなく、一度作成したパラメータを使いまわします。
p => p.Level のpの部分を共通にしたわけです。
ここにハマって2日を無駄にしました・・・。
そのままだとあまり意味のないコードですが、以下でも同じエラーが出ます。
Expression<Func<User, bool>> exp = p => p.Level >= 10;
var lmd = Expression.Lambda <Func<User, bool> > (
exp.Body,
Expression.Parameter(typeof(User), "p")
).Compile();
expから最初のパラメータを取得し、それを再利用することで回避出来ます。
Expression<Func<User, bool>> exp = p => p.Level >= 10;
var lmd = Expression.Lambda <Func<User, bool> > (
exp.Body,
exp.Parameters.First()
).Compile();
いずれも実験中にハマった内容ですが、いつか必要になる日があるかもしれないのでメモしておきます。