Repository
Ця стаття має кілька недоліків. Будь ласка, допоможіть удосконалити її або обговоріть ці проблеми на сторінці обговорення.
|
Repository — патерн, який розділяє рівні джерела даних і логіки програми. Часто використовується із патерном Unit Of Work
Цей розділ має вигляд переліку, який краще подати прозою. (15 червня 2019) |
- Використовується, як колекція
- Інкапсулює великі запити до БД в методи
- Рівень абстракції між Business Logic рівнем та Data Access рівнем
- Ізолює програму від змін джерела даних
- Джерело даних може бути змінено без будь-яких змін в бізнес логіці і з мінімальними змінами в Репозиторії
- Полегшує автоматизоване юніт тестування, Test Driven Development
- Легко створювати mock репозиторію
- Зростає кількість класів
- Погіршує продуктивність
- Обмежує у використанні особливостей ОРМ фреймворку
Використаємо 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>
{
. . .
}
- Unit Of Work та Repository часто використовують в парі.
Цей розділ статті ще не написано. (15 червня 2019) |
- Implementing the repository and unit of work patterns [Архівовано 14 вересня 2020 у Wayback Machine.]
- Repository and unit of work pattern [Архівовано 10 серпня 2020 у Wayback Machine.]
- Common mistakes with the repository pattern [Архівовано 13 червня 2019 у Wayback Machine.]