¿Ventajas de crear un repository genérico frente a un repository específico para cada objeto?

Estamos desarrollando una aplicación ASP.NET MVC, y ahora estamos creando las clases de repository / servicio. Me pregunto si hay ventajas importantes para crear una interfaz genérica de IRepository que implementen todos los repositorys, frente a cada repository que tiene su propia interfaz y conjunto de métodos únicos.

Por ejemplo: una interfaz genérica de IRepository podría parecerse (tomada de esta respuesta ):

public interface IRepository : IDisposable { T[] GetAll(); T[] GetAll(Expression<Func> filter); T GetSingle(Expression<Func> filter); T GetSingle(Expression<Func> filter, List<Expression<Func>> subSelectors); void Delete(T entity); void Add(T entity); int SaveChanges(); DbTransaction BeginTransaction(); } 

Cada repository implementaría esta interfaz, por ejemplo:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • etc.

La alternativa que hemos seguido en proyectos anteriores sería:

 public interface IInvoiceRepository : IDisposable { EntityCollection GetAllInvoices(int accountId); EntityCollection GetAllInvoices(DateTime theDate); InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated); InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique InvoiceEntity CreateInvoice(); InvoiceLineEntity CreateInvoiceLine(); void SaveChanges(InvoiceEntity); //handles inserts or updates void DeleteInvoice(InvoiceEntity); void DeleteInvoiceLine(InvoiceLineEntity); } 

En el segundo caso, las expresiones (LINQ u otras) estarían contenidas por completo en la implementación del Repositorio, quienquiera que esté implementando el servicio solo necesita saber a qué función del repository llamar.

Supongo que no veo la ventaja de escribir toda la syntax de expresión en la clase de servicio y pasar al repository. ¿No significaría esto que el código LINQ fácil de destruir se está duplicando en muchos casos?

Por ejemplo, en nuestro antiguo sistema de facturación, llamamos

 InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId) 

de algunos servicios diferentes (Cliente, Factura, Cuenta, etc.). Eso parece mucho más limpio que escribir lo siguiente en varios lugares:

 rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date); 

La única desventaja que veo al usar el enfoque específico es que podríamos terminar con muchas permutaciones de funciones Get *, pero esto todavía parece preferible a empujar la lógica de expresión hacia las clases de Servicio.

¿Qué me estoy perdiendo?

Este es un problema tan antiguo como el propio Repositorio. La reciente introducción de IQueryable de LINQ, una representación uniforme de una consulta, ha provocado mucha discusión sobre este tema.

Prefiero repositorys específicos, después de haber trabajado muy duro para construir un marco de repository genérico. No importa qué mecanismo inteligente probé, siempre terminé en el mismo problema: un repository es una parte del dominio que se está modelando, y ese dominio no es genérico. No se pueden eliminar todas las entidades, no se pueden agregar todas las entidades, no todas las entidades tienen un repository. Las consultas varían ampliamente; la API del repository se vuelve tan única como la propia entidad.

Un patrón que uso a menudo es tener interfaces de repository específicas, pero una clase base para las implementaciones. Por ejemplo, usando LINQ to SQL, podrías hacer:

 public abstract class Repository { private DataContext _dataContext; protected Repository(DataContext dataContext) { _dataContext = dataContext; } protected IQueryable Query { get { return _dataContext.GetTable(); } } protected void InsertOnCommit(TEntity entity) { _dataContext.GetTable().InsertOnCommit(entity); } protected void DeleteOnCommit(TEntity entity) { _dataContext.GetTable().DeleteOnCommit(entity); } } 

Reemplace DataContext con su unidad de trabajo de su elección. Un ejemplo de implementación podría ser:

 public interface IUserRepository { User GetById(int id); IQueryable GetLockedOutUsers(); void Insert(User user); } public class UserRepository : Repository, IUserRepository { public UserRepository(DataContext dataContext) : base(dataContext) {} public User GetById(int id) { return Query.Where(user => user.Id == id).SingleOrDefault(); } public IQueryable GetLockedOutUsers() { return Query.Where(user => user.IsLockedOut); } public void Insert(User user) { InsertOnCommit(user); } } 

Tenga en cuenta que la API pública del repository no permite que los usuarios sean eliminados. Además, exponer IQueryable es una lata de gusanos: hay tantas opiniones como botones de ombligo sobre ese tema.

De hecho estoy en desacuerdo con la publicación de Bryan. Creo que tiene razón, que finalmente todo es único y demás. Pero, al mismo tiempo, la mayor parte de eso aparece cuando diseñas, y creo que al obtener un repository genérico y usarlo mientras desarrollé mi modelo, puedo obtener una aplicación rápidamente, luego refactorizar a una mayor especificidad cuando encuentro el necesito hacerlo

Entonces, en casos como ese, a menudo he creado un IRepository genérico que tiene la stack CRUD completa, y eso me permite jugar rápidamente con la API y dejar que la gente juegue con la interfaz de usuario y realizar pruebas de integración y aceptación del usuario en paralelo. Luego, cuando encuentro que necesito consultas específicas en el repository, etc., comienzo a reemplazar esa dependencia con la específica, si es necesario, y desde allí. Una impl subyacente. es fácil de crear y utilizar (y posiblemente enganchar a un archivo en memoria db u objetos estáticos o objetos burlados o lo que sea).

Dicho eso, lo que comencé últimamente es romper el comportamiento. Entonces, si hace interfaces para IDataFetcher, IDataUpdater, IDataInserter e IDataDeleter (por ejemplo) puede mezclar y combinar para definir sus requisitos a través de la interfaz y luego tener implementaciones que se encargan de algunos o todos ellos, y yo puedo Todavía inyecte la implementación de “hágalo todo” para usar mientras estoy desarrollando la aplicación.

Pablo

Prefiero los repositorys específicos que se derivan del repository genérico (o la lista de repositorys generics para especificar el comportamiento exacto) con firmas de métodos invalidables.

Tener un repository genérico que esté envuelto por un repository específico. De esta forma puede controlar la interfaz pública pero aún así tener la ventaja de la reutilización de código que proviene de tener un repository genérico.

clase pública UserRepository: Repository, IUserRepository

¿No debería inyectar IUserRepository para evitar exponer la interfaz? Como han dicho las personas, es posible que no necesites la stack CRUD completa, etc.