Приходится иметь дело с форматом Base64? Тогда этот сайт идеально вам подойдет! Воспользуйтесь нашим невероятно удобным онлайн-инструментом для кодирования или декодирования ваших данных.

Кодирование Base64 в C# (CSharp): полное руководство

У вас есть байты. PNG, которому нужно путешествовать внутри JSON-ответа, токен, который должен уместиться в URL, строка текста, которая вот-вот войдёт в систему, принимающую только буквы и цифры. Где-то между byte[] в ваших руках и каналом, который ей предстоит пересечь, C# предлагает меню кодеров Base64, и выбор между ними - настоящее мастерство этой темы. Классическая одна-строка живёт в платформе с 2003 года, спан-варианты и URL-безопасные опции пришли вместе с современными средами выполнения, и каждый из них даёт разные обещания насчёт размера, переводов строк и алфавита. Эта статья проходит по всему меню, с рабочими примерами для каждой реальной работы, которую просят сделать от кодера.

Сначала домашние правила, одним дыханием, потому что домашняя страница этого сайта подробно объясняет формат: кодер берёт каждые три байта и записывает четыре символа из 64-буквенного алфавита, дополняя хвост одним-двумя знаками =, так что ваши данные уходят примерно на 33 процентов жирнее, чем пришли. Именно это число, а не какой-либо код, - самый важный факт этой статьи, и всё, что ниже, - про то, как платить его разумно.

Меню кодеров: выбирайте инструмент

Вот полное семейство API кодирования в мире .NET, с ситуацией, под которую каждый из них создан. Всё находится в самой среде выполнения, кроме URL-безопасного класса на старых платформах - тот приехал на маленьком пакете NuGet:

API Доступен с Для чего
Convert.ToBase64String(byte[]) .NET Framework 1.1 (2003) Классика. Целый массив на входе, заполненная строка на выходе. Без опций, без сюрпризов.
Convert.ToBase64String(byte[], int, int) .NET Framework 1.1 (2003) Кодирует срез большего массива, не выписывая его отдельно.
Convert.ToBase64String(byte[], Base64FormattingOptions) .NET 2.0 (2005) Классика с регулятором: по желанию вставляет перевод строки каждые 76 символов, по-почтовому.
Convert.ToBase64String(ReadOnlySpan<byte>, Base64FormattingOptions) .NET Core 2.1 (2018) Спан-версия: кодирует вид на буфер, без копирования массива, без выделения среза.
Convert.ToBase64CharArray(byte[], int, int, char[], int) .NET Framework 1.1 (2003) Пишет в символьный буфер, которым вы владеете, и возвращает, сколько символов занято.
Convert.TryToBase64Chars(ReadOnlySpan<byte>, Span<char>, out int, ...) .NET Core 2.1 (2018) Булево значение вместо исключений: кодирует, если буфер вмещает, сообщает false, если нет.
System.Buffers.Text.Base64.EncodeToUtf8, EncodeToUtf8InPlace .NET Core 2.1 (2018) Строгое спан-семейство: коды состояния вместо исключений и раздувание на месте для буферов, которыми вы уже владеете.
System.Buffers.Text.Base64Url.EncodeToString и его братья .NET 9 (2024) URL-безопасный алфавит, выдаётся без заполнения. На .NET Framework 4.6.2+ и .NET Standard 2.0: пакет NuGet Microsoft.Bcl.Memory.
ToBase64Transform + CryptoStream .NET Framework 1.1 (2003) Потоковое: кодирует файл по мере его потока, никогда не держа всю нагрузку в памяти.

Если ваш проект нацелен на версию .NET 2018 года или новее, первые семь строк и потоковая пара внизу уже в комплекте. Base64Url требует .NET 9 или новее, либо пакет Microsoft.Bcl.Memory на чём-то более старом. И взгляд вперёд: библиотеки .NET 11, на момент написания находящиеся в превью, с общим выпуском, которого ждут в конце 2026 года, добавляют существующим типам дополнительные удобные API и перегрузки Base64, так что меню продолжает расти. Ни один другой пакет в этой статье не требуется.

Стандартный вызов: Convert.ToBase64String

Девяносто процентов жизни кодирования в C# - это один вызов. Передайте ему байты, и он вернёт строку, которая их несёт:

using System;
using System.Text;

string text = "Man";
byte[] bytes = Encoding.UTF8.GetBytes(text);
string packed = Convert.ToBase64String(bytes);
Console.WriteLine(packed);
// TWFu

Обратите внимание на двухшаговую форму, потому что именно это - самый частый вопрос «почему мой Base64 не совпадает» в C#. Нет перегрузки, которая принимает string напрямую, и это сделано намеренно: строка в C# - это UTF-16, и платформа отказывается гадать, какие байты вы имели в виду, когда сказали «закодируй этот текст». Сначала вы выбираете байтовое представление - через Encoding.UTF8.GetBytes (или через ту кодировку, которой данные являются на самом деле), и только потом происходит шаг Base64. Остальное классическое семейство - тот же вызов с более узкой талией: перегрузка (byte[], int, int) кодирует срез буфера, не выписывая срез отдельно, а спан-перегрузка делает то же самое из ReadOnlySpan<byte>, и это правильный инструмент, когда данные - окно в больший буфер чтения. Ещё одно свойство классического кодировщика стоит сформулировать прямо: он никогда не падает и никогда не спрашивает. Он всегда выдаёт стандартный алфавит, всегда включает заполнение и всегда отдаёт ту же строку для того же входа, так что строка Base64 - надёжный отпечаток байтов, которые её породили.

Вопрос про 76 символов: переводы строк и Base64FormattingOptions

На классическом кодировщике есть один регулятор, и он стоит там с .NET 2.0: Base64FormattingOptions. Поставьте его в InsertLineBreaks, и кодировщик будет вставлять перевод строки после каждых 76 символов вывода - именно такую длину строк спецификация MIME использует для почтовых вложений. Поставьте None или воспользуйтесь перегрузками без опции, и получите одну длинную, неразрывную строку:

using System;

byte[] bytes = new byte[90];
string plain = Convert.ToBase64String(bytes);
string wrapped = Convert.ToBase64String(bytes,
  Base64FormattingOptions.InsertLineBreaks);

Console.WriteLine(plain.Length);   // 120
Console.WriteLine(wrapped.Length); // 122, один перевод строки добавлен после 76-го символа

Две детали про этот регулятор важны на практике. Первая: вставляемый перевод строки - это пара в стиле Windows, возврат каретки плюс перевод строки, а не голый перевод. Так что обёрнутый вывод содержит последовательности \r\n, и любой код, который потом «чистит» строку, удаляя только \n, останется с чужими возвратами каретки, прячущимися в данных. Вторая: обёртка происходит на 76 символах закодированного вывода, поэтому MIME-стандарт мог гарантировать, что почтовый транспорт, с его лимитами строк в 76 или 78 символов, никогда не разорёт группу из четырёх символов Base64 между строками: 76 кратно четырём, так что каждая строка заканчивается на границе группы. Обёрнутая форма нужна, когда вы производите почтовые тела, текстовые блоки в стиле PEM или что угодно, что будет нести legacy почтовый конвейер. Неообёрнутая форма нужна везде: JSON-нагрузки, URL-токены, API-ответы и файлы, которые будет декодировать строгий парсер, не любящий сюрпризов. А вот в JWT обёрнутая форма вообще не нужна: спецификация явно запрещает переводы строк, пробельные символы и даже заполнение.

Вы владеете выводом: символьные буферы и Try API

Иногда строка - не цель, цель - буфер. Вы дописываете в символьный массив фиксированного размера, пишете в кадр протокола или просто не хотите, чтобы среда выполнения выделяла вывод за вас. Для таких моментов у кодировщика есть режим символьного буфера с эпохи 1.1, а с эры спанов - ещё и режим Try. Метод с символьным буфером пишет в массив, который вы предоставляете, и сообщает, сколько символов он израсходовал, так что подгонка размера буфера - ваша работа, причём стандартная библиотека даже выдаёт вам формулу размера:

using System.Buffers.Text;
using System.Text;

byte[] bytes = Encoding.ASCII.GetBytes("Man");
char[] buffer = new char[Base64.GetMaxEncodedToUtf8Length(bytes.Length)];
int written = Convert.ToBase64CharArray(bytes, 0, bytes.Length, buffer, 0);

string packed = new string(buffer, 0, written);
Console.WriteLine(packed);
// TWFu

Метод-сосед Try делает ту же работу со спанами и отвечает булевым значением. Он кодирует вход в ваш целевой спан, сообщает число символов через out-параметр и возвращает false, если цели оказалось слишком мало, ничего не записав. Именно это последнее свойство делает его безопасным с недоверенными размерами входа: от неудачного вызова вы никогда не получите наполовину заполненный буфер:

using System;

byte[] bytes = { 1, 2, 3 };
char[] buffer = new char[4];

if (Convert.TryToBase64Chars(bytes, buffer, out int written,
  Base64FormattingOptions.None))
{
  Console.WriteLine(new string(buffer, 0, written));
  // AQID
}
else
{
  Console.WriteLine("Buffer too small, nothing was written.");
}

Для строгого спан-семейства в System.Buffers.Text.Base64 та же форма существует с договором OperationStatus вместо булева значения: EncodeToUtf8 заполняет спан байтов, которым вы владеете, и по статусу говорит, завершился ли он, исчерпал ли место или нуждается в большем входе, а EncodeToUtf8InPlace - тот, за который берутся, когда двоичные данные уже лежат в буфере, который вы готовы расширить под них: кодирование раздувает данные, поэтому метод записывает текст Base64 поверх конца того же буфера и сообщает, какой получился размер. Всё это делит одно правило про размер: вывод для n входных байтов всегда равен 4 * ceil(n / 3) символам вместе с заполнением, и вспомогательные функции GetMaxEncodedToUtf8Length и Base64Url.GetEncodedLength реализуют именно эту арифметику - второй для длины без заполнения, которая всегда равна размеру с заполнением или короче, - так что подбирайте размер по вспомогательным функциям и никогда не по заученной константе.

URL-безопасный кодировщик: Base64Url

В стандартном алфавите есть два символа, которые URL не любят. + в строке запроса по правилам разбора форм регулярно декодируется как пробел, а / и = оба требуют процентной кодировки, прежде чем смогут ехать в пути или параметре. URL-безопасный вариант Base64, определённый в разделе 5 RFC 4648, меняет + и / на - и _, которые нигде не требуют экранирования, и делает хвостовое заполнение = необязательным. С .NET 9 у среды выполнения есть выделенный для него класс, System.Buffers.Text.Base64Url, и у него есть одно поведение, которое удивляет в первый раз: он вообще не выдаёт заполнение:

using System.Buffers.Text;

byte[] bytes = { 1, 2 };
string classic = Convert.ToBase64String(bytes);
string urlSafe = Base64Url.EncodeToString(bytes);

Console.WriteLine(classic); // AQI=
Console.WriteLine(urlSafe); // AQI

В этой разнице и есть весь смысл. Сегмент JWT, идентификатор загрузки, токен в строке запроса, значение в пути URL: всем им нужна URL-безопасная форма без заполнения, и Base64Url.EncodeToString даёт её напрямую - и алфавит, и заполнение обработаны так, как это предписывают эти форматы. У класса есть полное семейство: кодирование в строку, в символьный спан и в спан UTF-8 байтов, плюс GetEncodedLength для подбора размера буфера и IsValid для проверки входа. Если ваш проект работает на более старой среде выполнения, добавьте пакет Microsoft.Bcl.Memory, который Microsoft публикует для бэкопорта класса на .NET Framework 4.6.2 и новее:

dotnet add package Microsoft.Bcl.Memory

А если пакет использовать нельзя, самодельная версия - это классический кодировщик плюс две замены и обрезка, и вы встретите её во множестве C#-кодовых баз:

using System;
using System.Text;

byte[] bytes = Encoding.UTF8.GetBytes("Hello World!");
string packed = Convert.ToBase64String(bytes)
  .Replace('+', '-')
  .Replace('/', '_')
  .TrimEnd('=');

Console.WriteLine(packed);
// SGVsbG8gV29ybGQh, URL-safe и без заполнения

Порядок операций в той цепочке стоит отметить: замена символов происходит на стандартном выводе, а заполнение обрезается последним, потому что обрезка сначала ничего бы не изменила, но сделала бы код менее читабельным, а замена после обрезки тоже сработала бы, но именно так рождаются тонкие баги. Используйте эту форму для токенов, идентификаторов и всего, что будет жить в URL, а стандартный алфавит оставьте для почтовых тел, JSON-нагрузок и файлов, где +, / и = чувствуют себя как дома.

Кормим кодировщик: строки, кодировки и выбор

Каждая задача кодирования, которая начинается с текста, начинается с одного и того же тихого решения: в какие байты превратится этот текст? Шаг Base64 детерминирован и невиновен, но шаг Encoding перед ним - именно там выводы расходятся, и расхождение может быть молчаливым. UTF-8 - допущение по умолчанию в современном вебе, и здесь это правильное значение по умолчанию: оно гоняет туда-обратно любой язык, именно его предположит каждая другая платформа, декодируя вашу нагрузку, и именно его Encoding.UTF8 даёт вам одним вызовом:

using System;
using System.Text;

string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
Console.WriteLine(packed);
// aMOpbGxvIOS4lueVjA==

Теперь смотрите, как тот же символ кодируется через другую кодировку, и увидите, почему «тот же текст» - плохо определённая вещь без привязанной к нему кодировки:

using System;
using System.Text;

string euro = "\u20ac";
string asUtf8 = Convert.ToBase64String(Encoding.UTF8.GetBytes(euro));
string asLatin1 = Convert.ToBase64String(
  Encoding.GetEncoding("ISO-8859-1").GetBytes(euro));

Console.WriteLine(asUtf8);   // 4oKs
Console.WriteLine(asLatin1); // Pw==

Две разные строки Base64 для одного и того же знака евро, обе совершенно валидные, и только одна из них декодируется обратно в знак евро на другой стороне. Ловушка с самым большим радиусом поражения - Encoding.Default: на .NET Framework под Windows это ANSI-кодировка системы, а на .NET (Core) это UTF-8, так что программа, кодирующая через Encoding.Default, на машине 2010 года даст другой Base64, чем на машине 2025 года, и оба вывода декодируются «правильно» на их родной платформе. Если декодированная нагрузка приезжает полной кракозябр с чужими акцентами, исходное кодирование использовало другую кодировку, чем предположило декодирование, и исправление - на этой стороне конвейера: закрепите кодировку явно, в обоих направлениях, в коде, который переживёт команду, написавшую его. И финальная заметка о самой системе типов: строка в C# - это UTF-16, так что если вы когда-нибудь пропустите через кодировщик сырые юниты кодировки UTF-16 (вызвав Encoding.Unicode.GetBytes), каждый ASCII-символ обойдётся в два байта, и ваш вывод удвоится в размере без всякой пользы, потому что декодер на другой стороне прочитает его как UTF-16 текст, а не как байты вашей исходной строки. Base64 несёт те байты, которые вы ему дали, и ему всё равно, что они значат.

Файлы: с диска в строку

Файлы - самая частая нагрузка кодирования и самая прощающая, потому что вопроса о кодировке нет: байты на диске - это данные, и кодировщику всё равно, складываются ли они в слово или в звуковую волну. Круговое путешествие - это чтение, кодирование и запись, и единственное настоящее решение - куда пойдёт результат:

using System.IO;

byte[] bytes = File.ReadAllBytes("photo.png");
string packed = Convert.ToBase64String(bytes);
File.WriteAllText("photo.b64", packed);

Console.WriteLine(packed.Length + " characters for "
  + bytes.Length + " bytes of image.");

Расчёт размера - это и есть вся история, и его стоит сделать до того, как выберете транспорт. Один мегабайт файла становится 1 333 336 символами Base64, и поскольку строка в C# хранит по два байта на символ, этот закодированный результат занимает примерно 2,7 мегабайта памяти в виде строки. Файл в десять мегабайт превращается в тринадцатимегабайтную строку, живущую в двадцати шести мегабайтах управляемой памяти. Для фотографии или конфига-блоба это не проблема, а для видео-нагрузки - очень веская причина использовать потоковый кодировщик, описанный ниже. Паттерн выше - тот, за который берутся для всего, что комфортно помещается в памяти, и это же самый незаметный паттерн любой функции «загрузить файл как Base64 в теле JSON»: прочитай файл, закодируй, положи строку в JSON и дай API-слою сделать свою работу.

Изображения в вебе: собираем data URI

Самый наглядный потребитель закодированных изображений - веб, и формат веба для «изображения, живущего внутри документа» - data URI: схема data:, за ней MIME-тип, флаг ;base64, запятая и закодированные байты. Собрать его в C# - это строковое сложение, и всю настоящую работу делает кодировщик:

using System.IO;

byte[] png = File.ReadAllBytes("logo.png");
string packed = Convert.ToBase64String(png);
string dataUri = "data:image/png;base64," + packed;

Console.WriteLine(dataUri.Substring(0, 30));
// data:image/png;base64,iVBORw0K

Префикс iVBORw0KGgo в том выводе - полезная контрольная точка: это Base64-форма восьмбайтовой сигнатуры PNG, так что любой PNG, который вы закодируете, начнётся именно так, а любой PNG data URI, который не начинается так, - не PNG. Три практические заметки принадлежат этому паттерну. Первая: data URI - полная копия изображения, раздутая на треть, встроенная в ваш HTML или CSS, так что он меняет сетевой запрос на постоянный вес страницы, выгодная сделка для фавиконки 4 КБ и грабёж для 4 МБ hero-изображения, и кодировщик не торгуется насчёт этих 33 процентов. Вторая: если изображение большое, переразмерьте или пережмите его перед кодированием, потому что каждый байт оригинала проявится в странице. Третья: берегите пользовательский SVG в пользовательском HTML: SVG может нести скрипт, так что его встраивание - напрямую или через <object>/<embed> - классическая поверхность XSS. Обычный <img> data URI не запустит его в современных браузерах, но та же разметка, переиспользованная в тех контекстах, запустит. PNG, JPEG, GIF и WebP в data URI - инертны; SVG - тот, кто не инертен.

Собираем JWT вручную

Собрать JSON Web Token с нуля - ритуал посвящения, и в C# он приятнее, чем в большинстве языков, потому что детали короткие. JWT - это три сегмента base64url, склеенные точками: закодированная шапка, закодированная нагрузка и подпись. Два первых - UTF-8 JSON-документы, а подпись вычисляется по первым двум сегментам, склеенным точкой. Вот вся сборка, с заготовкой вместо подписи, потому что криптографический шаг принадлежит вашему ключу подписи, а не истории Base64:

using System;
using System.Buffers.Text;
using System.Text;

string headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
string payloadJson = "{\"sub\":\"42\",\"name\":\"Ada\"}";

string header = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(headerJson));
string payload = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(payloadJson));
string signature = "c2lnbmF0dXJl"; // заготовка вместо настоящего значения HMAC или ECDSA

string jwt = header + "." + payload + "." + signature;
Console.WriteLine(jwt);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl

Два свойства Base64Url.EncodeToString выполняют тихую работу в том примере. Оно выдаёт URL-безопасный алфавит, так что ни +, ни / не могут появиться в токене, и оно опускает заполнение, так что = тоже никогда не появляется, - именно это требует спецификация JWS, и именно этого Convert.ToBase64String без помощи не сделает. Если вы на среде выполнения до .NET 9, та же работа идёт через стандартный кодировщик плюс цепочку правок из раздела о URL-безопасном: закодируйте, поменяйте два символа, обрежьте заполнение. Порядок сегментов важен для подписи, которая вычисляется по header плюс точка плюс payload как обычным ASCII-байтам, так что сначала соберите два сегмента и подпишите их точное склеивание, а не переформатированную версию JSON. И граница, которую стоит держать острой: для всего, что может дотянуться пользователь, не собирайте JWT вообще вручную. Пакет System.IdentityModel.Tokens.Jwt берёт на себя сборку, подпись, валидацию и срок действия, и его обработка base64url - это ровно тот алфавит и то правило заполнения. Ручная сборка - для тестов, демо и дня, когда нужно точно понимать, что делает библиотека.

HTTP-заголовки: Basic-аутентификация

Base64 появляется в голом HTTP в схеме Basic auth, и сторона кодирования - один из самых коротких конструкторов заголовков в протоколе: склейте имя пользователя и пароль двоеточием, закодируйте результат как UTF-8, прогоните через Base64 и приставьте имя схемы:

using System;
using System.Text;

string user = "ada";
string password = "s3cret";
string credentials = user + ":" + password;

string header = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials));
Console.WriteLine(header);
// Basic YWRhOnMzY3JldA==

Кодировка - капризная часть: RFC 7617 оставляет кодировку схемы Basic по умолчанию неопределённой ради обратной совместимости и предлагает лишь рекомендательную подсказку UTF-8, но именно этого ожидает каждый современный сервер, так что имя пользователя с акцентированным символом должно пройти через Encoding.UTF8, а не через то, что является значением по умолчанию на платформе, иначе сервер декодирует другую строку байтов и отклонит вход. Шаг Base64 - единственное кодирование в заголовке: не делайте процентную кодировку результата, не URL-кодируйте его, не делайте двойной Base64. Каждый из этих «полезных» лишних шагов - известный баг, и двойное кодирование - самый частый, потому что учётные данные иногда приходят уже закодированными из слоя, который уже прогнал их через Base64, а второе кодирование производит заголовок, который выглядит правдоподобно и молча падает на сервере. Две оговорки о самой схеме, чтобы они попали сюда, а не в раздел о безопасности, где они бы размылись: Basic-аутентификация передаёт пароль в форме, которая в одной команде от читаемого, поэтому она допустима только поверх TLS, и даже тогда это неправильный инструмент для большинства API-задач, именно поэтому bearer-токены и JWT забрали эту нишу. Работа кодировщика во всём этом - маленькая и честная: превратить склеенные двоеточием учётные данные в строку, безопасную для заголовка, и ничего больше.

Электронная почта: MIME и почему ToBase64Transform не обёртывает

Электронная почта - исторический дом Base64, и по-прежнему именно оттуда берёт начало правило 76-символьной строки: спецификация MIME обёртывает закодированные тела на 76 символах с CRLF между строками, чтобы ни один SMTP-прыжок не имел повода переобёртывать их. C# даёт вам два кодировщика для этой работы, и они дают разные обещания, что стоит понять до того, как выбирать один. Первый - классический Convert.ToBase64String с InsertLineBreaks, который вы видели в разделе о переводах строк, и он - ровно MIME-форма: обёртка на 76 с CRLF, готовая к вставке под заголовок Content-Transfer-Encoding: base64. Второй - ToBase64Transform, потоковый родственник, и вот тут сюрприз: он не вставляет переводы строк. У него нет для них режима, нет опции, нет флага конструктора, и его вывод - один длинный неообёрнутый поток:

using System.IO;
using System.Security.Cryptography;

using FileStream source = File.OpenRead("photo.png");
using MemoryStream destination = new MemoryStream();
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);

encoder.CopyTo(destination);
Console.WriteLine(destination.Length + " characters, no line breaks");

Так что практичное правило такое: для небольших и средних почтовых нагрузок прочитайте байты и используйте обёртывающий классический кодировщик, потому что MIME-форма получается напрямую. Для больших вложений идите потоком через ToBase64Transform, чтобы держать память ровной, и обёрните результат сами, если транспорту по-настоящему нужны 76-символьные строки, разрезая вывод на границах групп (каждые 76 символов, что всегда граница группы, как объяснил раздел о переводах строк). Преобразование делает правильно, оставаясь неообёрнутым: оно обрабатывает вход группами по три байта, а переводы строк - решение о формате, которое принадлежит слою, знающему про транспорт, а не слою, который превращает байты в символы в потоке.

Потоковое кодирование: большие файлы без двойного чтения

Когда нагрузка - это видео, резервная копия или что-то, что стыдно держать в строке, потоковый кодировщик - целое решение. Паттерн - зеркало потокового декодирования: CryptoStream поверх исходного файла, с ToBase64Transform в режиме чтения, и CopyTo в цель. Файл течёт в, Base64 течёт вон, и единственная память, которую держит процесс, - это буфер, которым поток пользуется внутри:

using System.IO;
using System.Security.Cryptography;

using FileStream source = File.OpenRead("video.mp4");
using FileStream target = File.Create("video.b64");
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);

encoder.CopyTo(target);
Console.WriteLine("Wrote " + target.Length + " characters.");

Два факта про этот паттерн стоит держать. Первый: размер вывода полностью определён размером входа, 4 символа на 3 байта, так что можно зарезервировать место в цели, заранее посчитать длину для заголовка content-length или заложить квоту диска до того, как потечёт хоть один байт. Второй: преобразование ожидает свой вход группами по три байта, и CryptoStream разбирается с этим выравниванием за вас, подавая преобразованию ровно то, что оно хочет, пока файл льётся потоком. Если вы когда-нибудь погоните преобразование вручную через TransformBlock, кормите его кратно трём и позвольте TransformFinalBlock слить хвост - тот один или два оставшихся байта, из которых складывается последняя неполная группа с её одним-двумя символами заполнения. Для большинства приложений форма CopyTo - всё, что вы когда-либо напишете, и это форма, которая хорошо ведёт себя при ограничении памяти, а это именно то место, где любят жить большие файлы.

Конфигурация, переменные окружения и базы данных

Другая частая задача кодирования в C#-приложениях - задача хранения: взять секрет или двоичный blob и положить его туда, где принимают только текст. Переменные окружения - видимый пример, потому что переменная окружения, по определению, - строка:

using System;
using System.Text;

string secret = "p@ssw0rd+and/symbols";
string packed = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));
Environment.SetEnvironmentVariable("SECRET_B64", packed);

string back = Encoding.UTF8.GetString(
  Convert.FromBase64String(Environment.GetEnvironmentVariable("SECRET_B64")));
Console.WriteLine(back == secret);
// True

В базах данных та же идея обычно появляется как свойство byte[], которое должен держать текстовый столбец, и у Entity Framework Core есть встроенный механизм ровно для этого: конвертер значений, который прогоняет ваши функции кодирования и декодирования при каждом чтении и записи:

using Microsoft.EntityFrameworkCore;

modelBuilder.Entity<Avatar>()
  .Property(a => a.ImageData)
  .HasConversion(
    v => Convert.ToBase64String(v),
    v => Convert.FromBase64String(v));

Этот конвертер - вся интеграция с базой данных: код на C# видит byte[], столбец видит строку Base64, и круговое путешествие невидимо в точке вызова. Две оговорки принадлежат этому разделу. Первая: столбец платит налог в 33 процента: текстовый столбец, размерированный под закодированную длину, держит на треть меньше данных, чем та же ширина в двоичном виде, так что если у вас столбец фиксированной ширины, размерируйте его под длину Base64, а если у вас varchar(max) или эквивалент, налог - только вопрос биллинга. Вторая, и она возвращается снова и снова: Base64 в конфигурационном файле - это форма, а не щит. Он держит значение в одной строке, убирает его с дороги текстовых редакторов и в одной команде от читаемого для любого, кто может прочитать файл. Секретам нужна настоящая защита: хранилище секретов, хранилище ключей, как минимум права на файлы, а Base64 - лишь транспортный формат, который секрет надевает, пока сидит в конфигурации.

Из командной строки

Каждый кодировщик заслуживает 15-строчной консольной жизни, и C#-вариант приятен, потому что вывод - обычная строка, для которой стандартный вывод и придуман. Вот весь инструмент: он принимает путь к файлу или стандартный ввод, кодирует его и пишет Base64 в терминал, откуда любой конвейер оболочки может его забрать:

using System;
using System.IO;
using System.Text;

string input = args.Length > 0
  ? File.ReadAllText(args[0])
  : Console.In.ReadToEnd();

byte[] bytes = Encoding.UTF8.GetBytes(input);
Console.WriteLine(Convert.ToBase64String(bytes));

Соберите его один раз, и он будет сидеть рядом с собственной утилитой base64 оболочки в те дни, когда вам конкретно нужно поведение .NET-кодировщика: тот же алфавит, то же заполнение и UTF-8 обработка среды выполнения C# для того, что конвейер ему подаст. Для двоичных файлов тот же каркас с File.ReadAllBytes вместо File.ReadAllText - и есть всё изменение, и вывод тогда описывает точные байты файла, а не его текстовую интерпретацию. Инструмент - ещё и хороший зонд: прогоните файл через него, прогоните вывод обратно через декодер статьи о декодировании и сравните два файла diff'ом - приятная проверка конца в конце, что обе стороны конвейера согласны в каждом байте.

Заполнение, или хвостовые равные знаки

Последние знаки = в строке Base64 - это бухгалтерия формата, и C#-кодировщики не сходятся во мнениях о них, что и есть источник конкретного и частого бага совместимости. Классический Convert.ToBase64String всегда заполняет, потому что классический декодер, с которым он в паре, всегда этого ждёт. Base64Url.EncodeToString никогда не заполняет, потому что URL-безопасные потребители, для которых он предназначен, - JWT и токен-API, - всегда ждут компактную форму. Когда ваш вывод пересекается со миром с противоположным ожиданием, исправление - арифметика, и это та же арифметика, которую статья о декодировании показала для обратного направления:

using System;

string padded = Convert.ToBase64String(new byte[] { 1, 2 });
Console.WriteLine(padded);           // AQI=
Console.WriteLine(padded.TrimEnd('=')); // AQI, что нужно URL-safe потребителю

string compact = "AQI";
string restored = compact + new string('=', (4 - compact.Length % 4) % 4);
Console.WriteLine(restored);         // AQI=, что нужен классическому декодеру

Формула (4 - length % 4) % 4 - это целый мир заполнения: она добавляет ноль, один или два символа, чтобы длина уложилась в кратно четырёх, а внешний остаток от деления не даёт уже заполненному входу подхватить лишние. Два предупреждения про заполнение, потому что именно там добросовестный код идёт не туда. Никогда не относите = к данным: оно не несёт информации, так что кодирование строки, которая уже содержит заполнение, будто это нагрузка, или URL-кодирование = в %3D внутри строки запроса, - оба способа произвести вывод, который выглядит правильно, но декодируется неправильно. И остерегайтесь небольшого семейства устаревших нагрузок, где заполнение записывали другим символом, точкой в некоторых старых системах, вместо стандартного =: если пришедшее значение использует точку там, где вы ждёте заполнения, нормализуйте её обратно в = перед декодированием или прогоните его через URL-безопасный путь без заполнения.

Насколько он быстр

Кодирование Base64 в современном .NET быстрое, и интересная часть - история про память, а не про CPU. Реализации среды выполнения оптимизированы SIMD-векторными инструкциями там, где их поддерживает железо, и мультимегабайтный вход кодируется за однозначные или низкие двузначные миллисекунды на обычном настольном компьютере, достаточно быстро, чтобы кодировщик был фактически бесплатным в любом приложении, которое вы напишете. Совет по производительности, который реально меняет код, - про форму. Вывод - строка в C#, а строка в C# хранит два байта на символ, так что стоимость закодированного результата в памяти - примерно 2,7 байта на входной байт (4 символа на 3 входных байта при 2 байтах на символ), и это число стоит знать, когда нагрузка в мегабайтах. Если вы кодируете тысячи маленьких нагрузок в цикле, предпочитайте спан- и символьно-буферные API, которые пишут в буферы, которые вы переиспользуете, а не строковые API, которые выделяют свежую управляемую строку на каждый вызов. Если вы кодируете один большой файл, полностью пропустите строку и используйте потоковое преобразование, потому что цена в два байта на символ, когда вы держите 13-мегабайтную строку, - чистая трата, если бы CopyTo держал рабочий набор в буферах потока. И если вы производите обёрнутый по MIME вывод, помните, что проход обёртки - второй заход по данным, так что обёртывайте только тогда, когда транспорту это нужно, а не по умолчанию.

Разговор о безопасности

У стороны кодирования Base64 есть один урок безопасности, и он зеркален декодерскому: именно вы принимаете решение подвергнуть читаемые данные, и формат вас не остановит. Base64 - кодирование, а не шифрование. У него нет ключа, нет алгоритма и нет никакой секретности, и вывод вашего вызова ToBase64String - в одной команде от входа, на любой машине, на любом языке, для любого. Так что первое правило - про то, что вы решаете закодировать: никогда не кладите пароль, токен или секрет в конфигурационный файл, «защищённый» Base64, потому что эта защита - ровно на один вызов декодирования от открытого вида, а у того, кто читает конфиг, есть команда. Если значение должно быть секретным, ему нужна настоящая защита, а Base64 - лишь форма, которую оно надевает, пока сидит в текстовом поле.

Второй урок - про канал, и он специфичен для того, что собирает эта статья. Заголовок Basic-аутентификации несёт пароль в форме, которую может прочитать любой прокси, любой лог и любой промежуточный узел, поэтому схема допустима только поверх TLS и за пределами старых интеграций почти ушла в прошлое. Data URI в HTML несёт изображение, а если изображение - пользовательский SVG, он несёт то, что несёт SVG, поэтому случай SVG в data URI требует той же осторожности, что и любой пользовательский контент. А значение Base64 в URL буквально в URL, что значит в истории браузера, в журнале доступа сервера, в заголовке referrer и в кэше прокси, так что токены, которым положено оставаться частными, не живут в строках запроса - с заполнением или без. Кодировщик во всех трёх случаях делает свою честную работу: превращает байты в строку, безопасную для переноски. Безопасность - в том, что вы несёте и куда; формат - гонец получше большинства, но это гонец, а не хранилище.

Ловушки, в которые попадают C#-кодировщики

Это ямы, которые постоянно высовываются на стороне кодирования C#-кода, и у каждой есть конкретная причина в том, как работает платформа:

  • Кодировка, которую вы не выбирали. Кодирование строки через Encoding.Default даёт разный Base64 на .NET Framework (ANSI-кодировка Windows) и на .NET (UTF-8). Оба вывода валидны, оба декодируются «правильно» на их родной платформе, и это не одни и те же байты. Закрепите кодировку явно.
  • Двойное кодирование. Вход уже был Base64 (конфиг, закодировавший уже закодированное значение, API, перекодирующее свой вход), и кодировщик, делая ровно то, что ему велели, произвёл Base64 из Base64. Результат выглядит правдоподобно и декодируется по одному слою за раз, и именно так в продакшене обнаруживают баг, на чин которого нужно два декодирования.
  • Переводы строк не в том месте. Обёрнутая по MIME форма, со своими парами CRLF, попадает в JSON-строку, сегмент JWT или параметр URL, где строгий потребитель зажевывается пробельными символами, о которых ему не говорили. Обёртывайте для почты, оставляйте в покое во всём остальном, а если срезаете чужую обёртку - срезайте и \r, и \n.
  • Стандартный алфавит в URL. + в строке запроса по правилам разбора форм декодируется как пробел, так что стандартное значение Base64, положенное в URL, возвращается с буквами там, где стояли плюсы. Используйте URL-безопасный алфавит или процентно закодируйте всё значение целиком - и никогда оба сразу.
  • Расхождение в заполнении. Ваш вывод заполнен, а потребитель хочет компактный, или наоборот, и ни одна сторона не виновата - они просто не согласны. Исправление - арифметика из раздела о заполнении, применённая той стороной, которая знает ожидание потребителя, а это обычно сторона, пишущая токен.
  • Память без бюджета. Закодированная строка - два байта на символ в памяти, так что файл 10 МБ становится 13-миллионно-символьной строкой весом примерно 27 МБ в управляемой памяти, и цикл, который строит такие строки по одной, в профилере проявится как мельница выделений без видимой причины. Подбирайте размер буферов вспомогательными функциями длины, большие - потоком, буферы в горячих циклах - переиспользуйте.
  • Преобразование, которое не обёртывает. ToBase64Transform выдаёт одну длинную строку. Код, который прогоняет через него «готовое к MIME» вложение и потом отправляет письмо, производит строку в 120 000 символов, которую какой-нибудь транспорт переобёртует посреди группы - ровно то повреждение, от которого и придумано 76-символьное правило.
  • Кодирование кодирования. Если подать строку Base64 в кодировщик, потому что «данные уже текст», получится второй слой. Кодировщик не знает и не заботится, что его вход похож на Base64; он кодирует столько символов, сколько строка случайно имеет, и декодер на другой стороне получает строку Base64 там, где ждал ваши данные.

Как рос кодировщик: экскурсия по версиям

У стороны кодирования API есть собственный таймлайн, и он тянется от второго выпуска .NET до того, что сейчас в превью:

  • .NET Framework 1.1, апрель 2003. Прибывают Convert.ToBase64String и ToBase64CharArray - всё классическое семейство в одном выпуске, с перегрузками для срезов уже включёнными, что для API 2003 года - маленькое чудо прозорливости.
  • .NET 2.0, 2005. Base64FormattingOptions и значение InsertLineBreaks вливаются в семейство, принося MIME-обёртку строк в платформу и положив конец эпохе самодельных циклов Substring в почтовом коде.
  • .NET Core 2.1, 2018. Эра спанов. Convert получает спан-кодирование и TryToBase64Chars, а новый класс System.Buffers.Text.Base64 прибывает со своим договором OperationStatus и раздуванием на месте, созданный для мира нулевых выделений.
  • .NET 5, 2020. Поставляются шестнадцатеричные братья (Convert.ToHexString и друзья) - тот же дизайн-паттерн, применённый к 16-символьному алфавиту, и паттерн класса-конвертера становится фирменным стилем.
  • .NET 7, 2022. X509Certificate2.ExportCertificatePem заставляет платформу производить PEM за вас - маркеры брони, 64-символьная обёртка и тело Base64 включительно, что незаметно отправляет на покой целый класс кода ручной форматировки сертификатов.
  • .NET 9, ноябрь 2024. System.Buffers.Text.Base64Url заезжает в коробку после лет просьб сообщества, с пакетом Microsoft.Bcl.Memory, бэкопортящим его для .NET Framework 4.6.2 и новее, и поведением без заполнения, которое JWT-код все эти годы крутил вручную.
  • .NET 11, в превью на момент написания. Следующий выпуск, которого ждут в конце 2026 года, добавляет существующим типам дополнительные удобные API и перегрузки Base64, продолжая движение к более удобному интерфейсу.

Сам формат имеет более старую биографию, и именно поэтому C# API выглядит так, как выглядит. Первое стандартизированное использование того, что мы теперь называем MIME Base64, - протокол Privacy-Enhanced Mail в 1987 году (RFC 989), спецификация MIME закрепила 76-символьную обёрнутую строками форму в 1993 году, а RFC 4648 в 2006 году дал формату его современную знающую про алфавит спецификацию, включая URL-безопасный вариант, первый первоклассный кодировщик для которого C# получил лишь в 2024 году. Три десятилетия почтовых и веб-конвенций - причина, по которой переводы строк, заполнение и два алфавита вообще существуют, и C#-кодировщик - место, где встречаются все три.

Маленькие чудеса

  • Четырёхсимвольный минимум. Наименьший возможный непустой вывод Base64 - четыре символа, потому что формат думает четвёрками, даже если вы дадите ему один байт. Один байт чего угодно кодируется в две буквы и два знака =, и эта форма - два символа данных в шляпе заполнения - отпечаток, который вы начнёте узнавать в конфигах и токенах.
  • Нули приветствуются. У кодировщика нет мнения о том, что значат байты, так что буфер, полный нулей, охотно кодируется в стену символов A, а двоичный файл со всеми его NUL-байтами проходит круговое путешествие, не потеряв ни одного. Тревога «строки не могут держать двоичное» принадлежит строковой стороне системы типов, а не кодировщику, который вообще никогда не видит строку.
  • Детерминизм как фича. Те же байты, те же опции, всегда та же строка. Нет метки времени, нет случайной соли, нет вариаций, поэтому строка Base64 - приличный экспресс-отпечаток содержимого файла: два файла с одинаковым Base64 - это один и тот же файл, и проверка - сравнение строк.
  • Два байта на символ - бесплатно. Строка в C# - это UTF-16, так что каждый символ вашего вывода Base64 занимает два байта в управляемой памяти. Кодировщик об этом не объявляет, свойство длины об этом не сообщает, и 13-миллионно-символьная строка просто весит 26 МБ - это число, которое стоит держать в голове, когда нагрузка большая.
  • CRLF по наследству. MIME-обёртка вставляет пары «возврат каретки - перевод строки», даже если ваш код работает под Linux, потому что правило пришло из спецификации почты, а не из платформы. Кодировщик - историк не меньше, чем конвертер, и он сохраняет окончания строк 1993 года на машине 2026 года.
  • Перегрузка для среза с первого дня. ToBase64String(byte[], int, int) кодирует окно в больший массив с 2003 года, за пятнадцать лет до того, как спаны сделали эту идею модной. Дизайнеры API эпохи 1.1 посмотрели на настоящие буферы и добавили форму «смещение плюс длина», и она до сих пор верное решение, когда данные - участок более крупного чтения.
  • 64-символьная строка сертификата. PEM обёртывается на 64 символах, а не на 76, и ExportCertificatePem это знает и обёртывает соответственно, и это одна из тихих деталей, которая делает совет «пусть этим займётся платформа» правильным для работы с сертификатами. Две ширины обёртки, одно семейство форматов, и платформа держит их различимыми.
  • Два алфавита, два имени. 64 значения в одной части API называются «стандартными», в другой - «URL-safe», и они различаются ровно двумя символами: 62-м и 63-м слотами. + и / на одной стороне, - и _ на другой, и каждый баг совместимости этой статьи живёт в тот момент, когда кто-то предположил, что две стороны - одно и то же.

Круг замыкается

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

Последнее обновление: 2026-09-08

Связанная статья: Декодирование Base64 в C# (CSharp): полное руководство