Создание CSV файлов в C#

Курс посвящён созданию и экспорту CSV-файлов в C# с учётом кодировок, разделителей и корректного форматирования данных. Вы изучите работу с потоками, обработку специальных символов и варианты генерации CSV для различных сценариев.

1. Формат CSV: структура, разделители, локали и кодировки

Формат CSV: структура, разделители, локали и кодировки

Зачем разбираться в формате CSV

CSV (Comma-Separated Values) часто называют простым форматом. На практике большинство ошибок при экспорте CSV в C# появляются не из-за записи файлов, а из-за деталей:

  • какой символ является разделителем
  • как экранировать кавычки и переносы строк внутри значения
  • как локаль влияет на числа и даты
  • в какой кодировке сохранить файл, чтобы его корректно открывали Excel и другие программы
  • Эта статья задаёт основу для следующих материалов курса: дальше мы будем писать CSV в C#, но сначала важно договориться о правилах.

    Что такое CSV

    CSV — это текстовый файл, где:

  • каждая строка соответствует одной записи (например, одному сотруднику)
  • в каждой строке значения полей разделены специальным символом (разделителем)
  • Наиболее известное описание формата — RFC 4180. Оно не покрывает все реальные вариации, но задаёт понятный базовый ориентир.

    Ссылка: RFC 4180 (CSV File Format)

    Структура CSV

    Строки и записи

    Обычно:

  • одна строка = одна запись
  • перенос строки отделяет записи
  • Практические нюансы:

  • Windows-программы часто ожидают перенос строк \r\n
  • Unix-системы используют \n
  • современные парсеры обычно понимают оба варианта
  • Заголовок (header)

    Часто первая строка содержит названия колонок:

  • Id,Name,Salary
  • Это не обязательная часть формата, но почти всегда полезная.

    Поля (values)

    Внутри строки значения идут последовательно и разделяются выбранным разделителем.

    Пример (разделитель запятая):

    Разделители: запятая, точка с запятой, таб

    Почему CSV не всегда про запятую

    Хотя в названии есть Comma, во многих локалях запятая используется как десятичный разделитель в числах. Поэтому, например, в русскоязычной среде распространён разделитель ;.

    Типичные варианты разделителя:

  • , запятая
  • ; точка с запятой
  • \t табуляция (часто называют TSV)
  • Как выбрать разделитель

    Выбор зависит от того, кто будет открывать файл:

  • если потребитель — ваша программа, выбирайте один формат и придерживайтесь его
  • если потребитель — Excel в разных странах, часто разумнее использовать ; для русской локали и , для англоязычной
  • если данные содержат много запятых и точек с запятой в тексте, иногда удобнее TSV
  • Встроенный в Windows разделитель списка

    На Windows есть настройка List separator (разделитель списка), которую Excel часто учитывает при открытии CSV.

    Вывод:

  • CSV может выглядеть одинаково, но открываться по-разному на разных компьютерах
  • в корпоративной среде это частая причина жалоб всё съехало по столбцам
  • Кавычки и экранирование

    Когда нужно заключать поле в кавычки

    Распространённое правило (в том числе из RFC 4180): поле нужно заключать в двойные кавычки "...", если внутри есть:

  • разделитель (например, запятая или ;)
  • перенос строки
  • двойная кавычка
  • Пример (разделитель запятая):

    Как экранировать двойную кавычку внутри значения

    В CSV двойная кавычка внутри кавычечного поля экранируется удвоением.

    Значение:

  • Он сказал "Привет"
  • В CSV внутри кавычек:

  • "Он сказал ""Привет"""
  • !Наглядно показывает, когда нужны кавычки и как удваивать кавычки внутри значения

    Частая ошибка

    Ошибка: всегда оборачивать все поля в кавычки без экранирования кавычек внутри.

    Правильно:

  • либо оборачивать выборочно по правилам
  • либо оборачивать всегда, но обязательно удваивать внутренние кавычки и корректно обрабатывать переносы строк
  • Локали: числа, даты, разделитель и предсказуемость

    Что такое локаль в контексте CSV

    Локаль (culture) влияет на то, как строкой представляются:

  • десятичные числа (например, 1.5 или 1,5)
  • даты (например, 02.03.2026 или 3/2/2026)
  • иногда разделитель списка (как это интерпретирует Excel)
  • В .NET это управляется CultureInfo.

    Ссылка: CultureInfo Class

    Почему это важно при экспорте

    Если вы используете ToString() без указания культуры, результат зависит от машины, где запущено приложение.

    Пример проблемы:

  • на сервере с en-US число станет 1234.56
  • у пользователя в ru-RU он ожидает 1234,56
  • В результате:

  • Excel может разделить число на два столбца (если разделитель ,)
  • или число будет воспринято как текст
  • Практическая стратегия

    Есть два распространённых подхода, и важно выбрать один.

    #### Подход для обмена между системами

  • используйте фиксированный разделитель (часто ,)
  • используйте CultureInfo.InvariantCulture для чисел и дат
  • делайте формат дат явным (например, ISO 8601)
  • Ссылка: CultureInfo.InvariantCulture Property

    #### Подход для людей и Excel в конкретной стране

  • подбирайте разделитель под целевую среду (часто ; для ru-RU)
  • форматируйте числа и даты по нужной культуре
  • Ограничение: такой CSV хуже подходит для межсистемного обмена, потому что он локалезависимый.

    Кодировки: UTF-8, BOM и совместимость

    Что такое кодировка

    Кодировка определяет, как символы (например, кириллица) представлены в байтах файла. Ошибка кодировки выглядит как кракозябры.

    Рекомендуемый выбор

    Для большинства современных сценариев:

  • используйте UTF-8
  • В .NET:

  • Encoding.UTF8
  • Ссылка: Encoding.UTF8 Property

    Excel и UTF-8

    Исторически Excel на Windows нередко открывал CSV в локальной ANSI-кодировке и мог не распознать UTF-8 без специальных подсказок. В современных версиях ситуация лучше, но в реальных организациях всё ещё встречаются проблемы.

    Практические варианты повышения совместимости:

  • сохранять в UTF-8 с BOM (метка порядка байтов) — многие версии Excel лучше распознают такой файл
  • или использовать импорт данных в Excel через мастер импорта, где можно выбрать кодировку
  • В .NET можно явно создать UTF-8 с BOM:

    Ссылка: UTF8Encoding Class

    Когда BOM может мешать

    Некоторые системы (особенно самописные парсеры) не ожидают BOM в начале файла и воспринимают его как часть первого поля.

    Вывод:

  • для Excel BOM часто помогает
  • для машинного обмена BOM иногда нежелателен
  • Минимальные примеры для C# (как будет в следующих статьях)

    Запись строк в файл с выбранной кодировкой

    Ссылка: StreamWriter Class

    Важная мысль перед реализацией

    Чтобы CSV открывался стабильно, вам нужно явно зафиксировать три решения:

  • разделитель
  • правила кавычек и экранирования
  • кодировку
  • И отдельно решить, как вы форматируете числа и даты:

  • по InvariantCulture (предсказуемо для систем)
  • или по конкретной культуре (удобно для людей в одной стране)
  • Краткий чеклист перед экспортом CSV

  • Определите целевого потребителя файла (система или человек).
  • Выберите разделитель: ,, ; или \t.
  • Реализуйте корректные кавычки: кавычить поля при необходимости и удваивать ".
  • Зафиксируйте правила форматирования чисел и дат (культура и форматы).
  • Выберите кодировку (обычно UTF-8) и решите, нужен ли BOM.
  • Что дальше по курсу

    В следующей статье мы начнём писать CSV в C# правильно: сделаем функцию, которая безопасно превращает значение в CSV-поле (с кавычками, переносами строк и удвоением кавычек), и соберём строки с нужным разделителем и кодировкой.

    2. Базовая запись CSV в файл: StreamWriter, File и управление ресурсами

    Базовая запись CSV в файл: StreamWriter, File и управление ресурсами

    Связь с предыдущей статьёй

    В предыдущей статье мы разобрали, почему CSV только кажется простым: разделители, кавычки, локали и кодировки напрямую влияют на то, как файл откроется в Excel и как его прочитает другая система.

    В этой статье мы сделаем следующий шаг: научимся надёжно записывать CSV-файл в C# с правильным управлением ресурсами и понятным выбором API.

    Цель статьи

    К концу статьи вы будете понимать:

  • чем отличаются File.WriteAllText и StreamWriter
  • как выбрать кодировку (включая UTF-8 с BOM)
  • зачем нужен using и что он гарантирует
  • как записать CSV построчно, не держа весь файл в памяти
  • > Важно: мы будем писать CSV технически корректно (с кодировкой, переводами строк и освобождением ресурсов). Полноценную универсальную функцию экранирования полей мы соберём в следующей статье, а здесь используем базовую реализацию, достаточную для практики.

    Два основных способа записать CSV в .NET

    В .NET чаще всего используют один из двух подходов:

  • File.WriteAllText или File.WriteAllLines для простых случаев
  • StreamWriter для контроля над записью и для больших файлов
  • Оба варианта корректны, но подходят для разных задач.

    Вариант A: File.WriteAllText и File.WriteAllLines

    File удобен, когда объём данных небольшой и вы готовы сначала собрать всё содержимое, а потом записать.

  • File.WriteAllText записывает одну строку целиком
  • File.WriteAllLines записывает набор строк
  • Документация: File

    Пример: записать готовый CSV-текст

    Пример: записать массив строк

    Когда этот вариант удобен

  • когда файл небольшой
  • когда проще собрать строки заранее
  • когда не нужен тонкий контроль над процессом записи
  • Ограничение

    Если данных много, вы либо держите большой массив строк в памяти, либо собираете гигантскую строку. Это лишняя нагрузка и риск замедления.

    Вариант B: StreamWriter для построчной записи

    StreamWriter позволяет писать файл постепенно: открыли поток, записали заголовок, затем строки по одной.

    Документация: StreamWriter

    Почему StreamWriter часто лучше для CSV

  • не нужно хранить весь CSV в памяти
  • проще писать данные из циклов, LINQ и источников данных (БД, API)
  • легче управлять переводами строк и кодировкой
  • Минимальный пример

    Кодировка при записи CSV

    CSV является текстовым форматом, поэтому кодировка определяет, будут ли корректно отображаться символы, например кириллица.

  • Encoding.UTF8 даёт UTF-8 без BOM
  • new UTF8Encoding(true) даёт UTF-8 с BOM
  • Документация: Encoding.UTF8, UTF8Encoding

    Практический выбор для Excel

    Во многих организациях до сих пор встречаются конфигурации Excel, где UTF-8 без BOM распознаётся хуже. Поэтому для файлов, которые будут открывать пользователи в Excel на Windows, часто выбирают UTF-8 с BOM.

    Таблица для ориентира:

    | Сценарий | Рекомендуемая кодировка | Почему | |---|---|---| | CSV для Excel (Windows) | UTF-8 с BOM | Часто лучше авто-распознавание кириллицы | | Машинный обмен между системами | UTF-8 без BOM | BOM иногда мешает самописным парсерам |

    Управление ресурсами: почему важен using

    Когда вы пишете файл через StreamWriter, внутри используется системный ресурс: файловый дескриптор.

    Если его не закрыть корректно:

  • файл может остаться заблокированным
  • часть данных может не успеть записаться из буфера
  • на долгоживущих приложениях будут накапливаться проблемы с ресурсами
  • using гарантирует вызов Dispose() у writer даже если внутри произойдёт ошибка.

    Документация: using statement

    !Схема показывает, что using закрывает файл и освобождает ресурсы даже при ошибках

    Два стиля using

    #### using как блок

    #### using var как декларация Этот стиль короче, а освобождение ресурса произойдёт в конце текущего блока кода.

    Асинхронная запись и await using

    Если вы используете асинхронные операции и объект поддерживает асинхронное освобождение ресурсов, применяется await using.

    Документация: IAsyncDisposable

    Для StreamWriter в современных версиях .NET обычно достаточно обычного using, но важно знать сам принцип, потому что в реальных проектах вы встретите типы, которые требуют await using.

    Практический пример: экспорт объектов в CSV построчно

    Ниже пример, который:

  • фиксирует разделитель ;
  • пишет UTF-8 с BOM
  • использует \r\n
  • делает базовое экранирование CSV-полей
  • Модель данных

    Базовая функция CSV-поля

    Эта функция реализует правило из предыдущей статьи: если внутри есть разделитель, кавычка или перенос строки, поле нужно обернуть в кавычки, а кавычки внутри удвоить.

    Запись файла через StreamWriter

    Типичные ошибки при записи CSV

  • Запись без using, из-за чего файл может остаться открытым или недозаписанным.
  • Использование ToString() без культуры для чисел и дат, из-за чего формат зависит от машины.
  • Запись кириллицы в кодировке по умолчанию, из-за чего в Excel появляются кракозябры.
  • Отсутствие экранирования кавычек и переносов строк внутри полей.
  • Смешивание разделителя и формата чисел, например , как разделитель колонок и , как десятичный разделитель.
  • Мини-чеклист перед тем как писать CSV в файл

  • Определите разделитель и используйте его везде.
  • Зафиксируйте кодировку и осознанно решите, нужен ли BOM.
  • Пишите через using, чтобы файл гарантированно закрывался.
  • Для больших объёмов данных используйте StreamWriter и построчную запись.
  • Не забывайте про экранирование значений и про культуру чисел и дат.
  • Что дальше

    В следующей статье мы сфокусируемся на правильной сборке строки CSV: сделаем более аккуратный и переиспользуемый код для преобразования разных типов (строки, числа, даты, null) в безопасные CSV-поля, чтобы экспорт был устойчивым в реальных данных.

    3. Экранирование значений: кавычки, разделители, переносы строк и null

    Экранирование значений: кавычки, разделители, переносы строк и null

    Как эта тема связана с предыдущими статьями

    В первой статье курса мы разобрали, что CSV зависит от деталей: разделителя, культуры чисел и дат, кодировки и правил кавычек. Во второй статье — как надёжно записать файл (кодировка, using, построчная запись через StreamWriter).

    Теперь соберём ключевой кирпич, без которого любой экспорт быстро ломается на реальных данных: правильное экранирование значений (ещё говорят преобразование значения в CSV-поле).

    Зачем нужно экранирование

    Если вы просто делаете string.Join(";", ...), то файл будет корректен только пока значения не содержат:

  • разделитель (например, ;)
  • двойную кавычку "
  • перенос строки \r или \n
  • null (пустое значение)
  • Эти случаи встречаются постоянно: комментарии, адреса, названия компаний, многострочные поля, данные из форм.

    Базовые правила CSV-поля

    Ориентир для большинства парсеров — RFC 4180 (CSV File Format).

    Когда поле нужно заключать в двойные кавычки

    Поле нужно заключать в "...", если внутри есть хотя бы один из символов:

  • текущий разделитель (например, ; или ,)
  • двойная кавычка "
  • перенос строки \r или \n
  • Пример с разделителем ;:

    Как экранировать двойные кавычки внутри значения

    В CSV двойная кавычка внутри кавычечного поля экранируется удвоением.

  • исходное значение: Он сказал "Привет"
  • CSV-поле: "Он сказал ""Привет"""
  • Что делать с переносами строк внутри значения

    Переносы строк допустимы, если поле заключено в двойные кавычки.

    Пример (внутри поля есть перевод строки):

    Практически важно:

  • при записи через StreamWriter.WriteLine вы формируете одну CSV-строку; перенос строки внутри значения должен остаться частью поля, а не разорвать запись
  • поэтому правило простое: если внутри значения есть \r или \n, поле обязательно кавычится
  • Пустое значение и null: это не одно и то же

    В C# null означает отсутствие значения. В CSV нет отдельного типа null, там всё — текст.

    Поэтому вам нужно заранее выбрать стратегию.

    Самая безопасная стратегия по умолчанию

  • null записывать как пустое поле (между разделителями ничего нет)
  • пустую строку "" записывать как пустое поле или как "" — оба варианта читаются многими парсерами как пустая строка, но различие может быть важно для некоторых систем
  • Пример:

    Интерпретация зависит от того, кто читает файл. Если вы контролируете чтение, можно договориться, что:

  • пустое поле = null
  • "" = пустая строка
  • Если не контролируете — чаще выбирают простое правило: и null, и пустую строку писать одинаково (пустым полем), чтобы не усложнять совместимость.

    Универсальная функция: строка -> CSV-поле

    Ниже функция, которая применяет правила:

  • null превращает в пустую строку
  • проверяет, нужно ли кавычить
  • удваивает "
  • при необходимости оборачивает в "..."
  • Это минимальная реализация, которую удобно переиспользовать в любом экспорте.

    !Схема показывает, когда поле нужно заключать в кавычки и как экранировать кавычки

    Преобразование разных типов: числа, даты, bool

    CSV хранит текст, поэтому любые типы вы превращаете в строки. Главная ошибка здесь — случайно зависеть от настроек компьютера.

    В прошлых статьях мы обсуждали культуру (CultureInfo) и кодировку. Здесь применим это на практике: форматируем типы предсказуемо, а потом прогоняем через ToCsvField.

    Документация: CultureInfo Class

    Рекомендуемая стратегия для обмена между системами

  • числа и даты форматировать с CultureInfo.InvariantCulture
  • даты писать в ISO-подобном формате, например yyyy-MM-dd или yyyy-MM-ddTHH:mm:ss
  • Практический пример: экспорт объектов с корректным экранированием

    Соберём небольшой, но практичный набор функций:

  • ToCsvField(string?) — экранирование
  • FormatValue(object?) — превращение разных типов в строку
  • запись строк через StreamWriter (то, что вы уже изучили)
  • Связанные API:

  • StreamWriter Class
  • UTF8Encoding Class
  • Разбор типичных случаев: как поле будет выглядеть в CSV

    Таблица ниже помогает быстро проверить себя.

    | Исходное значение | Почему это важно | Результат CSV-поля (разделитель ;) | |---|---|---| | Anna | нет спецсимволов | Anna | | A;B | содержит разделитель | "A;B" | | Он сказал "Привет" | содержит " | "Он сказал ""Привет""" | | Строка\n2 | содержит перенос строки | "Строка\n2" | | null | отсутствует значение | ` (пустое поле) |

    Ошибки, которые встречаются чаще всего

  • Экранировать " обратным слэшем (\") вместо удвоения: для CSV это обычно неверно.
  • Кавычить поле, но забывать удвоить внутренние кавычки: парсер «сломает» строку.
  • Не кавычить многострочные значения: запись разъедется по строкам.
  • Пытаться отличать null и пустую строку без договорённости с потребителем CSV.
  • Мини-чеклист для вашей функции экспорта

  • Решите, как вы кодируете null: пустое поле или специальный маркер.
  • Реализуйте правило кавычек: разделитель, ", \r или \n.
  • Реализуйте удвоение кавычек внутри кавычечного поля.
  • Форматируйте числа и даты предсказуемо (часто InvariantCulture).
  • После форматирования прогоняйте результат через функцию CSV-поля.
  • Что дальше по курсу

    Следующий шаг — сделать экспорт удобнее и безопаснее на уровне архитектуры: вынести настройки (разделитель, культура, кодировка, перенос строк, стратегия
    null) в отдельный объект и построить небольшой CSV-writer, чтобы в проекте не размножались ручные string.Join` и ошибки экранирования.

    4. Экспорт коллекций и объектов: LINQ, заголовки, порядок колонок

    Экспорт коллекций и объектов: LINQ, заголовки, порядок колонок

    Связь с предыдущими статьями

    В предыдущих материалах курса мы разобрали основу надёжного CSV-экспорта:

  • как устроен CSV: разделители, локали и кодировки
  • как правильно записывать файл через StreamWriter и using
  • как экранировать значения: кавычки, переносы строк, разделитель и null
  • Теперь соберём это в практику: научимся экспортировать коллекции объектов в CSV так, чтобы:

  • был заголовок (header)
  • порядок колонок был стабильным и предсказуемым
  • можно было легко выбирать и переименовывать колонки
  • можно было применять LINQ (фильтрацию, сортировку, проекции)
  • !Диаграмма показывает путь от объектов к CSV и место, где задаётся порядок колонок

    Главная идея: порядок колонок задаётся не объектом, а вашим описанием экспорта

    Если вы экспортируете объект целиком (например, через reflection), легко получить проблемы:

  • порядок свойств может оказаться неожиданным
  • появятся лишние поля, которые вы не хотели отдавать
  • трудно задать красивый заголовок (например, HireDate -> Дата найма)
  • Практичный подход: вы явно описываете колонки в нужном порядке.

    Минимальные строительные блоки

    Ниже мы используем 2 функции из прошлой статьи (в том же виде, только собранные вместе):

  • FormatValue превращает разные типы в строку предсказуемо (с учётом культуры)
  • ToCsvField экранирует строку по правилам CSV
  • Ссылки на базовые API:

  • StreamWriter
  • CultureInfo
  • Enumerable.Select
  • Модель данных для примеров

    Функции форматирования и экранирования

    Колонки как контракт экспорта

    Сделаем простой тип Column<T>:

  • Header отвечает за имя колонки в первой строке CSV
  • GetValue извлекает значение из объекта
  • Почему это удобно

  • порядок колонок равен порядку элементов в Column<T>[]
  • заголовки читаемые и стабильные
  • можно сделать вычисляемые поля (например, Salary * 12)
  • Универсальная функция экспорта коллекции объектов

    Функция ниже:

  • пишет заголовок
  • пишет строки по объектам
  • форматирует значения через FormatValue
  • экранирует через ToCsvField
  • не держит весь CSV в памяти (пишет построчно)
  • Обратите внимание на columns.Select(...). Это как раз место, где LINQ делает код компактнее и читабельнее.

    Ссылка на string.Join:

  • string.Join
  • Практический пример: стабильный порядок колонок и понятные заголовки

    Что гарантирует такой код

  • порядок колонок всегда: Id, Name, Salary, HireDate, Comment
  • заголовки не зависят от имён свойств в C#
  • переносы строк, кавычки и разделитель внутри комментария не ломают CSV
  • LINQ в экспорте: фильтрация, сортировка, проекция

    LINQ удобно использовать до записи в файл.

    Фильтрация и сортировка

    Ссылки:

  • Enumerable.Where
  • Enumerable.OrderBy
  • Вычисляемая колонка

    Например, годовая зарплата:

    CSV не знает типов, но decimal будет отформатирован через IFormattable внутри FormatValue.

    Заголовки: частые требования из реальных проектов

    Обычно требования выглядят так:

  • заголовки должны быть человекочитаемыми
  • заголовки должны быть на нужном языке
  • некоторые колонки нужно скрыть
  • С Column<T> это решается просто: меняете Header и список колонок.

    Пример: заголовки на русском и другой порядок

    Порядок колонок: почему не стоит полагаться на reflection по умолчанию

    Можно получить свойства типа через reflection, но есть риски:

  • порядок свойств не обязан совпадать с тем, как вы написали их в коде
  • легко случайно экспортировать лишнее (например, техническое поле)
  • производительность ниже, чем у прямых Func<T, object?>
  • Если reflection нужен (например, вы делаете универсальный экспорт для разных типов), задавайте порядок явно.

    Компромисс: reflection + список имён свойств в нужном порядке

    Идея: вы храните массив имён, и строите колонки в этом порядке.

    Ссылка:

  • Type.GetProperties
  • Этот вариант даёт контроль порядка, но всё ещё использует reflection при чтении значений.

    Важный нюанс: LINQ и память

    Select, Where, OrderBy сами по себе не записывают файл. В нашем WriteCsv запись идёт в foreach, поэтому:

  • файл пишется построчно
  • коллекция не обязана целиком помещаться в память
  • Исключение: OrderBy обычно требует буферизации, чтобы отсортировать элементы. Если данных очень много, сортировку лучше делать на уровне базы данных или источника данных.

    Типичные ошибки при экспорте объектов в CSV

  • Собирать порядок колонок «как получилось» вместо явного списка.
  • Писать заголовок отдельно, а порядок значений собирать иначе (получаются «перепутанные» колонки).
  • Использовать ToString() без культуры для чисел и дат.
  • Делать string.Join по сырым значениям без ToCsvField.
  • Сначала строить List<string> всех строк файла, а потом писать его целиком, хотя можно писать построчно.
  • Мини-чеклист

  • Опишите колонки явно (заголовок + извлечение значения) и в нужном порядке.
  • Применяйте LINQ для фильтрации и сортировки до записи.
  • Форматируйте значения предсказуемо (часто CultureInfo.InvariantCulture).
  • Всегда экранируйте поля через ToCsvField.
  • Пишите файл через StreamWriter и using.
  • Что дальше по курсу

    Следующий логичный шаг — сделать экспорт удобным и единообразным во всём проекте: вынести настройки (разделитель, кодировка, культура, перенос строк, стратегия null) в один объект и построить небольшой CSV writer, чтобы не копировать один и тот же код и не ошибаться в деталях.

    5. Продвинутые подходы: CsvHelper, большие файлы и тестирование экспорта

    Продвинутые подходы: CsvHelper, большие файлы и тестирование экспорта

    Как эта тема связана с предыдущими статьями

    Ранее в курсе мы научились делать CSV вручную и делать это правильно:

  • выбирать разделитель, кодировку и культуру
  • записывать файл через StreamWriter с корректным освобождением ресурсов
  • экранировать значения (кавычки, переносы строк, разделители, null)
  • экспортировать коллекции объектов со стабильным заголовком и порядком колонок
  • Эти навыки полезны всегда, потому что они объясняют, почему CSV ломается.

    В этой статье мы поднимемся уровнем выше и разберём:

  • когда выгодно использовать готовую библиотеку CsvHelper
  • как писать очень большие CSV-файлы без лишней памяти и с нормальной скоростью
  • как тестировать экспорт так, чтобы тесты были стабильными и реально ловили ошибки
  • !Общая картина: где CsvHelper находится между объектами и CSV-файлом

    Когда стоит использовать CsvHelper

    CsvHelper — популярная библиотека для чтения и записи CSV в .NET. Она берёт на себя большую часть рутинной логики:

  • корректное экранирование
  • заголовки
  • соответствие свойств объекта колонкам
  • форматирование типов (даты, числа, null) через настройки
  • Полезные источники:

  • CsvHelper на GitHub
  • Пакет CsvHelper на NuGet
  • Когда CsvHelper особенно выгоден

  • когда много разных CSV-экспортов, и вы не хотите копировать один и тот же код
  • когда нужны настройки формата в одном месте (разделитель, культура, null, заголовки)
  • когда хотите читать свой же CSV обратно (например, для проверки)
  • Когда ручной код тоже уместен

  • когда экспорт очень простой и строго фиксированный
  • когда вы хотите минимальные зависимости
  • когда вы пишете CSV в среде с особыми ограничениями
  • Важно: даже с CsvHelper вам всё равно нужно помнить базовые правила из прошлых статей: разделитель, культура, кодировка, переводы строк.

    Быстрый старт с CsvHelper: запись объектов в CSV

    Установка

    CsvHelper ставится как NuGet-пакет.

    Если вы работаете через .NET CLI:

    Документация по CLI:

  • dotnet add package
  • Минимальный пример записи

    Ниже пример, который:

  • пишет UTF-8 с BOM (часто удобно для Excel)
  • использует разделитель ;
  • пишет заголовок
  • пишет строки построчно
  • Ссылки на базовые API:

  • StreamWriter
  • UTF8Encoding
  • CultureInfo.InvariantCulture
  • Что важно в этом примере

  • CsvConfiguration(CultureInfo.InvariantCulture) помогает сделать числа и даты предсказуемыми.
  • Delimiter = ";" фиксирует разделитель явно (как мы делали в прошлых статьях).
  • NewLine = "\r\n" делает переводы строк предсказуемыми для Windows/Excel.
  • Контроль заголовков и порядка колонок через маппинг

    Одна из частых проблем CSV-экспорта — стабильный порядок колонок и человекочитаемые заголовки. В прошлой статье мы решали это типом Column<T>. В CsvHelper эту роль выполняет маппинг.

    Пример: ClassMap для точного порядка и названий

    Подключение маппинга при записи:

    Почему это полезно:

  • порядок колонок задаётся явно через Index(...)
  • заголовки можно переименовывать через Name(...)
  • экспорт не зависит от того, как устроен объект и в каком порядке идут свойства
  • Настройки формата: даты, null и особые значения

    CSV — текстовый формат, а значит вопрос всегда один: как именно превращать типы в текст.

    Практичные правила

  • числа и даты для обмена между системами обычно пишут с InvariantCulture
  • даты часто делают в стабильном формате
  • null чаще всего пишут как пустое поле
  • В CsvHelper это настраивается через конфигурацию и маппинг.

    Пример: для HireDate задать формат даты:

    Важно: формат yyyy-MM-dd удобен тем, что он однозначный и не зависит от страны.

    Большие файлы: как писать быстро и не съесть память

    Большой CSV — это не только про скорость, но и про то, как не держать всё в памяти.

    Главный принцип

    Не собирайте весь CSV в строку и не накапливайте все строки в List<string>. Пишите потоком:

  • записали заголовок
  • записали строку
  • записали следующую
  • Что мешает потоковой записи

  • ToList() до записи
  • сортировка OrderBy(...) по огромной коллекции в памяти
  • логика, которая сначала строит промежуточные строки для всех записей
  • > Если сортировка действительно нужна, лучше делать её там, где живут данные: например, в SQL-запросе, а не в памяти приложения.

    Как писать потоково через CsvHelper

    У WriteRecords(...) есть важное свойство: он пишет по мере перечисления IEnumerable<T>. Это означает, что вы можете передать генератор данных.

    Пример с yield return:

    Оптимизация записи на уровне потоков

    Для больших файлов часто полезно явно использовать FileStream и задать буфер.

    Ссылки:

  • FileStream
  • Практические замечания:

  • не вызывайте Flush() на каждой строке, это сильно замедляет запись
  • буфер в десятки килобайт обычно даёт более ровную производительность
  • Асинхронная запись

    Асинхронность полезна, когда вы:

  • пишете в сетевой поток
  • пишете в облачное хранилище
  • выгружаете по HTTP (например, в ответ контроллера)
  • Пример: запись из IAsyncEnumerable<T>.

    Ссылки:

  • IAsyncEnumerable<T>
  • FileOptions.Asynchronous
  • Тестирование CSV-экспорта: как ловить ошибки до пользователей

    CSV часто ломается на краевых случаях: кавычки внутри текста, переносы строк, разделители в значениях, null, культура чисел.

    Хороший тест экспорта должен быть:

  • детерминированным (одинаковым на любой машине)
  • маленьким и быстрым
  • чувствительным к изменениям формата
  • Что обязательно фиксировать в тестах

    | Что фиксируем | Почему это важно | Как фиксировать | |---|---|---| | Культура | числа и даты иначе форматируются на разных ПК | CultureInfo.InvariantCulture | | Переводы строк | \n и \r\n дают разные строки | NewLine = "\r\n" | | Разделитель | Excel и локали | Delimiter = ";" или "," | | Кодировка | особенно важно для реального файла | для строковых тестов обычно достаточно StringWriter |

    Юнит-тест: пишем CSV в память и сравниваем строку

    Вместо файлов удобно использовать StringWriter: это запись в память, без диска.

    Пример теста (подойдёт и для xUnit, и для NUnit, и для MSTest по смыслу):

    Что именно проверяет такой тест:

  • есть заголовок
  • порядок колонок не поменялся
  • decimal записался через точку (InvariantCulture)
  • null стал пустым полем
  • кавычки внутри значения корректно удвоены
  • Ссылки:

  • StringWriter
  • Тестирование через чтение назад

    Иногда строковое сравнение слишком строгое: меняется пробел, меняется формат даты, тесты начинают шуметь. Альтернатива: записали CSV, прочитали обратно и сравнили данные.

    Идея:

  • экспортируем CSV в StringWriter
  • читаем через CsvReader
  • сравниваем объекты или ключевые поля
  • Это часто удобнее, когда CSV большой и сложный, а вас интересует корректность данных.

    Ресурс:

  • Пример чтения и записи в CsvHelper (раздел Examples)
  • Какие тесты обычно нужны в реальном проекте

  • тест экранирования: значения с ;, ", \n, \r\n
  • тест культуры: числа и даты
  • тест стабильного порядка колонок
  • тест заголовков (что не поменялись случайно)
  • тест на null
  • Мини-чеклист продвинутого экспорта

  • Выберите подход: ручной writer или CsvHelper.
  • Зафиксируйте формат: разделитель, NewLine, культуру, правила для null.
  • Для больших файлов пишите потоково, не накапливайте всё в памяти.
  • Для тестов фиксируйте культуру и переводы строк.
  • Покройте тестами краевые случаи: кавычки, разделители и переносы строк внутри значений.
  • Что дальше

    Следующий логичный шаг для проекта — оформить экспорт как небольшой модуль:

  • единый объект настроек (разделитель, кодировка, культура, NewLine)
  • единые маппинги/описания колонок
  • единые тесты на формат
  • Так вы перестанете «собирать CSV руками» в разных местах и получите предсказуемый экспорт во всём приложении.