Pregunta

Me pregunto cómo manejar adecuadamente el problema de carga impaciente para los gráficos de objetos complejos cuando se usa el patrón Repository. Este no es un problema específico de ORM, supongo.

Primer intento:

public interface IProductRepository : IRepository<Product>
{
  Product GetById(int id);
  IProductRepository WithCustomers();
}

Esto funcionaría bien, pero implicaría repetirme todo el tiempo (escribir métodos personalizados 'Con' en implementaciones de repositorio en todas partes).

Siguiente enfoque:

public interface IRepository<T> where T : IAggregateRoot
{
  ...
  void With(Expression<Func<T, object>> propToExpand);
}
El método

With agregará un elemento a la colección privada que se utilizará más adelante para averiguar qué accesorios deben cargarse con ansias al recuperar las entidades necesarias.

Este tipo funciona y está bien. Pero no me gusta el uso:

productRepository.With(x=>x.Customer);
productRepository.With(x=>x.Price);
productRepository.With(x=>x.Manufacturer);
var product = productRepository.GetById(id);

Básicamente, el problema es que no hay encadenamiento. Me gustaría que fuera así:

var product = productRepository
  .With(x=>x.Customer)
  .With(x=>x.Price)
  .With(x=>x.Manufacturer)
  .GetById(id);

No pude lograr esto . Incluso si pudiera, no estoy seguro de si esa solución sería elegante.

Esto lleva a pensar que me falta algo fundamental (falta de ejemplos en cualquier lugar). ¿Hay diferentes maneras de cómo manejar esto? ¿Qué son las mejores prácticas?

¿Fue útil?

Solución

Problema interesante y estoy seguro de que no eres el primero que tiene problemas con esto (absolutamente lo tengo).

Para mí, la verdadera pregunta es: ¿dónde quieres poner tu lógica de carga ansiosa?

Fuera del repositorio en el código del cliente

var product = productRepository
.With(x=>x.Customer)
.With(x=>x.Price)
.With(x=>x.Manufacturer)
.GetById(id);

No creo que sea un buen diseño de software: parece que esto podría causar "muerte por mil cortes". Si tales construcciones se encuentran dispersas en toda su aplicación.

O dentro del repositorio . Ejemplo:

interface IProductRepository {
    Product GetById(int id);
    Product GetByIdWithCustomers(int i);
}

Para que su código de cliente se vea así:

var product = productRepository.GetByIdWithCustomers(id);

Normalmente hago un BaseRepository que tiene las operaciones CRUD básicas definidas:

public class BaseRepository<TEntity, TPrimaryKey> {
    public void Save(TEntity entity) { ... }
    public void Delete(TEntity entity) { ... }
    public TEntity Load(TPrimaryKey id) { ... } // just gets the entity by primary key
}

Luego extiendo esta Clase / Interfaz base para proporcionar métodos específicos para recuperar objetos de dominio. Su enfoque parece ir en una dirección algo similar.

public class MediaRepository : BaseRepository<Media, int> {
    public long CountMediaWithCategories() { ... }
    public IList<Media> MediaInCategories(IList<Category> categories) { .... }
}

Lo bueno: todas las cosas de ORM (configuración de carga ansiosa, profundidad de búsqueda, etc.) están encapsuladas en la clase Repository, el código del cliente solo obtiene el conjunto de resultados.

Intenté trabajar con repositorios muy genéricos, como tú, pero terminé escribiendo consultas y repositorios específicos para mis objetos de dominio.

Otros consejos

var product = productRepository
 .With(x=>x.Customer)
 .With(x=>x.Price)
 .With(x=>x.Manufacturer)
 .GetById(id);

Puedo entender su deseo de determinar la profundidad de la consulta del gráfico de objetos como se muestra arriba, pero creo que podría haber una manera más fácil de hacerlo. ¿Qué tal si en lugar de elegir devolver un Producto (con Cliente, Precio y Fabricante) por ID simplemente devuelvo el Producto, y todas esas otras cosas son propiedades del Producto cargadas de manera perezosa?

Logro esta 'accesibilidad completa del gráfico' al 'encadenar' por el modelo de objetos POCO en mi capa de acceso a datos. De esta manera no necesito saber cuánto datos cargados de entusiasmo se pueden extraer en un momento dado, solo pido lo que necesito en el gráfico de objetos, y el modelo sabe qué está cargado y qué necesita recuperarse además del DAL. Eche un vistazo a estos three respuestas : trato de explicar mi enfoque allí. Si necesita más aclaraciones, hágamelo saber y editaré esta respuesta.

Es una pregunta antigua, pero quizás pueda ayudar a alguien. He pasado un tiempo buscando un buen enfoque, aquí está lo que he encontrado en C #:

IRepository.cs:

public interface IRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , params Expression<Func<TEntity, object>>[] properties);
}

Repository.cs

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{    
    private readonly DbSet<TEntity> _dbset;

    public Repository(DbSet<TEntity> dbset)
    {
        _dbset = dbset;
    }

    public virtual IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> where
                              , Expression<Func<TEntity, object>>[] properties)
    {
        if (where == null) 
            throw new ArgumentNullException(nameof(where));    
        if (properties == null) 
            throw new ArgumentNullException(nameof(properties));

        var query = _dbset as IQueryable<TEntity>; // _dbSet = dbContext.Set<TEntity>()

        query = properties
                   .Aggregate(query, (current, property) => current.Include(property));

        return query.AsNoTracking().Where(where).ToList();
    }
}

Cómo utilizar:

var repository = new Repository<User>();
var users = repository.GetAll(p => p.Id == 1, d => d.Address, d => d.Carts);

Ref: Enlace

Puedo apreciar lo que estás tratando de hacer, pero estás algo más allá del patrón de repositorio básico.

Una interfaz de repositorio mínima puede incluir métodos para:

  • GetById
  • Añadir
  • Eliminar

Si agrega métodos adicionales además de eso, comienza a ejecutarse en situaciones en las que la Interfaz no necesariamente tiene sentido para todas sus raíces agregadas.

A veces simplemente no es posible tener una API completamente hermosa. Si lo que tienes funciona "suficientemente bueno" Para ti, me gustaría ir con él. Si necesita alejarse del patrón de repositorio para proporcionar una mejor API contra la que realizar la programación, ¡hágalo!

El patrón de repositorio no es una solución para todos / para todos. A veces necesitas una solución diferente.

Si desea indicar todos los Incluye que necesita fuera de su repositorio, puede enumerar los parámetros opcionales (C #) para cada método genérico:

TEntity Find(Func<TEntity, bool> expression, params string[] eagerLoads);

Luego en su nivel de cliente:

IProductRepository.Find(x => x.Id == id, "Customer", "Price")

Si desea estar seguro de tipo, enumere sus entidades:

public enum BusinessEntities { Customer, Price, Manufacturer }

IProductRepository.Find(x => x.Id == id, BusinessEntities.Customer.ToString(), BusinessEntities.Price.ToString())

Creo que es responsabilidad del cliente pedir específicamente lo que quiere. El repositorio genérico solo debe manejar CRUD básico.

En BaseRepository.cs puede crear este método:

public async Task<IEnumerable<T>> GetWithChild(string child)
{
    return await _entities.Include(child).ToListAsync();
}

En mi API también he implementado una capa de servicio, pero desde la API simplemente llamo a este método y le paso el nombre de la variable a cargar.

Obviamente, en tu situación, deberás incluir algunas cadenas más.

Publiqué una respuesta antes pero todavía no estaba contenta con la solución. Así que aquí hay una mejor solución.

en BaseRepository.cs

public async Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] properties)
{
      IQueryable<T> query = _entities;

      query = properties.Aggregate(query, (current, property) => current.Include(property));

      return await query.AsNoTracking().ToListAsync();
}

y simplemente puede utilizar el método de la siguiente manera

await _service.GetAll(x => x.Customer, x => x.Price, x => x.Manufacturer); 
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top