← Назад к вопросам

Generics: Реализация универсального Repository

2.2 Middle🔥 171 комментариев
#Основы C# и .NET

Условие

Создайте generic Repository с базовыми CRUD-операциями.

Требования:

  1. Интерфейс IRepository<T> с методами: GetById, GetAll, Add, Update, Delete
  2. Ограничение T: class, IEntity (где IEntity имеет свойство Id)
  3. Реализация через 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 управляет транзакциями