.ThenIncludeを使用してプロパティの子コレクションを返すことを処理できるEntity Framework Core 2.0のリポジトリメソッドを記述しようとしていますが、2番目の式に問題があります。エンティティの子プロパティ(ラムダのリストを指定)を返す.Includeの作業メソッドを次に示します。
public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
{
IQueryable<T> query = _context.Set<T>();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query.Where(predicate).FirstOrDefault();
}
ここで、2つの式のタプルを取得し、それらを.Include(a => a.someChild).ThenInclude(b => b.aChildOfSomeChild)チェーンにフィードするメソッドを記述しようとしています。これは、子の1つの子しか処理しないため、完璧なソリューションではありませんが、開始点です。
public T GetSingle(Expression<Func<T, bool>> predicate, params Tuple<Expression<Func<T, object>>, Expression<Func<T, object>>>[] includeProperties)
{
IQueryable<T> query = _context.Set<T>();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty.Item1).ThenInclude(includeProperty.Item2);
}
return query.Where(predicate).FirstOrDefault();
}
Intellisenseは「使用法から型を推測できません。型を明示的に指定してみてください」というエラーを返します。 Item2の式は、Item1が持つ子関係について知る必要があるため、何らかの形でItem1に関連するものとして分類する必要があるためだと感じています。
このようなメソッドを書くためのアイデアやより良いテクニックはありますか?
このリポジトリの方法はオンラインで見つけましたが、私が望んでいたとおりに動作します。 Yaredの答えは良かったが、そこまでずっとではなかった。
/// <summary>
/// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query.
/// </summary>
/// <param name="selector">The selector for projection.</param>
/// <param name="predicate">A function to test each element for a condition.</param>
/// <param name="orderBy">A function to order elements.</param>
/// <param name="include">A function to include navigation properties</param>
/// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
/// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
/// <remarks>This method default no-tracking query.</remarks>
public TResult GetFirstOrDefault<TResult>(Expression<Func<TEntity, TResult>> selector,
Expression<Func<TEntity, bool>> predicate = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
bool disableTracking = true)
{
IQueryable<TEntity> query = _dbSet;
if (disableTracking)
{
query = query.AsNoTracking();
}
if (include != null)
{
query = include(query);
}
if (predicate != null)
{
query = query.Where(predicate);
}
if (orderBy != null)
{
return orderBy(query).Select(selector).FirstOrDefault();
}
else
{
return query.Select(selector).FirstOrDefault();
}
}
使用法:
var affiliate = await affiliateRepository.GetFirstOrDefaultAsync(
predicate: b => b.Id == id,
include: source => source
.Include(a => a.Branches)
.ThenInclude(a => a.Emails)
.Include(a => a.Branches)
.ThenInclude(a => a.Phones));
EF Coreは遅延読み込みをサポートしていないため、同じ問題が発生しましたが、次の方法で回避策を講じようとしました。
最初に属性クラスを作成して、特定のクラスの他のプロパティから目的のナビゲーションプロパティをマークします。
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class NavigationPropertyAttribute : Attribute
{
public NavigationPropertyAttribute()
{
}
}
ナビゲーションプロパティを除外し、文字列ベースのEagerロードを使用してInclude/ThenIncludeを適用する拡張メソッド。
public static class DbContextHelper
{
public static Func<IQueryable<T>, IQueryable<T>> GetNavigations<T>() where T : BaseEntity
{
var type = typeof(T);
var navigationProperties = new List<string>();
//get navigation properties
GetNavigationProperties(type, type, string.Empty, navigationProperties);
Func<IQueryable<T>, IQueryable<T>> includes = ( query => {
return navigationProperties.Aggregate(query, (current, inc) => current.Include(inc));
});
return includes;
}
private static void GetNavigationProperties(Type baseType, Type type, string parentPropertyName, IList<string> accumulator)
{
//get navigation properties
var properties = type.GetProperties();
var navigationPropertyInfoList = properties.Where(prop => prop.IsDefined(typeof(NavigationPropertyAttribute)));
foreach (PropertyInfo prop in navigationPropertyInfoList)
{
var propertyType = prop.PropertyType;
var elementType = propertyType.GetTypeInfo().IsGenericType ? propertyType.GetGenericArguments()[0] : propertyType;
//Prepare navigation property in {parentPropertyName}.{propertyName} format and Push into accumulator
var properyName = string.Format("{0}{1}{2}", parentPropertyName, string.IsNullOrEmpty(parentPropertyName) ? string.Empty : ".", prop.Name);
accumulator.Add(properyName);
//Skip recursion of propert has JsonIgnore attribute or current property type is the same as baseType
var isJsonIgnored = prop.IsDefined(typeof(JsonIgnoreAttribute));
if(!isJsonIgnored && elementType != baseType){
GetNavigationProperties(baseType, elementType, properyName, accumulator);
}
}
}
}
NavigationPropertyAttribute
を実装するサンプルPOCOクラス
public class A : BaseEntity{
public string Prop{ get; set; }
}
public class B : BaseEntity{
[NavigationProperty]
public virtual A A{ get; set; }
}
public class C : BaseEntity{
[NavigationProperty]
public virtual B B{ get; set; }
}
リポジトリでの使用
public async Task<T> GetAsync(Expression<Func<T, bool>> predicate)
{
Func<IQueryable<T>, IQueryable<T>> includes = DbContextHelper.GetNavigations<T>();
IQueryable<T> query = _context.Set<T>();
if (includes != null)
{
query = includes(query);
}
var entity = await query.FirstOrDefaultAsync(predicate);
return entity;
}
サンプルクラスCのJSON結果は次のようになります。
{
"B" : {
"A" : {
"Prop" : "SOME_VALUE"
}
}
}