Почему открытие и закрытие подключения к базе данных должно быть корректно реализовано?
Что проверяют
Понимаете ли вы, что за подключением стоит пул, и что закрыть соединение в .NET означает вернуть его в пул, а не оборвать сетевую сессию. Отсюда становится ясно, почему утечка подключений не видна на отладке и проявляется только под нагрузкой.
Как отвечать
ADO.NET по умолчанию пулит соединения по строке подключения. Вызов Open() берёт готовое из пула, Close() или Dispose() возвращает его обратно. Если Dispose не вызван, объект занимает соединение до финализации сборщиком мусора, то есть неопределённо долго. Размер пула ограничен, по умолчанию сто подключений, и когда свободных нет, Open() ждёт, а потом падает по таймауту.
Практическая часть ответа:
await using var conn = new SqlConnection(cs);
await conn.OpenAsync(ct);- открывать как можно позже и закрывать как можно раньше, а не держать подключение на всё время обработки запроса;
- читатель данных держит соединение занятым, пока не закрыт, поэтому не возвращать его наружу из метода;
- транзакция без Dispose держит и подключение, и блокировки в базе;
- в EF Core за это отвечает контекст, и он тоже обязан быть короткоживущим, обычно на один запрос.
Где спотыкаются
Придумывают довод про безопасность: незакрытым подключением якобы воспользуется злоумышленник. Подключение живёт внутри процесса приложения и наружу не торчит, проблема тут ресурсная, а не про несанкционированный доступ.
Считают, что раз есть пул, закрывать необязательно, он сам разберётся. Пул и есть то, что ломается: он не знает, что объект забыт, он видит занятое соединение и честно ждёт возврата.
Путают симптом с причиной. Таймаут на открытии читают как «база не тянет» и поднимают размер пула или таймаут подключения, вместо того чтобы найти путь кода без Dispose. Смотреть надо на счётчики пула и на активные сессии со стороны базы.