Приведи пример совмещения сгенерированных CRUD-операций и кастомной бизнес-логики в приложении

«Приведи пример совмещения сгенерированных CRUD-операций и кастомной бизнес-логики в приложении» — вопрос из категории Архитектура, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Распространенный подход — использование шаблона Repository для базового CRUD и выделение Service Layer для сложной бизнес-логики. CRUD-операции могут быть сгенерированы инструментами (например, шаблонами Scaffold в ASP.NET Core).

1. Уровень данных (Repository / Generic Repository) - Сгенерированный/стандартный CRUD:

// Обобщенный репозиторий (может быть сгенерирован)
public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity); // Обычно не async, т.к. только меняет состояние в контексте
    Task DeleteAsync(int id);
    // ... другие базовые методы
}

public class EfRepository<T> : IRepository<T> where T : class
{
    private readonly AppDbContext _context;
    private readonly DbSet<T> _dbSet;

    public EfRepository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
    public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
    public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
    public void Update(T entity) => _dbSet.Update(entity);
    public async Task DeleteAsync(int id)
    {
        var entity = await GetByIdAsync(id);
        if (entity != null) _dbSet.Remove(entity);
    }
    // SaveChanges обычно вызывается на уровне Unit of Work или сервиса
}

2. Уровень бизнес-логики (Service) - Кастомная логика:

// Сервис, который использует репозиторий и добавляет бизнес-правила
public class ProductService
{
    private readonly IRepository<Product> _productRepository;
    private readonly IEmailService _emailService; // Пример другой зависимости

    public ProductService(IRepository<Product> productRepository, IEmailService emailService)
    {
        _productRepository = productRepository;
        _emailService = emailService;
    }

    // Кастомный метод: получение товаров со скидкой
    public async Task<IEnumerable<Product>> GetDiscountedProductsAsync()
    {
        var allProducts = await _productRepository.GetAllAsync();
        // Бизнес-правило: скидка применяется к товарам старше 30 дней
        return allProducts.Where(p => (DateTime.Now - p.CreatedDate).Days > 30)
                          .Select(p => new Product { 
                              Id = p.Id, 
                              Name = p.Name, 
                              Price = p.Price * 0.8m // 20% скидка
                          });
    }

    // Кастомный метод: размещение заказа с проверками и побочными эффектами
    public async Task PlaceOrderAsync(int productId, int quantity, string userId)
    {
        // 1. Использование CRUD: получить товар
        var product = await _productRepository.GetByIdAsync(productId);
        if (product == null) throw new ArgumentException("Товар не найден.");
        if (product.Stock < quantity) throw new InvalidOperationException("Недостаточно товара на складе.");

        // 2. Бизнес-логика: обновить остаток
        product.Stock -= quantity;
        _productRepository.Update(product); // Использование CRUD: обновление

        // 3. Создание сложной сущности (Order) - это тоже часть логики
        var order = new Order { ProductId = productId, UserId = userId, Quantity = quantity };
        // ... сохранение заказа через свой репозиторий IRepository<Order>

        // 4. Побочный эффект: уведомление
        if (product.Stock < 10) // Бизнес-правило: низкий запас
        {
            await _emailService.SendLowStockAlertAsync(product.Name, product.Stock);
        }
    }
}

Итог: CRUD-репозиторий предоставляет простой доступ к данным, а сервисный слой инкапсулирует как эти операции сочетаются для выполнения бизнес-требований, обеспечивая разделение ответственности.