He estado buscando durante bastante tiempo una buena solución a los problemas que presenta el patrón de repositorio típico (lista creciente de métodos para consultas especializadas, etc., consulte: http://ayende.com/blog/3955/repository- es-el-nuevo-singleton ).
Me gusta mucho la idea de utilizar consultas de comandos, especialmente mediante el uso del patrón de especificación. Sin embargo, mi problema con la especificación es que solo se relaciona con los criterios de selecciones simples (básicamente, la cláusula where), y no se ocupa de las otras cuestiones de las consultas, como unión, agrupación, selección o proyección de subconjuntos, etc. Básicamente, todos los aros adicionales por los que deben pasar muchas consultas para obtener el conjunto correcto de datos.
(nota: utilizo el término "comando" como en el patrón de comando, también conocido como objetos de consulta. No estoy hablando de comando como en la separación comando / consulta donde se hace una distinción entre consultas y comandos (actualizar, eliminar, insertar))
Así que estoy buscando alternativas que encapsulen toda la consulta, pero que sigan siendo lo suficientemente flexibles como para que no solo esté intercambiando repositorios de espaguetis por una explosión de clases de comando.
He usado, por ejemplo, Linqspecs, y aunque encuentro algo de valor en poder asignar nombres significativos a los criterios de selección, simplemente no es suficiente. Quizás estoy buscando una solución combinada que combine múltiples enfoques.
Estoy buscando soluciones que otros puedan haber desarrollado para abordar este problema o abordar un problema diferente, pero aún satisfacen estos requisitos. En el artículo vinculado, Ayende sugiere usar el contexto nHibernate directamente, pero creo que eso complica en gran medida su capa empresarial porque ahora también debe contener información de consulta.
Ofreceré una recompensa por esto, tan pronto como pase el período de espera. Por lo tanto, haga que sus soluciones sean dignas de recompensa, con buenas explicaciones, seleccionaré la mejor solución y votaré a los finalistas.
NOTA: Estoy buscando algo basado en ORM. No tiene que ser EF o nHibernate explícitamente, pero esos son los más comunes y encajarían mejor. Si se puede adaptar fácilmente a otros ORM, sería una ventaja. Linq compatible también sería bueno.
ACTUALIZACIÓN: Estoy realmente sorprendido de que no haya muchas buenas sugerencias aquí. Parece que las personas son totalmente CQRS o están completamente en el campo del repositorio. La mayoría de mis aplicaciones no son lo suficientemente complejas como para garantizar CQRS (algo que la mayoría de los defensores de CQRS dicen que no debes usarlo).
ACTUALIZACIÓN: Parece haber un poco de confusión aquí. No busco una nueva tecnología de acceso a datos, sino una interfaz razonablemente bien diseñada entre el negocio y los datos.
Idealmente, lo que estoy buscando es algún tipo de cruce entre los objetos de consulta, el patrón de especificación y el repositorio. Como dije anteriormente, el patrón de especificación solo se ocupa del aspecto de la cláusula where, y no de los otros aspectos de la consulta, como combinaciones, sub-selecciones, etc. Los repositorios se ocupan de toda la consulta, pero se salen de control después de un tiempo. . Los objetos de consulta también se ocupan de toda la consulta, pero no quiero simplemente reemplazar los repositorios con explosiones de objetos de consulta.
fuente

Respuestas:
Descargo de responsabilidad: dado que aún no hay buenas respuestas, decidí publicar una parte de una excelente publicación de blog que leí hace un tiempo, copiada casi literalmente. Puede encontrar la publicación completa del blog aquí . Asi que aqui esta:
Podemos definir las siguientes dos interfaces:
Los
IQuery<TResult>especifica un mensaje que define una consulta específica con los datos que devuelve usando elTResulttipo genérico. Con la interfaz definida previamente podemos definir un mensaje de consulta como este:Esta clase define una operación de consulta con dos parámetros, lo que dará como resultado una matriz de
Userobjetos. La clase que maneja este mensaje se puede definir de la siguiente manera:Ahora podemos permitir que los consumidores dependan de la
IQueryHandlerinterfaz genérica :Inmediatamente este modelo nos da mucha flexibilidad, porque ahora podemos decidir qué inyectar en el
UserController. Podemos inyectar una implementación completamente diferente, o una que envuelva la implementación real, sin tener que hacer cambios enUserController(y en todos los demás consumidores de esa interfaz).La
IQuery<TResult>interfaz nos brinda soporte en tiempo de compilación al especificar o inyectarIQueryHandlersen nuestro código. Cuando cambiamos elFindUsersBySearchTextQueryregresarUserInfo[]lugar (mediante la implementaciónIQuery<UserInfo[]>), laUserControllerdejará de recopilar, ya que el tipo de restricción genérica sobreIQueryHandler<TQuery, TResult>no será capaz de asignarFindUsersBySearchTextQueryaUser[].IQueryHandlerSin embargo, inyectar la interfaz en un consumidor tiene algunos problemas menos obvios que aún deben abordarse. El número de dependencias de nuestros consumidores puede ser demasiado grande y puede llevar a una sobreinyección del constructor, cuando un constructor toma demasiados argumentos. El número de consultas que ejecuta una clase puede cambiar con frecuencia, lo que requeriría cambios constantes en el número de argumentos del constructor.Podemos solucionar el problema de tener que inyectar demasiados
IQueryHandlerscon una capa extra de abstracción. Creamos un mediador que se ubica entre los consumidores y los manejadores de consultas:El
IQueryProcessores una interfaz no genérico con un método genérico. Como puede ver en la definición de la interfaz,IQueryProcessordepende de laIQuery<TResult>interfaz. Esto nos permite tener soporte de tiempo de compilación en nuestros consumidores que dependen delIQueryProcessor. Reescribamos elUserControllerpara usar el nuevoIQueryProcessor:El
UserControllerahora depende de unIQueryProcessorque pueda manejar todas nuestras consultas. ElUserController'sSearchUsersmétodo llama alIQueryProcessor.Processmétodo que pasa en un objeto de consulta inicializado. Dado queFindUsersBySearchTextQueryimplementa laIQuery<User[]>interfaz, podemos pasarla alExecute<TResult>(IQuery<TResult> query)método genérico . Gracias a la inferencia de tipos de C #, el compilador puede determinar el tipo genérico y esto nos ahorra tener que indicar explícitamente el tipo.ProcessTambién se conoce el tipo de retorno del método.Ahora es responsabilidad de la implementación de
IQueryProcessorencontrar el derechoIQueryHandler. Esto requiere algo de escritura dinámica y, opcionalmente, el uso de un marco de inyección de dependencia, y todo se puede hacer con solo unas pocas líneas de código:La
QueryProcessorclase construye unIQueryHandler<TQuery, TResult>tipo específico basado en el tipo de instancia de consulta proporcionada. Este tipo se utiliza para pedir a la clase de contenedor proporcionada que obtenga una instancia de ese tipo. Desafortunadamente, necesitamos llamar alHandlemétodo usando la reflexión (usando la palabra clave dinámica C # 4.0 en este caso), porque en este punto es imposible lanzar la instancia del controlador, ya que elTQueryargumento genérico no está disponible en tiempo de compilación. Sin embargo, a menosHandleque se cambie el nombre del método o se obtengan otros argumentos, esta llamada nunca fallará y, si lo desea, es muy fácil escribir una prueba unitaria para esta clase. El uso de la reflexión producirá una ligera caída, pero no es nada de lo que preocuparse.Para responder a una de sus inquietudes:
Una consecuencia de usar este diseño es que habrá muchas clases pequeñas en el sistema, pero tener muchas clases pequeñas / enfocadas (con nombres claros) es algo bueno. Este enfoque es claramente mucho mejor que tener muchas sobrecargas con diferentes parámetros para el mismo método en un repositorio, ya que puede agruparlos en una clase de consulta. Por lo tanto, todavía obtiene muchas menos clases de consulta que métodos en un repositorio.
fuente
TResultparámetro genérico de laIQueryinterfaz no es útil. Sin embargo, en mi respuesta actualizada,TResultelProcessmétodo deIQueryProcessorpara resolver elIQueryHandleren tiempo de ejecución usa el parámetro .IQueryabley asegurándome de no enumerar la colección, luego desde elQueryHandleracabo de llamar / encadenar las consultas. Esto me dio la flexibilidad de realizar pruebas unitarias de mis consultas y encadenarlas. Tengo un servicio de aplicación además de miQueryHandler, y mi controlador está a cargo de hablar directamente con el servicio en lugar del controladorMi forma de lidiar con eso es en realidad simplista y agnóstica de ORM. Mi punto de vista para un repositorio es el siguiente: el trabajo del repositorio es proporcionar a la aplicación el modelo requerido para el contexto, por lo que la aplicación solo le pide al repositorio lo que quiere, pero no le dice cómo obtenerlo.
Proporciono al método de repositorio un Criterio (sí, estilo DDD), que será utilizado por el repositorio para crear la consulta (o lo que sea necesario, puede ser una solicitud de servicio web). Las uniones y grupos en mi humilde opinión son detalles de cómo, no el qué y un criterio debería ser solo la base para construir una cláusula de dónde.
Modelo = el objeto final o la estructura de datos que necesita la aplicación.
Probablemente pueda usar los criterios ORM (Nhibernate) directamente si lo desea. La implementación del repositorio debe saber cómo utilizar los Criterios con el almacenamiento subyacente o DAO.
No conozco su dominio y los requisitos del modelo, pero sería extraño si la mejor manera es que la aplicación cree la consulta. ¿El modelo cambia tanto que no se puede definir algo estable?
Esta solución claramente requiere un código adicional, pero no acopla el resto a un ORM o lo que sea que esté usando para acceder al almacenamiento. El repositorio hace su trabajo para actuar como una fachada y, en mi opinión, está limpio y el código de 'traducción de criterios' es reutilizable
fuente
Hice esto, apoyé esto y deshice esto.
El principal problema es este: no importa cómo lo hagas, la abstracción agregada no te otorga independencia. Se filtrará por definición. En esencia, está inventando una capa completa solo para hacer que su código se vea lindo ... pero no reduce el mantenimiento, mejora la legibilidad ni le otorga ningún tipo de agnosticismo en el modelo.
La parte divertida es que respondió su propia pregunta en respuesta a la respuesta de Olivier: "esto es esencialmente duplicar la funcionalidad de Linq sin todos los beneficios que obtiene de Linq".
Pregúntate: ¿cómo no podría ser?
fuente
Puede utilizar una interfaz fluida. La idea básica es que los métodos de una clase devuelven la instancia actual de esta misma clase después de haber realizado alguna acción. Esto le permite encadenar llamadas a métodos.
Al crear una jerarquía de clases adecuada, puede crear un flujo lógico de métodos accesibles.
Lo llamarías así
Solo puede crear una nueva instancia de
Query. Las otras clases tienen un constructor protegido. El objetivo de la jerarquía es "deshabilitar" los métodos. Por ejemplo, elGroupBymétodo devuelve aGroupedQuerywhich es la clase base deQueryy no tiene unWheremétodo (el método where está declarado enQuery). Por lo tanto, no es posible llamarWheredespuésGroupBy.Sin embargo, no es perfecto. Con esta jerarquía de clases, puede ocultar miembros sucesivamente, pero no mostrar nuevos. Por lo tanto,
Havinglanza una excepción cuando se llama antesGroupBy.Tenga en cuenta que es posible llamar
Wherevarias veces. Esto agrega nuevas condicionesANDa las condiciones existentes. Esto facilita la construcción de filtros mediante programación a partir de condiciones únicas. Lo mismo es posible conHaving.Los métodos que aceptan listas de campos tienen un parámetro
params string[] fields. Le permite pasar nombres de campo único o una matriz de cadenas.Las interfaces fluidas son muy flexibles y no requieren que crees muchas sobrecargas de métodos con diferentes combinaciones de parámetros. Mi ejemplo funciona con cadenas, sin embargo, el enfoque se puede extender a otros tipos. También puede declarar métodos predefinidos para casos especiales o métodos que acepten tipos personalizados. También puede agregar métodos como
ExecuteReaderoExceuteScalar<T>. Esto le permitiría definir consultas como estaIncluso los comandos SQL construidos de esta manera pueden tener parámetros de comando y así evitar problemas de inyección de SQL y al mismo tiempo permitir que el servidor de base de datos almacene en caché los comandos. Esto no es un reemplazo para un mapeador O / R, pero puede ayudar en situaciones en las que crearía los comandos usando una simple concatenación de cadenas de otro modo.
fuente