Sto cercando di scrivere un metodo di repository per Entity Framework Core 2.0 in grado di gestire il ritorno di raccolte di proprietà figlio utilizzando .ThenInclude, ma ho problemi con la seconda espressione. Ecco un metodo di lavoro per .Include, che restituirà le proprietà figlio (fornisci un elenco di lambda) della tua entità.
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();
}
Ora ecco il mio tentativo di scrivere un metodo che prenderà una Tupla di due espressioni e le inserirà in una catena .Include (a => a.someChild) .ThenInclude (b => b.aChildOfSomeChild). Questa non è una soluzione perfetta perché gestisce solo un figlio di un bambino, ma è un inizio.
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 restituisce un errore che dice "Il tipo non può essere dedotto dall'utilizzo, prova a specificare esplicitamente il tipo". Ho la sensazione che sia perché l'espressione nell'elemento 2 deve essere classificata come in qualche modo correlata all'articolo 1, perché ha bisogno di conoscere la relazione del bambino che ha.
Qualche idea o tecnica migliore per scrivere un metodo come questo?
Ho trovato questo metodo di deposito online e fa esattamente quello che volevo. La risposta di Yared era buona, ma non del tutto lì.
/// <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();
}
}
Uso:
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));
Ho avuto lo stesso problema dal momento che EF Core non supporta il caricamento lento, ma ho cercato di ottenere una soluzione alternativa nel modo seguente:
Per prima cosa crea una classe di attributi per contrassegnare le nostre proprietà di navigazione desiderate da altre proprietà di una determinata classe.
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
public class NavigationPropertyAttribute : Attribute
{
public NavigationPropertyAttribute()
{
}
}
Metodi di estensione per filtrare le proprietà di navigazione e applicare Include / ThenInclude utilizzando il caricamento Eager basato su stringhe.
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);
}
}
}
}
Esempi di classi POCO che implementano NavigationPropertyAttribute
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; }
}
Utilizzo nel repository
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;
}
Il risultato Json per la classe di esempio C potrebbe essere:
{
"B" : {
"A" : {
"Prop" : "SOME_VALUE"
}
}
}