Solución:
Esta es una pregunta un poco vieja, pero como no tiene una respuesta aceptada, pensé en publicar mi solución a esto.
Estoy usando EF Core y quería hacer exactamente esto, acceder a la carga ansiosa desde fuera de mi clase de repositorio para poder especificar las propiedades de navegación para cargar cada vez que llamo a un método de repositorio. Como tengo una gran cantidad de tablas y datos, no quería un conjunto estándar de entidades de carga ansiosa, ya que algunas de mis consultas solo necesitaban la entidad principal y otras necesitaban todo el árbol.
Mi implementación actual solo admite IQueryable
método (es decir. FirstOrDefault
, Where
, básicamente las funciones lambda estándar) pero estoy seguro de que podría usarlo para pasar a sus métodos de repositorio específicos.
Empecé con el código fuente de EF Core EntityFrameworkQueryableExtensions.cs
que es donde el Include
y ThenInclude
Se definen los métodos de extensión. Desafortunadamente, EF usa una clase interna IncludableQueryable
para contener el árbol de propiedades anteriores para permitir un tipo fuerte incluye posteriores. Sin embargo, la implementación de esto no es más que IQueryable
con un tipo genérico adicional para la entidad anterior.
Creé mi propia versión llamé IncludableJoin
eso toma un IIncludableQueryable
como parámetro de constructor y lo almacena en un campo privado para su posterior acceso:
public interface IIncludableJoin<out TEntity, out TProperty> : IQueryable<TEntity>
{
}
public class IncludableJoin<TEntity, TPreviousProperty> : IIncludableJoin<TEntity, TPreviousProperty>
{
private readonly IIncludableQueryable<TEntity, TPreviousProperty> _query;
public IncludableJoin(IIncludableQueryable<TEntity, TPreviousProperty> query)
{
_query = query;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public IEnumerator<TEntity> GetEnumerator()
{
return _query.GetEnumerator();
}
public Expression Expression => _query.Expression;
public Type ElementType => _query.ElementType;
public IQueryProvider Provider => _query.Provider;
internal IIncludableQueryable<TEntity, TPreviousProperty> GetQuery()
{
return _query;
}
}
Tenga en cuenta el interno GetQuery
método. Esto será importante más adelante.
A continuación, en mi genérico IRepository
interfaz, definí el punto de partida para la carga ansiosa:
public interface IRepository<TEntity> where TEntity : class
{
IIncludableJoin<TEntity, TProperty> Join<TProperty>(Expression<Func<TEntity, TProperty>> navigationProperty);
...
}
los TEntity
tipo genérico es el interfaz de mi entidad EF. La implementación de la Join
El método en mi repositorio genérico es así:
public abstract class SecureRepository<TInterface, TEntity> : IRepository<TInterface>
where TEntity : class, new()
where TInterface : class
{
protected DbSet<TEntity> DbSet;
protected SecureRepository(DataContext dataContext)
{
DbSet = dataContext.Set<TEntity>();
}
public virtual IIncludableJoin<TInterface, TProperty> Join<TProperty>(Expression<Func<TInterface, TProperty>> navigationProperty)
{
return ((IQueryable<TInterface>)DbSet).Join(navigationProperty);
}
...
}
Ahora, para la parte que realmente permite múltiples Include
y ThenInclude
. Tengo varios métodos de extensión que toman y regresan y IIncludableJoin
para permitir el encadenamiento de métodos. Dentro del cual llamo al EF Include
y ThenInclude
métodos en el DbSet:
public static class RepositoryExtensions
{
public static IIncludableJoin<TEntity, TProperty> Join<TEntity, TProperty>(
this IQueryable<TEntity> query,
Expression<Func<TEntity, TProperty>> propToExpand)
where TEntity : class
{
return new IncludableJoin<TEntity, TProperty>(query.Include(propToExpand));
}
public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
this IIncludableJoin<TEntity, TPreviousProperty> query,
Expression<Func<TPreviousProperty, TProperty>> propToExpand)
where TEntity : class
{
IIncludableQueryable<TEntity, TPreviousProperty> queryable = ((IncludableJoin<TEntity, TPreviousProperty>)query).GetQuery();
return new IncludableJoin<TEntity, TProperty>(queryable.ThenInclude(propToExpand));
}
public static IIncludableJoin<TEntity, TProperty> ThenJoin<TEntity, TPreviousProperty, TProperty>(
this IIncludableJoin<TEntity, IEnumerable<TPreviousProperty>> query,
Expression<Func<TPreviousProperty, TProperty>> propToExpand)
where TEntity : class
{
var queryable = ((IncludableJoin<TEntity, IEnumerable<TPreviousProperty>>)query).GetQuery();
var include = queryable.ThenInclude(propToExpand);
return new IncludableJoin<TEntity, TProperty>(include);
}
}
En estos métodos obtengo el interno IIncludableQueryable
propiedad utilizando el mencionado GetQuery
método, llamando al relevante Include
o ThenInclude
método, luego devolviendo un nuevo IncludableJoin
objeto para soportar el método de encadenamiento.
Y eso es. El uso de esto es así:
IAccount account = _accountRepository.Join(x=>x.Subscription).Join(x=>x.Addresses).ThenJoin(x=>x.Address).FirstOrDefault(x => x.UserId == userId);
Lo anterior cargaría la base Account
entidad, es hijo uno a uno Subscription
, es una lista de niños de uno a muchos Addresses
y es niño Address
. Cada función lambda en el camino está fuertemente tipada y es compatible con intellisense para mostrar las propiedades disponibles en cada entidad.
Puedes cambiarlo a algo como esto:
public virtual TEntity GetById<TEntity>(int id, Func<IQueryable<TEntity>, IQueryable<TEntity>> func)
{
DbSet<TEntity> result = this.Set<TEntity>();
IQueryable<TEntity> resultWithEagerLoading = func(result);
return resultWithEagerLoading.FirstOrDefault(e => e.Id == id);
}
Y puedes usarlo así:
productRepository.GetById(2, x => x.Include(p => p.Orders)
.ThenInclude(o => o.LineItems)
.Include(p => p.Parts))