Какое ожидаемое поведение метода, создающего и сохраняющего записи в базу в C#
Что проверяют
Можете ли вы описать контракт метода: что он гарантирует на выходе, как ведёт себя при ошибке и повторном вызове. Следом часто спрашивают, какие тесты вы бы на него написали.
Как отвечать
Опишите поведение через гарантии:
- валидирует вход: некорректные данные отсекаются до обращения к базе, с понятной ошибкой или
Result, а неNullReferenceExceptionиз глубины; - сохраняет атомарно: запись либо целиком в базе, либо её нет. В EF Core один вызов
SaveChangesуже выполняется в транзакции, явнаяBeginTransactionнужна, только когда в операции несколькоSaveChangesили есть работа вне контекста; - возвращает результат: созданную сущность или её id, сгенерированный базой. После
SaveChangesEF Core сам проставит ключ в объект. Для веб-API это201 Createdс адресом ресурса; - обрабатывает конфликты: нарушение уникального индекса приходит как
DbUpdateException, его стоит превратить в бизнес-ошибку «уже существует», а не отдавать500; - асинхронен и отменяем: сигнатура вида
Task<Order> CreateAsync(..., CancellationToken ct), внутриSaveChangesAsync(ct); - идемпотентен, если нужно: повторный вызов с тем же ключом не создаёт дубль.
Про DbContext: в приложении с DI он живёт в scope запроса и внедряется через конструктор, а new DbContext() внутри метода ломает эту модель. Он не потокобезопасен, две параллельные операции на одном контексте дают исключение. AddAsync нужен только для генераторов значений вроде HiLo, в остальных случаях документация рекомендует обычный Add.
Тесты: запись в базе и ключ проставлен, дубль отклонён, при ошибке ничего не записалось. Гонять их лучше на настоящей базе через Testcontainers: провайдер InMemory не проверяет ни ограничения, ни транзакции.
Где спотыкаются
Ловят Exception и возвращают false. Вызывающий код не знает, что случилось.
Вызывают SaveChanges внутри цикла на каждую запись. Это отдельная транзакция и поход в базу на каждый элемент, а при сбое на середине половина данных уже сохранена.
Блокируют асинхронный код через .Result или .Wait(), получая голодание пула потоков, а в старом ASP.NET и десктопе дедлок.