¿Deben los repositorios devolver IQueryable?

66

He estado viendo muchos proyectos que tienen repositorios que devuelven instancias de IQueryable. Esto permite filtros adicionales y la clasificación se puede realizar en IQueryableotro código, lo que se traduce en diferentes SQL que se generan. Tengo curiosidad de dónde vino este patrón y si es una buena idea.

Mi mayor preocupación es que IQueryablees una promesa de llegar a la base de datos algún tiempo después, cuando se enumera. Esto significa que se arrojaría un error fuera del repositorio. Esto podría significar que se lanza una excepción de Entity Framework en una capa diferente de la aplicación.

También me he encontrado con problemas con los Conjuntos de resultados activos múltiples (MARS) en el pasado (especialmente cuando se usan transacciones) y parece que este enfoque llevaría a que esto suceda con más frecuencia.

Siempre he llamado AsEnumerableo ToArrayal final de cada una de mis expresiones LINQ para asegurarme de que la base de datos se golpea antes de abandonar el código del repositorio.

Me pregunto si regresar IQueryablepodría ser útil como un bloque de construcción para una capa de datos. He visto un código bastante extravagante con un repositorio que llama a otro repositorio para construir uno aún más grande IQueryable.

Travis Parks
fuente
¿Cuál sería la diferencia si la base de datos no estuviera disponible en el momento de la ejecución diferida de la consulta frente al momento de la construcción de la consulta? El código consumidor tendría que lidiar con esa situación independientemente.
Steven Evers
Pregunta interesante, pero probablemente será difícil de responder. Una búsqueda simple muestra un debate bastante acalorado sobre su pregunta: duckduckgo.com/?q=repository+return+iqueryable
Corbin marzo
66
Todo se reduce a si desea permitir que sus clientes agreguen cláusulas Linq que puedan beneficiarse de la ejecución diferida. Si agregan una wherecláusula a un diferido IQueryable, solo tiene que enviar esos datos por cable, no todo el conjunto de resultados.
Robert Harvey
Si está utilizando un patrón de repositorio, no, no debe devolver IQueryable. Aquí hay un buen por qué .
Arnis Lapsa
1
@SteveEvers Hay una gran diferencia. Si se lanza una excepción en mi repositorio, puedo envolverla con un tipo de excepción específico de la capa de datos. Si sucede más adelante, tengo que capturar el tipo de excepción original allí. Cuanto más cerca esté de la fuente de la excepción, más probable es que sepa qué la causó. Esto es importante para crear mensajes de error significativos. En mi humilde opinión, permitir que una excepción específica de EF salga de mi repositorio es una violación de la encapsulación.
Travis Parks

Respuestas:

47

Devolver IQueryable definitivamente brindará más flexibilidad a los consumidores del repositorio. Pone la responsabilidad de reducir los resultados al cliente, lo que naturalmente puede ser tanto un beneficio como una muleta.

En el lado bueno, no necesitará crear toneladas de métodos de repositorio (al menos en esta capa): GetAllActiveItems, GetAllNonActiveItems, etc., para obtener los datos que desea. Dependiendo de su preferencia, nuevamente, esto podría ser bueno o malo. Usted va (/ debería) es necesario definir los contratos de comportamiento que sus implementaciones se adhieren a, pero dónde va depende de usted.

Por lo tanto, puede colocar la lógica de recuperación arenosa fuera del repositorio y dejar que se use como quiera el usuario. Por lo tanto, exponer IQueryable brinda la mayor flexibilidad y permite consultas eficientes en lugar de filtrado en memoria, etc., y podría reducir la necesidad de hacer una tonelada de métodos de obtención de datos específicos.

Por otro lado, ahora le has dado a tus usuarios una escopeta. Pueden hacer cosas que quizás no haya planeado (usar en exceso .include (), realizar pesadas consultas pesadas y filtrar en la memoria en sus respectivas implementaciones, etc.), lo que básicamente evitaría los controles de comportamiento y estratificación porque ha dado Acceso completo.

Entonces, dependiendo del equipo, su experiencia, el tamaño de la aplicación, la estratificación general y la arquitectura ... depende: - \

hanzolo
fuente
Tenga en cuenta que podría, si desea trabajar para ello, devolver su IQueryable, que a su vez llama a la base de datos, pero agrega capas y controles de comportamiento.
psr
1
@psr ¿Podría explicar qué quiere decir con esto?
Travis Parks
1
@TravisParks: IQueryable no tiene que ser implementado por la base de datos, puede escribir clases IQueryable usted mismo, es bastante difícil. Por lo tanto, podría escribir un contenedor IQueryable alrededor de una base de datos IQueryable y tener su límite IQueryable de acceso completo a la base de datos subyacente. Pero esto es lo suficientemente difícil como para no esperar que se haga ampliamente.
psr
2
Tenga en cuenta que incluso cuando no devuelva IQueryables, puede evitar tener que hacer muchos métodos (GetAllActiveItems, GetAllNonActiveItems, etc.) simplemente pasando un Expression<Func<Foo,bool>>método a su método. Estos parámetros se pueden pasar Wheredirectamente al método, lo que permite al consumidor elegir su filtro sin necesidad de acceder directamente al IQueryable <Foo>.
Flater
1
@hanzolo: Depende de lo que quieras decir con refactorización. Es cierto que cambiar el diseño (por ejemplo, agregar nuevos campos o cambiar las relaciones) es más difícil cuando tiene una base de código en capas y poco acoplada; pero la refactorización es menos dolorosa cuando solo necesita cambiar una capa. Todo depende de si espera rediseñar la aplicación o si simplemente está refactorizando su implementación de la misma arquitectura básica. Todavía recomendaría un acoplamiento flojo en cualquier caso, pero no está equivocado, ya que se agrega a la refactorización al cambiar los conceptos de dominio central.
Flater
29

Exponer IQueryable a interfaces públicas no es una buena práctica. La razón es fundamental: no puede proporcionar la realización IQueryable como se dice que es.

La interfaz pública es un contrato entre el proveedor y los clientes. Y en la mayoría de los casos hay una expectativa de implementación completa. IQueryable es un truco:

  1. IQueryable es casi imposible de implementar. Y actualmente no hay una solución adecuada. Incluso Entity Framework tiene muchas excepciones no admitidas .
  2. Hoy, los proveedores queryable tienen una gran dependencia de la infraestructura. Sharepoint tiene su propia implementación parcial y mi proveedor de SQL tiene una implementación parcial distinta.

El patrón de repositorio nos brinda una representación clara por capas y, lo más importante, independencia de realización. Entonces podemos cambiar en la fuente de datos futura.

La pregunta es " ¿Debería haber posibilidad de sustitución de la fuente de datos? ".

Si mañana parte de los datos se pueden mover a los servicios de SAP, a la base de datos NoSQL, o simplemente a los archivos de texto, ¿podemos garantizar la implementación adecuada de la interfaz IQueryable?

( más puntos buenos acerca de por qué esta es una mala práctica)

Artru
fuente
Esta respuesta no se sostiene por sí sola sin consultar el enlace externo volátil. Considere resumir al menos los puntos clave hechos por el recurso externo e intente mantener la opinión personal fuera de su respuesta ("la peor idea que he escuchado"). También considere eliminar el fragmento de código que parece no estar relacionado con nada de IQueryable.
Lars Viklund
1
Gracias @LarsViklund, la respuesta se actualiza con ejemplos y descripción del problema
Artru
19

Siendo realistas, tiene tres alternativas si desea una ejecución diferida:

  • Hágalo de esta manera: exponga un IQueryable.
  • Implemente una estructura que exponga métodos específicos para filtros específicos o "preguntas". ( GetCustomersInAlaskaWithChildrenabajo)
  • Implemente una estructura que exponga una API de filtro / clasificación / página fuertemente tipada y construya IQueryableinternamente.

Prefiero el tercero (aunque también he ayudado a colegas a implementar el segundo), pero obviamente hay algo de configuración y mantenimiento involucrado. (T4 al rescate!)

Editar : para aclarar la confusión en torno a lo que estoy hablando, considere el siguiente ejemplo robado de IQueryable puede matar a su perro, robar a su esposa, matar su voluntad de vivir, etc.

En el escenario donde expondrías algo como esto:

public class CustomerRepo : IRepo 
{ 
     private DataContext ct; 
     public Customer GetCustomerById(int id) { ... } 
     public Customer[] GetCustomersInAlaskaWithChildren() { ... } 
} 

la API de la que estoy hablando le permitiría exponer un método que le permitiría expresar GetCustomersInAlaskaWithChildren(o cualquier otra combinación de criterios) de manera flexible, y el repositorio lo ejecutaría como IQueryable y le devolvería los resultados. Pero lo importante es que se ejecuta dentro de la capa del repositorio, por lo que aún aprovecha la ejecución diferida. Una vez que obtenga los resultados, aún puede LINQ-to-objects en él al contenido de su corazón.

Otra ventaja de un enfoque como este es que, como son clases POCO, este repositorio puede vivir detrás de un servicio web o WCF; puede estar expuesto a AJAX u otras personas que no saben lo primero sobre LINQ.

Galáctico vaquero
fuente
44
Entonces, ¿crearía una API de consulta para sustituir la API de consulta integrada de .NET?
Michael Brown
44
Me parece bastante inútil
ozz
77
@MikeBrown El punto de esta pregunta, y mi respuesta, es que desea exponer el comportamiento o las ventajas de IQueryable sin exponerse a IQueryablesí mismo . ¿Cómo vas a hacer eso con la API incorporada?
GalacticCowboy
2
Esa es una muy buena tautología. No has explicado por qué quieres evitar eso. Heck WCF Data Services expone IQueryable a través del cable. No estoy tratando de molestarte, simplemente no entiendo esta aversión a la gente que parece cuestionable.
Michael Brown
2
Si solo vas a usar un IQueryable, ¿por qué usarías un repositorio? Los repositorios están destinados a ocultar detalles de implementación. (Además, WCF Data Services es bastante más nuevo que la mayoría de las soluciones en las que he trabajado en los últimos 4 años que usaban un repositorio como este ... Por lo tanto, no es probable que se actualicen pronto para hacer uso de un juguete nuevo y brillante.)
GalacticCowboy
8

Realmente solo hay una respuesta legítima: depende de cómo se use el repositorio.

En un extremo, su repositorio es un envoltorio muy delgado alrededor de un DBContext para que pueda inyectar una apariencia de capacidad de prueba alrededor de una aplicación basada en bases de datos. Realmente no existe una expectativa del mundo real de que esto se pueda usar de manera desconectada sin un DB compatible con LINQ detrás de él porque su aplicación CRUD nunca va a necesitar eso. Entonces claro, ¿por qué no usar IQueryable? Probablemente preferiría IEnumarable ya que obtienes la mayoría de los beneficios [léase: ejecución demorada] y no se siente tan sucio.

A menos que esté sentado en ese extremo, me esforzaría por aprovechar el espíritu del patrón de repositorio y devolver las colecciones materializadas apropiadas que no tienen una implicación de una conexión de base de datos subyacente.

Wyatt Barnett
fuente
66
Con respecto al regreso IEnumerable, es importante tener en cuenta que ((IEnumerable<T>)query).LastOrDefault()es muy diferente de query.LastOrDefault()(suponiendo que queryes un IQueryable). Por lo tanto, solo tiene sentido regresar IEnumerablesi vas a recorrer todos los elementos de todos modos.
Lou
7
 > Should Repositories return IQueryable?

NO si desea realizar un desarrollo basado en pruebas / pruebas unitarias porque no hay una manera fácil de crear un repositorio simulado para probar su lógica de negocios (= repositorio-consumidor) aislada de la base de datos u otro proveedor IQueryable.

k3b
fuente
6

Desde un punto de vista de arquitectura pura, IQueryable es una abstracción con fugas. Como generalidad, proporciona al usuario demasiada potencia. Dicho esto, hay lugares donde tiene sentido. Si está utilizando OData, IQueryable hace que sea extremadamente fácil proporcionar un punto final que sea fácilmente filtrable, ordenable, agrupable ... etc. De hecho, prefiero crear DTO en los que mis entidades se mapean y el IQueryable devuelve el DTO. De esa manera, prefiltro mis datos y realmente solo uso el método IQueryable para permitir la personalización de los resultados por parte del cliente.

Slick86
fuente
6

Me he enfrentado a esa elección también.

Entonces, resumamos los lados positivo y negativo:

Positivos:

  • Flexibilidad. Definitivamente es flexible y conveniente permitir que el cliente cree consultas personalizadas con un solo método de repositorio

Negativos:

  • No comprobable . (en realidad probará la implementación subyacente de IQueryable, que generalmente es su ORM. Pero en su lugar, debe probar su lógica)
  • Desenfoca la responsabilidad . Es imposible decir qué hace ese método particular de su repositorio. En realidad, es un método divino, que puede devolver cualquier cosa que desee en toda su base de datos.
  • Insegura . Sin restricciones sobre lo que se puede consultar con este método de repositorio, en algunos casos, tales implementaciones pueden ser inseguras desde el punto de seguridad.
  • Problemas de almacenamiento en caché (o, para ser genéricos, cualquier problema previo o posterior al procesamiento). Por lo general, no es aconsejable, por ejemplo, almacenar en caché todo en su aplicación. Intentará almacenar en caché algo que cause una carga significativa en su base de datos. ¿Pero cómo hacer eso, si solo tiene un método de repositorio, que los clientes utilizan para emitir prácticamente cualquier consulta en la base de datos?
  • Problemas de registro . Registrar algo en la capa del repositorio le hará inspeccionar IQueryable primero para verificar si esta llamada en particular se registra o no.

Recomendaría no usar este enfoque. Considere en su lugar crear varias Especificaciones e implementar métodos de repositorio, que puedan consultar la base de datos utilizando esos. El patrón de especificación es una excelente manera de encapsular un conjunto de condiciones.

Vladislav Rastrusny
fuente
5

Creo que exponer IQueryable en su repositorio es perfectamente aceptable durante las fases iniciales de desarrollo. Existen herramientas de interfaz de usuario que pueden funcionar con IQueryable directamente y manejar la clasificación, el filtrado, la agrupación, etc.

Dicho esto, creo que simplemente exponer la basura en el repositorio y llamarlo un día es irresponsable. Tener operaciones útiles y ayudantes de consultas para consultas comunes le permite centralizar la lógica de una consulta y al mismo tiempo exponer una superficie de interfaz más pequeña a los consumidores del repositorio.

Para las primeras iteraciones de un proyecto, exponer IQueryable públicamente en el repositorio permite un crecimiento más rápido. Antes de la primera versión completa, recomendaría hacer que la operación de consulta sea privada o protegida y que las consultas comodines estén bajo un mismo techo.

Michael Brown
fuente
4

Lo que he visto hacer (y lo que estoy implementando) es lo siguiente:

public interface IEntity<TKey>
{
    TKey Id { get; set; }
}

public interface IRepository<TEntity, in TKey> where TEntity : IEntity<TKey>
{
    void Create(TEntity entity);
    TEntity Get(TKey key);
    IEnumerable<TEntity> GetAll();
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> expression);
    void Update(TEntity entity);
    void Delete(TKey id);
}

Lo que esto le permite hacer es tener la flexibilidad de hacer una IQueryable.Where(a => a.SomeFilter)consulta en su repositorio concreteRepo.GetAll(a => a.SomeFilter)sin exponer ningún negocio LINQy.

dav_i
fuente