El respositorio genérico.Un Derrochador en épocas de crisis.

Durante muchos días,semanas,meses e incluso años he visto la siguiente definición de un repositorio.

1. Interface IRepository.

   1: public interface IRepository<T> where T:class

   2: {

   3:    T Get(int Id);

   4:    T Insert(T Entity);

   5:    void Update(T Entity);

   6:    void Delete(T Entity);

   7:    IEnumerable<T> GetAll();

   8:  

   9:    IUnitOfWork UnifOfWork { get;}

  10: }

2. Interface IUnitOfWork

   1: public interface IUnitOfWork

   2: {

   3:     void Commit();

   4:     void RollBack();

   5: }

3. Interface IContext que implementamos en  nuestro contexto de Entity Framework, en ella simplemente voy a definir un método que me devuelva un DbSet<T>

   1: public interface IContext:IUnitOfWork

   2: {

   3:     DbSet<TEntity> CreateSet<TEntity>() where TEntity : class;

   4: }    

El objetivo de este post no es demostrar que evidentemente estamos ligados fuertemente a EF, desde el momento que utilizamos DbSet<T>, sino analizar la interface IRepository.

Para ello voy a crear una clase base o Layer Supertype donde voy a implementar la interface IRepository

   1: public class Repository<T> : IRepository<T> where T:class

   2: {

   3:     readonly IContext _Context;

   4:     readonly IUnitOfWork _UnitOfWork;

   5:     public Repository(IContext Context)

   6:     {

   7:         if (Context == null)

   8:             throw new NullReferenceException("El contexto no puede ser nulo");

   9:  

  10:         _Context = Context;

  11:         _UnitOfWork = Context;

  12:     }

  13:     public T Get(int Id)

  14:     {

  15:         return GetSet().Find(Id);

  16:     }

  17:  

  18:     public T Insert(T Entity)

  19:     {

  20:         throw new NotImplementedException();

  21:     }

  22:  

  23:     public void Update(T Entity)

  24:     {

  25:         throw new NotImplementedException();

  26:     }

  27:  

  28:     public void Delete(T Entity)

  29:     {

  30:         throw new NotImplementedException();

  31:     }

  32:  

  33:     public IEnumerable<T> GetAll()

  34:     {

  35:         return GetSet().AsEnumerable();

  36:     }

  37:  

  38:     public IUnitOfWork UnifOfWork

  39:     {

  40:         get { return _UnitOfWork; }

  41:     }

  42:     private DbSet<T> GetSet()

  43:     {

  44:         return _Context.CreateSet<T>();

  45:     }

  46: }

Como podéis observar los métodos Insert,Update y Delete no están implementados, puesto que como dije anteriormente no es objetivo de este post y aparte tampoco me apetece:).

Vamos a realizar un análisis de esto.

1. Estamos ligados a EF sí y no más comentarios:).

2.La interface IRepository<T> expone métodos que con total seguridad  no necesitemos? Sí.

Un ejemplo sencillo la entidad X no se  puede modificar ni eliminar. Para ello os recomiendo esta lectura que ayer concretamente twitee.

https://twitter.com/_PedroHurtado/status/210311759872000000

3. Si vemos estas pegas y lo seguimos utilizando, porque no le damos una vuelta de tuerca. Para ello os voy a exponer el siguiente requisito y lógicamente no voy a escribir todas las entidades necesarias sino quiero que os lo imaginéis:).

Un Cliente tiene una forma de pago asignada por defecto, para que cuando cree una nueva factura se la asigne a esta. Con no otro objetivo que ayudar al usuario y este lógicamente podrá cambiarla por cada operación, entendido no?

Tal y como hemos creado nuestro repositorio no nos queda otra opción que leer todos los datos del cliente y no son tres campos en la vida real. Para obtener el Id y la Descripción de la forma de pago tenemos que leer todos los datos del cliente y además hacer otra lectura para recuperar el nombre de la forma de pago. Luego nos quejamos que EF es lento o somos nosotros los que lo hacemos lento.

Porque no exponemos nuestro método Get con la siguiente firma

TResult Get<TResult>(int Id,Expression<Func<T,TResult>> Select);

Lo mismo ahora podemos implementar nuestro Get de esta forma

   1: public TResult Get<TResult>(int Id, Expression<Func<T, TResult>> Select)

   2: {

   3:     return GetSet()

   4:             .Where(c => c.Id == Id)

   5:             .Select(Select)

   6:             .FirstOrDefault();

   7: }

y definir un repositorio para la clase Cliente que nos devuelva bien la Entidad Cliente o parte de ella, queréis verlo?

Bueno pues escribimos las clases necesarias para ello y veréis:)

   1: public class Entity

   2: {

   3:    public int Id { get; set; }

   4: }

   5: public class Cliente:Entity

   6: {       

   7:    public string Nombre { get; set; }

   8:    public FormaPago FormaPago { get; set; }

   9: }

  10: public class FormaPago:Entity

  11: {

  12:    public string Nombre { get; set; }

  13: }

  14: public interface IRepositoryCliente : IRepository<Cliente>

  15: {

  16:  

  17: }

  18: public class RepositoryCliente : Repository<Cliente>, IRepositoryCliente

  19: {

  20:    public RepositoryCliente(IContext Context)

  21:        : base(Context)

  22:    {

  23:    }

  24: }

  25:  

  26: public class Contexto : DbContext, IContext

  27: {

  28:  

  29:    public DbSet<TEntity> CreateSet<TEntity>() where TEntity : class

  30:    {

  31:        throw new NotImplementedException();

  32:    }

  33:  

  34:    public void Commit()

  35:    {

  36:        throw new NotImplementedException();

  37:    }

  38:  

  39:    public void RollBack()

  40:    {

  41:        throw new NotImplementedException();

  42:    }

  43: }

Mirad lo que puedo hacer, ooooo!!!

   1: public class Test

   2: {

   3:     public Test()

   4:     {

   5:         using (Contexto ct = new Contexto())

   6:         {

   7:             var idCliente =10;

   8:             var repositorio = new RepositoryCliente(ct);

   9:             //Devolver un cliente

  10:             var cliente = repositorio.Get(idCliente, c => c);

  11:             //Devolver partes de un cliente

  12:             var formapago = repositorio.Get(idCliente, c => new { Id = c.FormaPago.Id, Nombre = c.FormaPago.Nombre });

  13:             

  14:         }

  15:     }

  16: }

Pero lo que más me gusta es esto.

Dibujo

Pedro la verdad que eres un sacrílego, no se cuantos principios  has roto y cuantos antipatrones has implementado, además un método que devuelve un Tipo Anónimo.

Yo me contesto. Si lo que tu me digas, pero yo necesito el Id y el Nombre de la Forma de Pago no doscientos campos que tiene el Cliente. Anda derrochador,malgastoso no vas a tener ni un duro cuando seas mayor:).

Conclusiones.

Porque no nos acostumbramos a leer exclusivamente lo que necesitamos y nos dejamos de películas, creo que la vida nos irá mejor. Una última reflexión, ¿me habré cargado AutoMapper?. Otro malgastoso, o estoy en el camino:).

18 comentarios en “El respositorio genérico.Un Derrochador en épocas de crisis.”

  1. Buen post! 😉

    Bueno, hay mucho que hablar sobre como y si implementar repositorios. Hay varias visiones, además de necesidades específicas.

    Pero ayende p.ej. hace tiempo que aboga por no implementar ninguna interfaz repositorio: http://ayende.com/blog/3955/repository-is-the-new-singleton
    En su lugar dice de implementar “proveedores de consultas”. Y sí, átate al ORM. Total, el ORM es una abstracción de la BBDD no? Para que quieres independizarte de él, abstraer una abstracción? Y más cuando por culpa de esta abstracción tu arquitectura te impide optimizar las consultas a la BBDD. KISS.

    Pero como digo, esa es solo una visión…

    Saludos!

  2. Hola Pedro,

    Buen post! pero me da un poco repelus lo de usar un tipo anónimo de retorno.

    A lo mejor con lo que digo estoy metiendo la pata hasta el fondo y lo digo de memoria sin probarlo (Si tengo un rato lo probaré) pero si tu repo expone un metodo que devuelva IQueryable con AsQueryable() y en tu servicio usas ese IQueryable para retornar una entidad que no sea anónima del tipo por ejemplo ClienteSimple que tenga esas 2 propiedades?

    Un saludo

  3. Hola Pedro,

    Me gusta la idea de sacar sólo lo que necesitas y no me pone nervioso el tipo anónimo (queda muy cerca la definición del tipo anónimo del sitio en que se usa y no devuelves ningún object por ahí, o sea que bien).

    Lo que no me convence es mezclar en el repositorio la consulta con proyección (lo que sería tu tipo anónimo) con la carga de entidades.

    El tema del repositorio da para mucho como dice Eduard. Hace un tiempo hice un repaso sobre varias alternativas por si te animas a echarles un ojo y el último caso que ponía me convence más, separando la parte de lectura de la parte más “transaccional”:

    http://blog.koalite.com/tag/repository/

    Un saludo,

    Juanma

  4. Si me permites la opinión Pedro, esto es tan recurrente y hay ya tantas opiniones como comentan Eduard y el amigo Juanma que poco más hay que decir, pero, como siempre “para un martillo no todo pueden ser clavos”, si asumes lo que dices entonces, en mi opinión, asumes que tienes un modelo anémico en el que las entidades son solo datos ,por eso te permite esas proyecciones, y por lo tanto, en mi opinión para este escenario, hasta me sobrearía el ORM..

    Saludos
    Unai

  5. Buenas.

    A mi me parece bien en “este caso concreto” devolver un tipo genérico.

    Estoy de acuerdo es que es una “buena práctica” evitar como norma general retornar un tipo genérico, porque es incómodo y tendente a fallos que el llamante tenga que saber cómo es ese tipo anónimo para poder usarlo con corrección, pero en esta implementación concreta es el mismo llamante el que dice cómo tiene que ser ese tipo que retorna, por lo cual considero que no estámos violando el espírito de la norma.

    Sobre implementaciones del patrón repositorios, opino (aunque es una obviedad) que lo importante es conocer el patrón y adaptarlo a la situación concreta de su uso. Cada implementación tendrá sus ventajas, y esta indudablemente la tiene en ciertos escenarios. Además tendremos al dba contento al traernos solo lo que necesitamos. xD

    Un saludo.

  6. @Unai, no veo la relación entre tener un modelo anémico y tener proyecciones para leer los datos.

    Puedes tener entidades llenas de lógica que, al persistirse en la base de datos, almacenan campos calculados que luego lees en proyecciones (ejemplo canónico, el total del pedido).

    Con NH serían propiedades de sólo lectura, es decir, se leen de la entidad y se guardan en la BD, pero no al revés. No sé si EF permite eso.

  7. Hola @Pedro,

    Muy buen post, me parece genial el cargador genérico que permite cargar la información que necesitas y no más.

    Respondiendo un poco a @Unai, no veo que el método que se plantea necesariamente haga que las entidades sean anémicas. Es posible que se pueda hacer un mal uso, pero no tiene por qué. Con un buen uso tienes una muy buena potencia.

    Por ejemplo, lo veo muy útil para cargar DTOs o datos concretos de la BD (siempre y cuando se necesite algo que esté dentro del flujo de la ejecución de servicios).
    Por ejemplo, se quiere materializar el nombre del cliente y la dirección en la factura (evitando cambios posteriores de la instancia de cliente)

    var factura = repositorioFactura.Create();
    var clienteDTO = repositorioCliente.Get(id, c => new { Id = c.Id, Nombre = c.Nombre, Direccion = C.Direccion });
    factura.ClienteNombre = clienteDTO.Nombre;
    factura.ClienteDirección = cliente.Direccion;

    u otro ejemplo, para la devolución de DTOs a la capa de presentación:

    var clienteDTO = repositorioCliente.Get(id, c => new ClienteDTO { Id = c.Id, Nombre = c.Nombre, Direccion = C.Direccion });
    return clienteDTO;

    Me parece una buena alternativa siempre y cuando se realice un buen uso evitando lo que comenta @Unai,

    Salu2,

    XaviPaper

  8. Juanma, XaviPaper, me gusta la discusión aunque estas en los comentarios no sean lo mejor…

    Juanma,XaviPaper:

    No me estoy metiendo con las proyecciones en si mismas, y parece que vais por ahí, por ejemplo Xavi en tu ejemplo haces una proyeccion de clientes para crearte un DTO y devolverlo a un usario, estupendo, estas haciendo solo lectura, lo que digo es que para eso yo nunca utilizaría un repo y toda ese sobrecoste de materializacion, si solamente quieres una consulta de lectura para que toda esa artillería???
    La segunda, hablar de repositorios como se comenta, no es tan facil cuando no es “solo lectura” como en el caso anterior, porque para empezar no todas las entidades tienen que tener su correspondiente repositorio, esa es una de las cosas que haces cuando defines tu modelo, a mayores de esto que es muy importante, otra elemento esencial es garantizar una consistencia entre las mismas, por ejemplo, si Pedido que es una entidades que tiene consistencia con sus lineas se consulta parcialmente estas rompiendo la consistencia y por lo tanto las reglas inherentes al mismo, por supuesto, y repito porque me pareció que ‘jugasteis’ un poco con mi comentario anterior, siempre y cuando tu único trabajo no sea leer, caso comentado en el punto anterior..

    No se si ha quedado claro mi punto de vista, pero en resumen, bien si las proyecciones son para query, áún asi podríamos debatir si la técnica/tecnología es la mejor pero no para tus procesos por lo anteriormente mencionado..

    Unai

  9. Hola @Unai,

    Totalmente de acuerdo con que para hacer proyección a DTO es mejor utilizar otra arquitectura, y también de acuerdo con el segundo punto sobre los agregados y consistencia, pero… no siempre la consistencia debe estar por encima del rendimiento… Depende. En escenarios controlados y donde la carga de entidades puede pesar mucho (por ejemplo con entidades con muchos campos, TEXTs, BLOBS, …) puede ser muy interesante.

    Aun así el primer ejemplo que he comentado con carga de sólo lectura sin proyección a DTO también es muy interesante. Por ejemplo, con algo similar a esto se puede conseguir la primary de una entidad para asignarla a la fk ni necesidad de cargarla toda.

    De todas formas entiendo y comparto lo que comentas, pero técnicas como esta pueden ser muy útiles en ciertos momentos.

    Saludos y me alegra tener estas conversaciones tan enriquecedoras.

    Xavi Paper

  10. Xavi, me alegra sabe que me he explicado un poco mejor, a mi también me parece enriquecedor el tema, la pena es que el mecanismo no es lo más agil posible :-), a ver si algun dia podemos hacerlo de otra forma..

    Unai

  11. Vale, ahora veo por donde vas, Unai. Es una lástima, pero estamos todos de acuerdo en casi todo, y así es más aburrido 🙂

    La única duda que me queda es cuanta diferencia de rendimiento hay entre usar la hidratación de objetos de un ORM frente a hidratarlos con cosas más ligeras, como Dapper (o DataReaders a pelo, me da igual).

    Seguro que gana Dapper, pero no sé si compensa la complicación de tener que usar una transacción compartida entre el ORM y el otro mecanismo (a menos que seas StackOverflow, claro).

    Juanma.

  12. Lo primero es ser educado y por eso os doy las gracias a todos por vuestros aportes.

    Lo que veo que no os puedo poner guindas que queréis pasteles:).

    @Juanma muy bueno tu aporte, la verdad que no lo había leído,pero a partir de ahora estaré más pendiente de lo que escribes.

    @Unay tu puedes escribir sin pedir permiso por lo menos en mi blog. Ahora fíjate como de un tema recurrente han surgido montones de buenas ideas. Eso es lo bueno de estás cosas y ayer tal como escribía sabía que tormenta se iba a formar:).

    @Eduard como siempre un buen aporte, aunque te digo que yo abstraigo, piensa que desde el DAO al DbContext tampoco han pasado tantos años, y lo que más me preocupa son los últimos a la velocidad que surgen. DataContext,ObjectContext,DbContext y creo que no hemos terminado con los Context:).

    @Jonathan buen comentario,como te dije coincido con lo que estás diciendo:)

    @Xavi eres todo un enamorado de la arquitectura, me alegra que veas productivas partes de mis locuras:)

    @Luis yo no quiero devolver un tipo anónimo, más bien resolverlo en el propio método y ahorrar gastos innecesarios, que tenemos la prima de riesgo muy alta:).

    Queréis mi opinión en una aplicación tipo ERP, veo como candidatos óptimos a implementar este método a “Clientes,Proveedores,Productos o Artículos” y consumirlos desde facturas,pedidos,albaranes etc. Ya os digo que desde mi experiencia en esos mundos infames se ahorra un buen puñado de bytes:).

    Y ahora como veo que queréis bizcocho que la guinda os ha sabido a poco. Os voy a pasar un link y quiero que comentemos un método.

    http://microsoftnlayerapp.codeplex.com/SourceControl/changeset/view/68836#1105602

    Concretamente el método “FindBankAccountActivities” alguien haría eso? recuperar la entidad BackAcount y devolver un dto de las actividades, perdón a quien le pueda ofender pero he visto aberraciones y esta es de las más grandes.

  13. Te refieres funcionalmente? por que en eso no me meto porque para eso es un ejemplo y seguramente funcionalmente hay mas cosas que esta o te refieres a la proyeccion de código?

    return account.BankAccountActivity
    .ProjectedAsCollection();

    Unai

  14. @Pedro, no he visto el código entero (codeplex es un poco inmanejable), pero a mi no me parece mal. Todo depende de lo que haya en BankAccountActivity y de dónde se esté usando ese método.

    Si la entidad es casi calcada al DTO (cosa que desgraciadamente ocurre muchas veces por aplicar DDD en escenarios CRUD) yo no me complicaría y lo haría así antes de meter más cosas
    (YAGNI). Ya habrá tiempo para optimizar la lectura o el change tracking si hace falta.

  15. Juanma, si, efectivamente si tus DTO son como las entidades algo no está funcionando bien, puesto que aunque sea una forma de desacoplar y ganar en frecuencia de cambio, no es tan ui driven como debería ser, y por desgracia, en este sencillo ejemplo se cae en eso, por variados y diversos motivos que no vienen a cuento, por ahora. Seguramente otra de las cosas que se podrá decir es por tener que accceder al root para ir a una de las entidades de ese agregado, pero claro, eso es norma del patrón que no creo que sea como para discutir aquí, se le podría decir a Evans la próxima vez que venga a españa. De todas formas, me gustaría ir a lo que dije anteriormente, si descontextualizas el código e intentas que esto sea un ejemplo para hacer solamente lectura entonces esta aplicación de estilo es incorrecta, seguro seguro, de igual forma que si intentas utilizar otro mecanismo y lo descontextualizas a reporting, master data etc también quedará fuera de lugar. La verdad es que tengo pendiente un post que nunca llego a escribir, sobre como muchas veces nos encabezonamos en utilizar las mismos estilos en toda una solución cuando en realidad, en una solución empresarial deberíanos de tener/juntar/usar/aprovechar varios estilos, ¿utilizaríamos ddd para master data services? utilizaríamos un orm para read-models? utilizaríamos cualquiera de las dos anteriores para reporting? utilizarías peta poco para tu core domain? que utilizarías para un EDA? y si haces SCADA?..

    Unai

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *