Introducción al patrón Repositorio
El concepto de repositorio fue popularizado por Eric Evans en su obra fundamental Domain-Driven Design, donde se presenta como una abstracción para acceder a entidades del dominio sin exponer detalles de persistencia. Aunque el libro no proporciona implementaciones concretas, este patrón ha evolucionado en la práctica para adaptarse a distintos escenarios: tamaño del proyecto, necesidades de escalabilidad, concurrencia y mantenibilidad. A continuación, se exploran tres enfoques progresivos para diseñar repositorios eficientes.
- Implementación básica: Proyectos pequeños y corta vida útil
Para aplicaciones simples con bajo mantenimiento futuro, un diseño directo del repositorio puede ser suficiente. Se define una interfaz genérica que encapsula operaciones CRUD estándar:
public interface IGenericRepository<T> where T : class, new()
{
T Insert(T entity);
T Update(T entity);
T GetById(Guid id);
T FindOne(Expression<Func<T, bool>> filter);
int Delete(Guid id);
int BulkDelete(IEnumerable<Guid> ids);
List<T> GetAll();
List<T> GetPaginated(
Expression<Func<T, bool>> filter,
Expression<Func<T, object>> orderBy,
SortDirection direction,
int offset,
int limit,
out int totalCount);
}
La implementación utiliza Entity Framework con contexto por operación, garantizando simplicidad:
public class BasicRepository<T> : IGenericRepository<T> where T : class, new()
{
private readonly string _connectionString;
public BasicRepository(IDbConnectionFactory connectionFactory)
{
_connectionString = connectionFactory.GetConnectionString();
}
public T Insert(T entity)
{
using var context = new ApplicationDbContext(_connectionString);
context.Set<T>().Add(entity);
return context.SaveChanges() > 0 ? entity : null;
}
public T Update(T entity)
{
using var context = new ApplicationDbContext(_connectionString);
context.Entry(entity).State = EntityState.Modified;
return context.SaveChanges() > 0 ? entity : null;
}
public T GetById(Guid id)
{
using var context = new ApplicationDbContext(_connectionString);
return context.Set<T>().Find(id);
}
public int Delete(Guid id)
{
using var context = new ApplicationDbContext(_connectionString);
var entity = context.Set<T>().Find(id);
if (entity == null) throw new ArgumentException("Entidad no encontrada");
context.Entry(entity).State = EntityState.Deleted;
return context.SaveChanges();
}
// Resto de métodos similares...
}
Este enfoque es adecuado para prototipos o sistemas con poco crecimiento esperado. Sin embargo, viola el principio abierto/cerrado, ya que cualquier nueva funcionalidad requiere modificar la interfaz base.
- Diseño extensible: Proyectos medianos o grandes
Cuando se anticipan cambios frecuentes o lógica de negocio especializada, convieen extender el repositorio mediante herencia e interfaces específicas. El truco clave es marcar todos los métodos como virtual para permitir la sobreescritura:
public class ExtendableRepository<T> : IGenericRepository<T> where T : class, new()
{
protected ApplicationDbContext Context { get; }
public ExtendableRepository(ApplicationDbContext context)
{
Context = context;
}
public virtual T Insert(T entity)
{
Context.Set<T>().Add(entity);
Context.SaveChanges();
return entity;
}
public virtual T GetById(Guid id)
{
return Context.Set<T>().Find(id);
}
// Todos los demás métodos declarados como 'virtual'
}
Luego, para una entidad como User, se crea una interfaz específica:
public interface IUserRepository : IGenericRepository<User>
{
void BulkUpdateLastLogin(DateTime newDate);
Task<List<User>> FindInactiveUsersAsync(int daysThreshold);
}
Y su implementación hereda del repositorio genérico:
public class UserRepository : ExtendableRepository<User>, IUserRepository
{
public UserRepository(ApplicationDbContext context) : base(context) { }
public void BulkUpdateLastLogin(DateTime newDate)
{
var users = Context.Users.Where(u => u.Name == "zhang").ToList();
foreach (var user in users)
{
user.LastLogin = newDate;
Context.Entry(user).State = EntityState.Modified;
}
Context.SaveChanges();
}
public async Task<List<User>> FindInactiveUsersAsync(int daysThreshold)
{
var cutoff = DateTime.UtcNow.AddDays(-daysThreshold);
return await Context.Users.Where(u => u.LastLogin < cutoff).ToListAsync();
}
}
En el servicio de negocio:
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public void RegisterNewUser()
{
var user = new User { Id = Guid.NewGuid(), Name = "Alice" };
_userRepository.Insert(user);
}
public void RefreshOldSessions() => _userRepository.BulkUpdateLastLogin(DateTime.UtcNow);
}
Aunque aumenta la cantidad de archivos, este modelo mejora notablemente la cohesión y facilita pruebas unitarias.
- Alto rendimiento y concurrencia: Repositorio asíncrono + Unidad de Trabajo
En sistemas con alta demanda de escritura o acceso concurrente, es crítico usar programación asincrónica y agrupar operaciones en una única tranascción. Aquí entra en juego el patrón Unit of Work.
Primero, se redefine el repositorio con soporte asíncrono:
public interface IAsyncRepository<T> where T : class, IAggregateRoot
{
Task<T> GetByIdAsync(Guid id);
Task<T> FindSingleAsync(Expression<Func<T, bool>> predicate);
Task<IReadOnlyList<T>> FindAllAsync(Expression<Func<T, bool>> predicate);
Task<IReadOnlyList<T>> GetPagedAsync(
Expression<Func<T, bool>> filter,
Expression<Func<T, object>> sort,
SortDirection dir,
int page,
int size,
out int count);
void Add(T entity);
void Update(T entity);
void Remove(T entity);
}
El repositorio delega las operaciones de estado al contexto compartido:
public class AsyncRepository<T> : IAsyncRepository<T> where T : class, IAggregateRoot
{
protected readonly ApplicationDbContext DbContext;
public AsyncRepository(ApplicationDbContext dbContext)
{
DbContext = dbContext;
}
public async Task<T> GetByIdAsync(Guid id)
{
return await DbContext.Set<T>().FindAsync(id);
}
public void Add(T entity) => DbContext.Set<T>().Add(entity);
public void Update(T entity) => DbContext.Entry(entity).State = EntityState.Modified;
public void Remove(T entity) => DbContext.Entry(entity).State = EntityState.Deleted;
}
La unidad de trabajo coordina todas las operaciones pendientes:
public interface IUnitOfWork : IAsyncDisposable
{
IAsyncRepository<User> UserRepository { get; }
IAsyncRepository<Order> OrderRepository { get; }
Task SaveChangesAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
private IAsyncRepository<User> _userRepository;
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
}
public IAsyncRepository<User> UserRepository =>
_userRepository ??= new AsyncRepository<User>(_context);
public async Task SaveChangesAsync()
{
await _context.SaveChangesAsync();
}
public async ValueTask DisposeAsync()
{
await _context.DisposeAsync();
}
}
Esta arquitectura permite acumular múltiples operaciones y ejecutarlas en una sola ronda contra la base de datos, reduciendo latencia y mejorando el manejo de concurrencia con mecanismos como SaveChangesAsync() y reintento ante conflictos.