- PVSM.RU - https://www.pvsm.ru -
Привет! Меня зовут Сергей Сорокин, я .NET-разработчик с 12-летним стажем. Занимаюсь бэкендом, архитектурой и высокими нагрузками.
Знаю, о чем вы подумали, прочитав заголовок: "О боже, еще одна ORM? В 2025 году? Зачем, если есть Dapper и EF Core?".
Я тоже так думал. Но когда ты работаешь в Enterprise-системах, где производительность критична, а база данных — это не просто хранилище, а мощный инструмент обработки данных, стандартные решения начинают показывать свои слабые места.
Сегодня я хочу рассказать о Visor — ORM, которую я создал, чтобы превратить работу с базой данных в вызов типизированного API, убрать оверхед рефлексии и решить извечную боль с передачей списков (TVP) в SQL Server. А заодно показать, как Source Generators позволяют писать код, который работает быстрее, чем то, что вы пишете руками.
Давайте сразу расставим точки над «i». Я не призываю переносить бизнес-логику в хранимые процедуры. Это плохая практика, которая ведет к боли при тестировании и масштабировании. Бизнес-логика должна жить в коде приложения.
Но есть нюанс.
Есть логика доступа к данным. Типовые, тяжелые, оптимизированные выборки. Групповые вставки. Агрегации. Зачем тянуть мегабайты сырых данных в приложение, чтобы отфильтровать их в памяти, если SQL Server сделает это за миллисекунды, имея под рукой индексы и статистику?
Я пришёл к концепции, где База данных выступает как сервис (API).
Endpoint — это хранимая процедура или функция.
Contract — это сигнатура процедуры и Table-Valued Parameters (TVP) или композитные типы (Postgres).
При таком подходе происходит четкое разделение ответственности:
DBA / SQL Developer отвечает за план запроса, индексы и целостность данных. Он предоставляет нам идеальный "черный ящик" — процедуру.
Backend Developer отвечает за бизнес-процессы, оркестрацию и API для фронтенда. Мы не думаем, как база достает пользователей по email. Мы просто вызываем метод GetUserByEmail.
Чтобы не превратить базу в клубок спагетти, я следую строгому правилу: Никакой вложенности. Процедуры должны быть одноуровневыми. Одна процедура не вызывает другую. Это спасает от каскадного рефакторинга и позволяет менять реализацию внутри процедуры, не ломая контракт с бэкендом.
Я искал инструмент, который сочетал бы удобство интерфейсов (как Refit для HTTP) и максимальную производительность (как ручной ADO.NET [1]).
Dapper — это стандарт скорости, но:
Runtime Reflection: Вся магия маппинга происходит в рантайме через IL Emit. Это «черный ящик». Вы не видите код маппинга, не можете его отладить. Ошибки (например, несовпадение типов) вылетают только при выполнении.
Table-Valued Parameters (TVP): Это главная боль. Чтобы передать список объектов в процедуру, в Dapper нужно либо создавать DataTable (что дико аллоцирует память и медленно), либо писать вручную кастомные классы-наследники IEnumerable<SqlDataRecord>. Это тонна бойлерплейта.
«Молчаливые» ошибки: Если вы переименовали колонку в базе, а в DTO забыли — Dapper часто просто оставит поле пустым (null или 0). В Enterprise это недопустимо. Мне нужен Strict Mapping — если колонки нет, приложение должно упасть с четкой ошибкой, а не работать с некорректными данными.
EF Core — отличный комбайн, но для работы с процедурами он избыточен.
Overhead: ChangeTracker, построение графов, Snapshots. В моих тестах на массовых операциях EF потреблял в 60 раз больше памяти, чем наше решение.
Второй сорт: Процедуры в EF всегда ощущаются как костыль сбоку от LINQ.
Так родился Visor. Главная идея — Source Generators. Я переношу всю работу по созданию SQL-команд, параметров и маппингу результатов с этапа выполнения (Runtime) на этап компиляции (Compile Time).
Вы просто описываете интерфейс, как будто это REST-клиент:
[Visor(VisorProvider.SqlServer)]
public interface IUserRepository
{
// Простой вызов
[Endpoint("sp_GetUserById")]
Task<UserDto> GetUserAsync(int id);
// Массовая вставка (TVP)
[Endpoint("sp_ImportUsers")]
Task ImportUsersAsync(List<UserItemDto> users);
}
Во время сборки Visor генерирует класс UserRepository, который реализует этот интерфейс.
Самое интересное — как мы реализовали передачу списков. Вместо создания DataTable (как это делают почти все), генератор создает код, который использует IEnumerable<SqlDataRecord> с yield return.
Что это дает? Мы стримим данные из вашего List<UserDto> напрямую в сетевой поток SQL Server. Без промежуточных буферов, без копирования массивов, без лишних аллокаций памяти.
Сгенерированный код выглядит примерно так (упрощенно):
private static IEnumerable<SqlDataRecord> MapToSqlDataRecord(IEnumerable<UserDto> rows)
{
var record = new SqlDataRecord(metadata);
foreach (var row in rows)
{
record.SetInt32(0, row.Id);
record.SetString(1, row.Name);
yield return record; // <--- Магия здесь
}
}
Я сравнил вставку 10 000 записей в MS SQL Server через процедуру с TVP. Соперники:
Visor (TVP Streaming)
EF Core 10 (Bulk Insert / AddRange)
Dapper (Стандартная вставка в цикле / Execute)
Результаты (BenchmarkDotNet):
|
Method |
Time (Mean) |
Memory Allocated |
GC Gen0/1/2 |
|
Visor (TVP) |
51.82 ms |
1.07 MB |
0 / 0 / 0 |
|
EF Core 10 |
517.73 ms |
65.04 MB |
8 / 3 / 1 |
|
Dapper |
43,069.73 ms |
15.34 MB |
1 / 0 / 0 |
Выводы:
Visor быстрее EF Core в 10 раз. И потребляет в 60 раз меньше памяти.
Visor быстрее Dapper (loop) в 800 раз. Конечно, Dapper можно ускорить, если вручную реализовать SqlDataRecord, но Visor делает это за вас автоматически.
Zero GC: Обратите внимание на колонку GC. Visor не создал ни одного мусорного объекта в поколениях 0/1/2.
Я создал инструмент не для того, чтобы «убить» EF Core или Dapper. Я создал его для конкретной ниши: High-Load Enterprise системы, где база данных используется на полную мощность через хранимые процедуры.
Что дает Visor:
Скорость: Работает на уровне ручного ADO.NET [1].
Надежность: Строгая типизация и валидация схемы на этапе компиляции.
Чистота: Ваш код не зависит от деталей реализации доступа к данным.
Мульти-провайдерность: Сейчас поддерживается MSSQL и PostgreSQL (да, там мы используем массивы и композитные типы, но API для разработчика остается тем же).
Проект полностью Open Source. Если вам близок подход «Database as an API» или вы просто хотите посмотреть, как работают Source Generators в .NET 10 — добро пожаловать в репозиторий.
Буду рад конструктивной критике и пул-реквестам!
Автор: AcheronSoft
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/437716
Ссылки в тексте:
[1] ADO.NET: http://ADO.NET
[2] AcheronSoft/Visor: A high-performance, source-generated ORM for .NET that treats your Database as an API. Type-safe access to Stored Procedures without runtime reflection.: https://github.com/AcheronSoft/Visor
[3] Источник: https://habr.com/ru/articles/971758/?utm_source=habrahabr&utm_medium=rss&utm_campaign=971758
Нажмите здесь для печати.