Repository — патерн, який розділяє рівні джерела даних і логіки програми. Часто використовується із патерном Unit Of Work
Використаємо Entity Framework. Нехай дано клас-сутність User
public class User { public int Id { get; set; } public string Name { get; set; } }
Тепер напишемо інтерфейс репозиторію. Варто зазначити, що репозиторій є колекцією, і має поводитись як колекція, та не містити методів Update(), Save() тощо. Також однією із великих помилок, є те що методи повертають IQueryable замість IEnumerable. Якщо повертати IQueryable це дозволить надбудувати над запитом, ще запити, що не є вірним, оскільки мета цього патерну якраз і є уникнення великих запитів. В такому разі, краще написати ще один метод, який буде виконувати більший запит.
public interface IRepository<TEntity> where TEntity : class { int Count(); int Count(Expression<Func<TEntity, bool>> predicate); IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "", int? page = null, int? amount = null); TEntity Get(int id); void Insert(TEntity entity); void Delete(object id); void Delete(TEntity entityToDelete); void Delete(Expression<Func<TEntity, bool>> predicate); }
Тепер реалізуємо цей інтерфейс у вигляді узагальненого класу. При реалізації ми повертаємо сутність, а не DTO. Мапування — це не відповідальність репозиторію.
public class GenericRepository<TEntity> : IRepository<TEntity> where TEntity : class { // FIELDS // узагальнений контекст protected DbContext dbContext; protected DbSet<TEntity> dbSet; // CONSTRUCTORS public GenericRepository(DbContext dbContext) { this.dbContext = dbContext; this.dbSet = dbContext.Set<TEntity>(); } // METHODS public virtual int Count() { return dbSet.Count(); } public virtual int Count(Expression<Func<TEntity, bool>> predicate) { return dbSet.Count(predicate); } public virtual IEnumerable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "", int? page = null, int? amount = null) { // filter IQueryable<TEntity> query = dbSet; if (filter != null) { query = query.Where(filter); } // include properties foreach (string includeProperty in includeProperties.Split(new char[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)) { query = query.Include(includeProperty); } // ordering if (orderBy != null) query = orderBy(query); // paging if (page.HasValue && amount.HasValue) query = query.Skip((page.Value - 1) * amount.Value).Take(amount.Value); return query; } public virtual TEntity Get(int id) { return dbSet.Find(id); } public virtual void Insert(TEntity entity) { dbSet.Add(entity); } public virtual void Delete(object id) { // find if (id == null) throw new ArgumentNullException(nameof(id)); TEntity entityToDelete = dbSet.Find(id); // delete finded if (entityToDelete == null) throw new InvalidOperationException("There is no records with such id"); Delete(entityToDelete); } public virtual void Delete(TEntity entityToDelete) { if (entityToDelete == null) throw new ArgumentNullException(nameof(entityToDelete)); if (dbContext.Entry(entityToDelete).State == EntityState.Detached) { dbSet.Attach(entityToDelete); } dbSet.Remove(entityToDelete); } public virtual void Delete(Expression<Func<TEntity, bool>> predicate) { if (predicate != null) dbSet.RemoveRange(dbSet.Where(predicate)); else dbSet.RemoveRange(dbSet); } }
Тепер залишилось для кожної сутності реалізувати свій репозиторій. Напишемо інтерфейс, який додаватиме (а можливо і ні) новий функціонал для конкретного репозиторію.
public interface IUserRepository: IRepository<User> { User GetByName(string name); }
Та конкретна реалізація:
public class UserRepository : GenericRepository<User>, IUserRepository { public UserRepository(DbContext dbContext) : base(dbContext) { } public User GetByName(string name) { return dbSet.First(u => u.Name == name); } }
При потребі варто також узагальнювати тип ключа:
public interface IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey> { . . . }