← Назад к вопросам
Generics: Реализация универсального Repository
2.2 Middle🔥 171 комментариев
#Основы C# и .NET
Условие
Создайте generic Repository с базовыми CRUD-операциями.
Требования:
- Интерфейс IRepository<T> с методами: GetById, GetAll, Add, Update, Delete
- Ограничение T: class, IEntity (где IEntity имеет свойство Id)
- Реализация через Entity Framework Core DbContext
Интерфейс:
public interface IEntity { int Id { get; set; } }
public interface IRepository<T> where T : class, IEntity { Task<T?> GetByIdAsync(int id); Task<IEnumerable<T>> GetAllAsync(); Task<T> AddAsync(T entity); Task UpdateAsync(T entity); Task DeleteAsync(int id); }
Дополнительно:
- Добавьте метод GetAsync с Expression<Func<T, bool>> predicate
- Реализуйте Unit of Work паттерн
Критерии оценки:
- Правильное использование generic constraints
- Понимание covariance/contravariance
- Корректная работа с async/await
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Generic Repository Pattern с Unit of Work
Базовые интерфейсы
// 1. Интерфейс для всех сущностей в системе
public interface IEntity
{
int Id { get; set; }
}
// 2. Основной interface репозитория
public interface IRepository<T> where T : class, IEntity
{
// CRUD операции
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
// Расширенные операции
Task<IEnumerable<T>> GetAsync(Expression<Func<T, bool>> predicate);
Task<T?> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);
Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null);
Task<bool> ExistsAsync(Expression<Func<T, bool>> predicate);
}
// 3. Unit of Work интерфейс
public interface IUnitOfWork : IAsyncDisposable
{
IRepository<TEntity> Repository<TEntity>() where TEntity : class, IEntity;
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
Реализация Generic Repository
public class Repository<T> : IRepository<T> where T : class, IEntity
{
protected readonly DbContext _dbContext;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_dbSet = dbContext.Set<T>();
}
// GetById — поиск по первичному ключу
public virtual async Task<T?> GetByIdAsync(int id)
{
if (id <= 0)
throw new ArgumentException("Id должен быть больше 0", nameof(id));
return await _dbSet.FindAsync(id);
}
// GetAll — получить все записи
public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
// Add — добавить новую сущность
public virtual async Task<T> AddAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
_dbSet.Add(entity);
await _dbContext.SaveChangesAsync();
return entity;
}
// Update — обновить сущность
public virtual async Task UpdateAsync(T entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
// Проверить что сущность отслеживается
if (_dbContext.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbContext.Entry(entity).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
}
// Delete — удалить сущность по Id
public virtual async Task DeleteAsync(int id)
{
if (id <= 0)
throw new ArgumentException("Id должен быть больше 0", nameof(id));
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbSet.Remove(entity);
await _dbContext.SaveChangesAsync();
}
}
// Get — поиск по условию (predicate)
public virtual async Task<IEnumerable<T>> GetAsync(
Expression<Func<T, bool>> predicate)
{
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
return await _dbSet.Where(predicate).ToListAsync();
}
// FirstOrDefault — первая сущность по условию
public virtual async Task<T?> FirstOrDefaultAsync(
Expression<Func<T, bool>> predicate)
{
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
return await _dbSet.FirstOrDefaultAsync(predicate);
}
// Count — количество сущностей
public virtual async Task<int> CountAsync(
Expression<Func<T, bool>>? predicate = null)
{
if (predicate == null)
return await _dbSet.CountAsync();
return await _dbSet.CountAsync(predicate);
}
// Exists — существует ли сущность
public virtual async Task<bool> ExistsAsync(
Expression<Func<T, bool>> predicate)
{
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
return await _dbSet.AnyAsync(predicate);
}
}
Специализированный Repository с доп. функциями
// Если нужны дополнительные возможности — наследуемся
public class AdvancedRepository<T> : Repository<T>
where T : class, IEntity
{
public AdvancedRepository(DbContext dbContext) : base(dbContext) { }
// Paginated queries
public async Task<(IEnumerable<T> Items, int Total)> GetPagedAsync(
Expression<Func<T, bool>>? predicate,
int page,
int pageSize)
{
if (page < 1 || pageSize < 1)
throw new ArgumentException("Page и PageSize должны быть > 0");
var query = predicate == null
? _dbSet.AsQueryable()
: _dbSet.Where(predicate);
var total = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return (items, total);
}
// Batch operations
public async Task AddRangeAsync(IEnumerable<T> entities)
{
if (entities == null || !entities.Any())
throw new ArgumentException("Коллекция не может быть пустой");
_dbSet.AddRange(entities);
await _dbContext.SaveChangesAsync();
}
public async Task DeleteRangeAsync(Expression<Func<T, bool>> predicate)
{
if (predicate == null)
throw new ArgumentNullException(nameof(predicate));
var entitiesToDelete = await _dbSet.Where(predicate).ToListAsync();
_dbSet.RemoveRange(entitiesToDelete);
await _dbContext.SaveChangesAsync();
}
// Include для eager loading
public IQueryable<T> IncludeNavigation(params Expression<Func<T, object>>[] navigationProperties)
{
IQueryable<T> query = _dbSet;
foreach (var navigationProperty in navigationProperties)
{
query = query.Include(navigationProperty);
}
return query;
}
}
Unit of Work реализация
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _dbContext;
private readonly Dictionary<Type, object> _repositories;
private IDbContextTransaction? _transaction;
public UnitOfWork(DbContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_repositories = new Dictionary<Type, object>();
}
// Получить репозиторий для типа T
public IRepository<TEntity> Repository<TEntity>()
where TEntity : class, IEntity
{
var type = typeof(TEntity);
if (!_repositories.ContainsKey(type))
{
var repositoryInstance = Activator.CreateInstance(
typeof(Repository<>).MakeGenericType(type),
_dbContext);
_repositories.Add(type, repositoryInstance!);
}
return (IRepository<TEntity>)_repositories[type];
}
// Сохранить все изменения
public async Task<int> SaveChangesAsync()
{
return await _dbContext.SaveChangesAsync();
}
// Транзакции
public async Task BeginTransactionAsync()
{
_transaction = await _dbContext.Database.BeginTransactionAsync();
}
public async Task CommitTransactionAsync()
{
try
{
await SaveChangesAsync();
await _transaction?.CommitAsync()!;
}
catch
{
await RollbackTransactionAsync();
throw;
}
finally
{
await _transaction?.DisposeAsync()!;
_transaction = null;
}
}
public async Task RollbackTransactionAsync()
{
try
{
await _transaction?.RollbackAsync()!;
}
finally
{
await _transaction?.DisposeAsync()!;
_transaction = null;
}
}
// Dispose
public async ValueTask DisposeAsync()
{
await _dbContext.DisposeAsync();
_repositories.Clear();
}
}
Пример сущности
public class User : IEntity
{
public int Id { get; set; }
public string Email { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
public class Product : IEntity
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
Использование в приложении
// Регистрация в DI
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddUnitOfWork(this IServiceCollection services)
{
services.AddScoped<IUnitOfWork, UnitOfWork>();
return services;
}
}
// В Startup.cs
services.AddDbContext<AppDbContext>();
services.AddUnitOfWork();
// В сервисе
public class UserService
{
private readonly IUnitOfWork _unitOfWork;
public UserService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
// Получить пользователя
public async Task<User?> GetUserAsync(int id)
{
return await _unitOfWork.Repository<User>().GetByIdAsync(id);
}
// Поиск с условием
public async Task<IEnumerable<User>> FindUsersByEmailAsync(string emailDomain)
{
return await _unitOfWork.Repository<User>()
.GetAsync(u => u.Email.EndsWith(emailDomain));
}
// Транзакция
public async Task TransferUserDataAsync(int fromUserId, int toUserId)
{
await _unitOfWork.BeginTransactionAsync();
try
{
var fromUser = await _unitOfWork.Repository<User>()
.GetByIdAsync(fromUserId);
var toUser = await _unitOfWork.Repository<User>()
.GetByIdAsync(toUserId);
// Обновить данные
toUser.Email = fromUser.Email;
await _unitOfWork.Repository<User>().UpdateAsync(toUser);
await _unitOfWork.CommitTransactionAsync();
}
catch
{
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
}
Понимание Generics Constraints
where T : class — T должен быть типом класса (не struct/int) where T : IEntity — T должен реализовывать интерфейс IEntity where T : class, IEntity — оба условия одновременно
Это гарантирует:
- T имеет свойство Id
- T может быть null
- T может быть использован с DbSet<T>
Преимущества решения
- DRY — CRUD логика написана один раз
- Type-safe — компилятор проверяет типы
- Testable — легко мокировать IRepository
- Extensible — можно создавать специализированные репозитории
- Transaction support — Unit of Work управляет транзакциями