Devi lavorare con il formato Base64? Allora questo sito è perfetto per te! Usa il nostro praticissimo strumento online per codificare o decodificare i tuoi dati.

Codifica Base64 in C# (CSharp): una guida completa

Sei in possesso dei byte. Un PNG che deve viaggiare dentro una risposta JSON, un token che deve stare in un URL, una riga di testo che sta per entrare in un sistema che accetta solo lettere e cifre. Da qualche parte tra il byte[] che hai in mano e il canale che deve attraversare, C# offre un menu di encoder Base64, e scegliere tra loro è la vera abilità di questa materia. La classica riga sola è nel framework dal 2003, le opzioni basate sugli span e quelle URL-safe sono arrivate con i runtimes moderni, e ognuna fa promesse diverse su dimensioni, a capo e alfabeto. Questo articolo percorre l'intero menu, con esempi funzionanti per ogni lavoro vero che viene chiesto a un encoder.

Prima le regole di casa, in un fiato, dato che la home page di questo sito spiega il formato in profondità: l'encoder prende ogni tre byte e scrive quattro caratteri da un alfabeto di 64 simboli, aggiungendo in coda uno o due caratteri = di padding, così i tuoi dati escono circa un terzo più grassi di quanto siano arrivati. Quel numero, non un codice qualsiasi, è il fatto più importante di questo articolo, e tutto ciò che segue riguarda il pagarlo in modo sensato.

Il menu degli encoder: scegli il tuo strumento

Ecco la famiglia completa di API di codifica nel mondo .NET, con la situazione per cui ogni singola API è stata costruita. Tutto è nel runtime stesso, a eccezione della classe URL-safe sui framework più vecchi, che viaggia dentro un piccolo pacchetto NuGet:

API Disponibile da A cosa serve
Convert.ToBase64String(byte[]) .NET Framework 1.1 (2003) Il classico. Array intero in entrata, stringa con padding in uscita. Niente opzioni, niente sorprese.
Convert.ToBase64String(byte[], int, int) .NET Framework 1.1 (2003) Codifica una fetta di un array più grande, senza prima copiarla fuori.
Convert.ToBase64String(byte[], Base64FormattingOptions) .NET 2.0 (2005) Il classico con la manopola: inserisci facoltativamente un a capo ogni 76 caratteri, il modo MIME.
Convert.ToBase64String(ReadOnlySpan<byte>, Base64FormattingOptions) .NET Core 2.1 (2018) La versione span: codifica una vista su un buffer, nessuna copia dell'array, nessuna allocazione della fetta.
Convert.ToBase64CharArray(byte[], int, int, char[], int) .NET Framework 1.1 (2003) Scrivi in un buffer di caratteri che possiedi, e ottieni quanti caratteri sono stati usati.
Convert.TryToBase64Chars(ReadOnlySpan<byte>, Span<char>, out int, ...) .NET Core 2.1 (2018) Un booleano al posto delle eccezioni: codifica se il buffer basta, riporta false se non basta.
System.Buffers.Text.Base64.EncodeToUtf8, EncodeToUtf8InPlace .NET Core 2.1 (2018) La famiglia span rigorosa: codici di stato al posto delle eccezioni, e gonfiaggio in loco per i buffer che possiedi già.
System.Buffers.Text.Base64Url.EncodeToString e i suoi fratelli .NET 9 (2024) L'alfabeto URL-safe, emesso senza padding. Su .NET Framework 4.6.2+ e .NET Standard 2.0: il pacchetto NuGet Microsoft.Bcl.Memory.
ToBase64Transform + CryptoStream .NET Framework 1.1 (2003) In streaming: codifica un file man mano che scorre, senza mai tenere l'intero payload in memoria.

Se il tuo progetto punta a una versione .NET dal 2018 in poi, le prime sette righe e la coppia streaming in fondo sono già di serie. Base64Url richiede .NET 9 o successivo, oppure il pacchetto Microsoft.Bcl.Memory su tutto ciò che è più vecchio. E una nota in avanti: le librerie .NET 11, in preview al momento della scrittura con un rilascio generale atteso per la fine del 2026, aggiungono altre API di comodità Base64 e sovraccarichi ai tipi esistenti, quindi il menu continua a crescere. Nessun altro pacchetto in questo articolo è richiesto.

La chiamata standard: Convert.ToBase64String

Novanta per cento della vita di codifica in C# è una singola chiamata. Gli passi i byte, e ti restituisce la stringa che li porta:

using System;
using System.Text;

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

Nota la forma a due passi, perché è la domanda più comune in C# sul "perché il mio Base64 non corrisponde". Non c'è un sovraccarico che accetta una string direttamente, ed è così per design: una stringa C# è UTF-16, e il framework si rifiuta di indovinare quali byte intendevi quando hai detto "codifica questo testo". Prima scegli la rappresentazione in byte, con Encoding.UTF8.GetBytes (o qualunque charset siano davvero i dati), e solo allora avviene il passo Base64. Il resto della famiglia classica è la stessa chiamata in una versione più compatta: il sovraccarico (byte[], int, int) codifica una fetta di un buffer senza copiare la fetta fuori, e il sovraccarico span fa la stessa cosa da un ReadOnlySpan<byte>, che è lo strumento giusto quando i dati sono una finestra su un più grande buffer di lettura. Un'altra proprietà dell'encoder classico vale la pena di essere detta senza giri di parole: non fallisce mai e non chiede mai nulla. Emette sempre l'alfabeto standard, include sempre il padding, e ti dà sempre la stessa stringa per lo stesso input, quindi una stringa Base64 è un'impronta affidabile dei byte che l'hanno prodotta.

La questione dei 76 caratteri: a capo e Base64FormattingOptions

C'è una manopola sull'encoder classico, ed è lì da .NET 2.0: Base64FormattingOptions. Impostala su InsertLineBreaks e l'encoder inserisce un a capo dopo ogni 76 caratteri di output, la lunghezza di riga che la specifica MIME usa per gli allegati email. Impostala su None, o usa i sovraccarichi senza l'opzione, e ottieni una lunga stringa ininterrotta:

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, un a capo aggiunto dopo il carattere 76

Due dettagli su quella manopola contano nella pratica. Primo, l'a capo che inserisce è la coppia Windows, ritorno a capo più a capo, non un semplice a capo. Quindi l'output avvolto contiene sequenze \r\n, e qualunque codice che poi "pulisce" la stringa togliendo solo \n si ritroverà con ritorni a capo vaganti nascosti nei dati. Secondo, l'avvolgimento avviene a 76 caratteri di output codificato, ed è per questo che lo standard MIME poteva garantire che il trasporto email, con i suoi limiti di riga a 76 o 78 caratteri, non avrebbe mai spezzato un gruppo di quattro caratteri Base64 tra le righe: 76 è un multiplo di quattro, quindi ogni riga termina su un confine di gruppo. Vuoi la forma avvolta quando produci corpi email, blocchi di testo in stile PEM, o qualunque cosa una pipeline di posta legacy debba portare. Vuoi la forma non avvolta dappertutto altrove: payload JSON, token URL, risposte API, e file che verranno decodificati da un parser rigoroso che non ama le sorprese. E non vuoi mai la forma avvolta dentro un JWT, dove la specifica vieta esplicitamente a capo, spazi bianchi e persino il padding.

Prendere in mano l'output: buffer di caratteri e le API Try

A volte la stringa non è il traguardo, il buffer lo è. Stai aggiungendo a un array di caratteri a dimensione fissa, stai scrivendo in un frame di protocollo, o semplicemente non vuoi che il runtime allochi l'output per te. Per quei momenti l'encoder ha avuto una modalità buffer di caratteri fin dai tempi della 1.1, e una modalità Try dall'era degli span. Il metodo buffer di caratteri scrive in un array che fornisci e ti dice quanti caratteri ha usato, quindi dimensionare il buffer è il tuo lavoro, e la libreria standard ti passa persino la formula di dimensionamento:

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

Il metodo fratello Try fa lo stesso lavoro dagli span e risponde con un booleano. Codifica l'input nel tuo span di destinazione, riporta il conteggio dei caratteri nel parametro out, e restituisce false se la destinazione era troppo piccola, senza scrivere nulla. Quella ultima proprietà lo rende sicuro da usare con dimensioni di input non fidate: non ottieni mai un buffer mezzo pieno da una chiamata fallita:

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.");
}

Per la famiglia span rigorosa in System.Buffers.Text.Base64, la stessa forma esiste con il contratto OperationStatus al posto di un booleano: EncodeToUtf8 riempie uno span di byte che possiedi e ti dice, tramite lo stato, se ha finito, è rimasto senza spazio, o ha bisogno di più input, e EncodeToUtf8InPlace è quello da afferrare quando i dati binari stanno già in un buffer che sei disposto a far crescere: la codifica gonfia i dati, quindi il metodo scrive il testo Base64 sulla coda dello stesso buffer e riporta quanto è lungo il risultato. Tutti questi condividono una regola sul dimensionamento: l'output per n byte di input sono sempre 4 * ceil(n / 3) caratteri incluso il padding, e gli helper GetMaxEncodedToUtf8Length e Base64Url.GetEncodedLength implementano quell'aritmetica - il secondo per la lunghezza senza padding, che è sempre la dimensione con padding o più corta - quindi dimensiona dagli helper e mai da una costante ricordata a memoria.

L'encoder URL-safe: Base64Url

L'alfabeto standard ha due caratteri che gli URL non amano. Il + in una query string viene di routine decodificato come spazio dalle regole di parsing dei form, e sia / che = vogliono il percent-encoding prima di poter viaggiare in un path o in un parametro. La variante URL-safe del Base64, definita nella sezione 5 della RFC 4648, sostituisce + e / con - e _, che non hanno bisogno di escaping da nessuna parte, e rende facoltativo il padding finale =. Da .NET 9 il runtime ha una classe dedicata, System.Buffers.Text.Base64Url, e ha un comportamento che sorprende la gente alla prima: non emette padding per niente:

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

Quella differenza è l'intero punto. Un segmento JWT, un identificatore di upload, un token in una query string, un valore in un path URL: tutti vogliono la forma URL-safe senza padding, e Base64Url.EncodeToString la dà direttamente, con alfabeto e padding entrambi gestiti come specificano quei formati. La classe ha la famiglia completa, codifica in stringa, in char span e in byte span UTF-8, più GetEncodedLength per il dimensionamento del buffer e IsValid per validare l'input in entrata. Se il tuo progetto gira su un runtime più vecchio, aggiungi il pacchetto Microsoft.Bcl.Memory, che Microsoft pubblica per fare il backport della classe su .NET Framework 4.6.2 e successivi:

dotnet add package Microsoft.Bcl.Memory

E se non puoi usare il pacchetto, la versione fatta a mano è l'encoder classico più due sostituzioni e una rimozione del padding, che incontrerai in un gran numero di codebase 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 e senza padding

L'ordine delle operazioni in quella catena merita di essere notato: le sostituzioni di caratteri avvengono sull'output standard, e il padding viene tolto per ultimo, perché toglierlo prima non cambierebbe nulla ma renderebbe il codice più difficile da leggere, e sostituire dopo la rimozione funzionerebbe comunque, ma è così che nascono i bug sottili. Usa questa forma per i token, gli identificatori e qualunque cosa che vivrà in un URL, e riserva l'alfabeto standard per i corpi email, i payload JSON e i file, dove +, / e = stanno perfettamente a casa loro.

Alimentare l'encoder: stringhe, charset e la scelta di codifica

Ogni lavoro di codifica che parte da un testo inizia con la stessa decisione silenziosa: in quali byte diventa questo testo? Il passo Base64 è deterministico e innocente, ma il passo Encoding che lo precede è dove gli output divergono, e la divergenza può essere silenziosa. L'UTF-8 è l'assunzione di default sul web moderno, ed è il default giusto anche qui: fa l'andata e il ritorno per ogni lingua, è ciò che ogni altra piattaforma assumerà quando decodificherà il tuo payload, ed è ciò che Encoding.UTF8 ti dà in una chiamata:

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==

Ora guarda lo stesso carattere codificato attraverso un charset diverso, e vedi perché "lo stesso testo" senza un charset allegato non è una cosa ben definita:

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==

Due stringhe Base64 diverse per lo stesso segno dell'euro, entrambe perfettamente valide, e solo una delle due si decodificherà di nuovo in un segno dell'euro dall'altra parte. La trappola con l'impatto più ampio è Encoding.Default: su .NET Framework su Windows è la code page ANSI del sistema, mentre su .NET (Core) è UTF-8, quindi un programma che codifica con Encoding.Default produce Base64 diverso su una macchina del 2010 rispetto a una del 2025, e entrambi gli output si decodificheranno "correttamente" sulla loro piattaforma di casa. Se un payload decodificato arriva pieno di mojibake accentato, la codifica originale ha usato un charset diverso da quello che la decodifica ha assunto, e la correzione sta da questa parte del tubo: fissa la codifica esplicitamente, in entrambe le direzioni, in codice che supererà di vita la squadra che l'ha scritto. E una nota finale sul sistema di tipi in sé: una stringa C# è UTF-16, quindi se mai passi unità di codice UTF-16 grezze all'encoder (chiamando Encoding.Unicode.GetBytes), ogni carattere ASCII costa due byte e il tuo output raddoppia di dimensione senza alcun beneficio, perché il decoder dall'altra parte lo leggerà come testo UTF-16, non come i byte della tua stringa originale. Il Base64 porta i byte che gli dai, e non gli importa cosa significano.

File: dal disco a una stringa

I file sono il payload di codifica più comune e il più tollerante, perché non c'è questione di charset: i byte sul disco sono i dati, e all'encoder non importa se formano una parola o una forma d'onda. Il giro completo è una lettura, una codifica e una scrittura, e l'unica decisione vera è dove va il risultato:

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.");

I conti sulle dimensioni sono l'intera storia, e valgono la pena di essere fatti prima di scegliere un trasporto. Un megabyte di file diventa 1.333.336 caratteri Base64, e poiché una stringa C# conserva due byte per carattere, quel risultato codificato occupa circa 2,7 megabyte in memoria come stringa. Un file da dieci megabyte diventa una stringa di tredici megabyte che sta in ventisei megabyte di memoria gestita. Per una foto o un blob di configurazione, nessuno di questi è un problema, ed è una ragione molto buona per usare l'encoder in streaming, qui sotto, quando il payload è un video. Il pattern di sopra è quello da usare per qualunque cosa che stia comoda in memoria, ed è anche il pattern che ogni funzionalità "carica un file come Base64 in un corpo JSON" usa silenziosamente: leggi il file, codificalo, metti la stringa nel JSON, e lascia che il livello API faccia il suo lavoro.

Immagini sul web: costruire i Data URI

Il consumatore più visibile delle immagini codificate è il web, e il formato del web per "un'immagine che vive dentro il documento" è il data URI: lo scheme data: seguito dal tipo MIME, un flag ;base64, una virgola e i byte codificati. Costruirne uno in C# è concatenazione di stringhe, e l'encoder fa tutto il lavoro vero:

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

Il prefisso iVBORw0KGgo in quell'output è un checkpoint utile: è la forma Base64 della firma PNG di otto byte, quindi qualunque PNG tu codifichi inizierà così, e qualunque data URI PNG che non inizia così non è un PNG. Tre note pratiche spettano a questo pattern. Prima, il data URI è una copia completa dell'immagine, gonfiata di un terzo, incorporata nel tuo HTML o CSS, quindi scambia una richiesta di rete per peso permanente della pagina, un affare per un favicon da 4 KB e una truffa per un'immagine hero da 4 MB, e l'encoder non negozierà sul 33 percento. Secondo, se l'immagine è grande, ridimensionala o ricomprimila prima di codificarla, perché ogni byte dell'originale compare nella pagina. Terzo, fai attenzione agli SVG forniti dagli utenti in HTML rivolti agli utenti: uno SVG può portare script, quindi incorporarlo - inline, o tramite <object>/<embed> - è una classica superficie XSS. Un semplice data URI in <img> non li eseguirà nei browser moderni, ma lo stesso markup riusato in quei contesti, sì. PNG, JPEG, GIF e WebP nei data URI sono inerti; lo SVG è quello che non lo è.

Assemblare un JWT a mano

Costruire un JSON Web Token da zero è una prova d'iniziazione, e in C# è una prova più gentile che nella maggior parte degli altri linguaggi, perché i pezzi sono corti. Un JWT è tre segmenti base64url uniti da punti: l'intestazione codificata, il payload codificato e la firma. I primi due sono documenti JSON in UTF-8, e la firma è calcolata sui primi due segmenti uniti da un punto. Ecco l'intero assemblaggio, con una firma finta al posto di quella vera, perché il passo crittografico spetta alla tua chiave di firma e non alla storia 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"; // finto valore al posto dell'HMAC o ECDSA reale

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

Due proprietà di Base64Url.EncodeToString fanno lavoro silenzioso in quell'esempio. Emette l'alfabeto URL-safe, quindi né + né / possono comparire nel token, e omette il padding, quindi non compare mai nemmeno =, ed è esattamente ciò che richiede la specifica JWS, ed è esattamente ciò che Convert.ToBase64String non farebbe senza aiuto. Se sei su un runtime precedente a .NET 9, lo stesso lavoro passa per l'encoder standard più la catena di messa a punto della sezione URL-safe: codifica, sostituisci i due caratteri, togli il padding. L'ordine dei segmenti conta per la firma, che è calcolata su header più un punto più payload come byte ASCII grezzi, quindi assembra prima i due segmenti e firma la loro concatenazione esatta, non una versione riformattata del JSON. E un confine da tenere ben distinto: per qualunque cosa che un utente può raggiungere, non assemblare JWT a mano per niente. Il pacchetto System.IdentityModel.Tokens.Jwt gestisce costruzione, firma, validazione e scadenza per te, e la sua gestione del base64url è precisamente questo alfabeto e questa regola di padding. L'assemblaggio a mano è per i test, le demo e il giorno in cui devi capire esattamente cosa fa la libreria.

Header HTTP: autenticazione Basic

Il Base64 compare nell'HTTP semplice nello scheme di autenticazione Basic, e il lato codifica è uno dei costruttori di header più brevi del protocollo: unisci nome utente e password con due punti, codifica il risultato come UTF-8, passalo nel Base64, e prefissalo con il nome dello scheme:

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==

Il charset è la parte delicata: la RFC 7617 lascia indefinito il charset di default dello scheme Basic per retrocompatibilità e offre solo un suggerimento UTF-8 a titolo consultivo, ma è proprio quello che ogni server moderno si aspetta, quindi un nome utente accentato dovrebbe passare per Encoding.UTF8, non per qualunque sia il default della piattaforma, altrimenti il server decodificherà una stringa di byte diversa e rifiuterà il login. Il passo Base64 è l'unica codifica nell'header: non fare percent-encoding del risultato, non URL-encodarlo, non fare doppio Base64. Ciascuno di quei passi extra "utile" è un bug noto, e quello della doppia codifica è il più comune, perché le credenziali a volte arrivano pre-codificate da un livello che le ha già passate nel Base64, e una seconda codifica produce un header che ha un aspetto plausibile e fallisce in silenzio sul server. Due avvertenze sullo scheme in sé, così finiscono qui invece che nella sezione di sicurezza dove sarebbero diluite: l'autenticazione Basic trasmette la password in una forma che è a un comando dalla leggibilità, quindi è accettabile solo su TLS, e anche in quel caso è lo strumento sbagliato per la maggior parte del lavoro API, ed è per questo che i bearer token e i JWT hanno preso il sopravvento. Il lavoro dell'encoder in tutto questo è quello piccolo e onesto: trasformare le credenziali unite da due punti in una stringa sicura per l'header, e nient'altro.

Email: MIME e perché ToBase64Transform non avvolge

L'email è la casa storica del Base64, ed è ancora il posto da cui viene la regola della riga da 76 caratteri: la specifica MIME avvolge i corpi codificati a 76 caratteri con CRLF tra le righe, così che nessun hop SMTP abbia una ragione per riavvolgerli. C# ti dà due encoder per questo lavoro, e fanno promesse diverse, il che vale la pena di capirlo prima di sceglierne uno. Il primo è il classico Convert.ToBase64String con InsertLineBreaks, che hai visto nella sezione sugli a capo, ed è esattamente la forma MIME, avvolta a 76 con CRLF, pronta da incollare sotto un header Content-Transfer-Encoding: base64. Il secondo è ToBase64Transform, il cugino in streaming, ed ecco la sorpresa: non inserisce a capo. Non ha una modalità per loro, non ha un'opzione, non ha un flag del costruttore, e il suo output è un lungo flusso non avvolto:

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");

Quindi la regola pratica è: per payload email piccoli e medi, leggi i byte e usa l'encoder classico che avvolge, perché ottieni la forma MIME direttamente. Per allegati grandi, usa lo streaming con ToBase64Transform per tenere la memoria piatta, e avvolgi tu il risultato se il trasporto ha davvero bisogno di righe da 76 caratteri, dividendo l'output sui confini di gruppo (ogni 76 caratteri, che è sempre un confine di gruppo, come spiegato nella sezione sugli a capo). La transform fa la cosa giusta restando non avvolta: elabora l'input in gruppi di tre byte, e gli a capo sono una decisione di formattazione che spetta al livello che conosce il trasporto, non a quello che converte byte in caratteri in un tubo.

In streaming: codificare file grandi senza leggerli due volte

Quando il payload è un video, un backup, o qualcosa che ti vergognaresti di tenere in una stringa, l'encoder in streaming è la soluzione intera. Il pattern è lo specchio dello streaming lato decodifica: un CryptoStream sul file di sorgente, con ToBase64Transform in modalità lettura, e un CopyTo nel target. Il file scorre dentro, il Base64 scorre fuori, e l'unica memoria che il processo tiene è il buffer che lo stream usa internamente:

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.");

Due fatti su questo pattern valgono la pena di essere tenuti. Primo, la dimensione dell'output è completamente determinata dalla dimensione dell'input, 4 caratteri per 3 byte, quindi puoi riservare lo spazio del target, precalcolare la lunghezza per un header content-length, o riservare una quota di disco prima che un singolo byte scorra. Secondo, la transform si aspetta il suo input in gruppi di tre byte, e CryptoStream gestisce quell'allineamento per te, dando alla transform esattamente ciò che vuole man mano che il file scorre. Se mai guidi la transform a mano con TransformBlock, alimentala con multipli di tre, e lascia che TransformFinalBlock dreni la coda, gli uno o due byte residui che diventano l'ultimo gruppo parziale con i suoi uno o due caratteri di padding. Per la maggior parte delle applicazioni la forma CopyTo è tutto ciò che scriverai mai, ed è la forma che si comporta bene sotto un limite di memoria, che è esattamente dove i file grandi amano vivere.

Configurazione, variabili d'ambiente e database

L'altro lavoro di codifica comune nelle applicazioni C# è il lavoro di storage: prendere un segreto o un blob binario e metterlo in un posto che accetta solo testo. Le variabili d'ambiente sono l'esempio visibile, perché una variabile d'ambiente è, per definizione, una stringa:

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

In un database la stessa idea di solito compare come una proprietà byte[] che una colonna di testo deve contenere, e Entity Framework Core ha un meccanismo built-in esattamente per questo, un convertitore di valori che esegue le tue funzioni di codifica e decodifica a ogni lettura e scrittura:

using Microsoft.EntityFrameworkCore;

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

Quel convertitore è l'intera integrazione col database: il codice C# vede un byte[], la colonna vede una stringa Base64, e il giro completo è invisibile nel punto di chiamata. Due avvertenze spettano a questa sezione. Prima, la colonna paga la tassa del 33 percento: una colonna di testo dimensionata per la lunghezza codificata contiene un terzo di dati in meno della stessa larghezza come binario, quindi se hai una colonna a larghezza fissa, dimensionala per la lunghezza Base64, e se hai un varchar(max) o equivalente, la tassa è solo un problema di fatturazione. Secondo, ed è quella che torna sempre, il Base64 in un file di configurazione è una forma, non uno scudo. Tiene il valore su una riga, lo tiene fuori dai piedi degli editor di testo, ed è a un comando dalla leggibilità da chiunque può leggere il file. I segreti hanno bisogno di protezione vera, un secret store, un key vault, al minimo i permessi di file, e il Base64 è solo il formato di trasporto che il segreto indossa mentre sta nella configurazione.

Dalla riga di comando

Ogni encoder merita una vita console di 15 righe, e quello C# è piacevole perché l'output è una stringa semplice per cui lo standard output è stato fatto. Ecco lo strumento intero: prende un percorso di file o lo standard input, lo codifica, e scrive il Base64 nel terminale, dove qualunque pipeline del shell può prenderlo:

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));

Compilalo una volta e sta accanto all'utilità base64 del shell stesso per i giorni in cui vuoi specificamente il comportamento dell'encoder .NET: lo stesso alfabeto, lo stesso padding, e la gestione UTF-8 del runtime C# di qualunque cosa il pipe gli passi. Per i file binari lo stesso scheletro con File.ReadAllBytes al posto di File.ReadAllText è l'intero cambiamento, e l'output allora descrive i byte esatti del file piuttosto che la sua interpretazione testuale. Lo strumento è anche una buona sonda: fai passare un file attraverso di esso, fai passare l'output di nuovo attraverso il decoder dell'articolo sulla decodifica, e fai il diff dei due file, il che è un controllo end-to-end soddisfacente che entrambi i lati del pipe sono d'accordo su ogni byte.

Padding, o i caratteri = finali

I caratteri = finali di una stringa Base64 sono la contabilità del formato, e gli encoder C# non sono d'accordo su di loro, il che è la fonte di un bug di interop specifico e comune. Il classico Convert.ToBase64String fa sempre il padding, perché il decoder classico con cui fa coppia se lo aspetta sempre. Base64Url.EncodeToString non fa mai il padding, perché i consumatori URL-safe che mira, i JWT e le API di token, si aspettano sempre la forma compatta. Quando il tuo output entra in un mondo con l'aspettativa opposta, la correzione è aritmetica, ed è la stessa aritmetica che l'articolo sulla decodifica ha mostrato per la direzione inversa:

using System;

string padded = Convert.ToBase64String(new byte[] { 1, 2 });
Console.WriteLine(padded);           // AQI=
Console.WriteLine(padded.TrimEnd('=')); // AQI, ciò che un consumatore URL-safe vuole

string compact = "AQI";
string restored = compact + new string('=', (4 - compact.Length % 4) % 4);
Console.WriteLine(restored);         // AQI=, ciò che un decoder classico vuole

La formula (4 - length % 4) % 4 è l'intero universo del padding: aggiunge zero, uno o due caratteri per far atterrare la lunghezza su un multiplo di quattro, e il modulo esterno impedisce a un input già con padding di prenderne altri. Due avvertenze sul padding, perché è lì che il codice ben intenzionato sbaglia. Non trattare mai il = come dati: non porta informazione, quindi codificare una stringa che contiene già padding come se fosse payload, o fare URL-encoding del = in %3D dentro una query string, sono entrambi modi per produrre un output che ha l'aspetto giusto e si decodifica sbagliato. E diffida della piccola famiglia di payload legacy dove il padding era scritto come un carattere diverso, un punto in alcuni sistemi più vecchi, al posto dello standard =: se un valore che ricevi usa un punto dove ti aspetti il padding, normalizzalo di nuovo a = prima di decodificare, o passalo per il percorso URL-safe senza padding.

Quanto è veloce

La codifica Base64 nel .NET moderno è veloce, e la parte interessante è la storia della memoria, non quella della CPU. Le implementazioni del runtime sono ottimizzate con istruzioni vettoriali SIMD dove l'hardware le supporta, e input da diversi megabyte si codificano in millisecondi a una cifra o a doppia cifra bassa su una macchina desktop ordinaria, abbastanza veloce da rendere l'encoder di fatto gratuito in qualsiasi applicazione che scriverai. Il consiglio sulle performance che cambia davvero il codice riguarda la forma. L'output è una stringa C#, e una stringa C# conserva due byte per carattere, quindi il costo in memoria di un risultato codificato è circa 2,7 byte per byte di input (4 caratteri per 3 byte di input, a 2 byte per carattere), il che è un numero che vale la pena conoscere quando il payload è nei megabyte. Se stai codificando migliaia di piccoli payload in un loop, preferisci le API span e buffer di caratteri, che scrivono in buffer che riutilizzi, alle API stringa, che allocano una stringa gestita fresca per chiamata. Se stai codificando un singolo file grande, salta la stringa del tutto e usa la transform in streaming, perché il costo di 2 byte per carattere di tenere una stringa da 13 megabyte è puro spreco quando un CopyTo avrebbe tenuto il set di lavoro nei buffer dello stream. E se stai producendo output avvolto in MIME, ricorda che il passo di avvolgimento è una seconda corsa sui dati, quindi avvolgi solo quando il trasporto lo richiede, non come default.

La conversazione sulla sicurezza

Il lato encoder del Base64 ha una lezione di sicurezza, ed è l'inverso di quella del decoder: sei tu a fare la scelta di esporre dati leggibili, e il formato non ti fermerà. Il Base64 è codifica, non cifratura. Non ha chiave, non ha algoritmo, e non ha segretezza di alcun tipo, e l'output della tua chiamata a ToBase64String è a un comando di distanza dall'input, su qualunque macchina, in qualunque linguaggio, da chiunque. Quindi la prima regola riguarda ciò che scegli di codificare: non mettere mai una password, un token o un segreto in un file di configurazione "protetto" dal Base64, perché la protezione è profonda esattamente una chiamata di decodifica, e la persona che legge la configurazione ha il comando. Se il valore deve essere segreto, ha bisogno di protezione vera, e il Base64 è solo la forma che indossa mentre sta nel campo di testo.

La seconda lezione riguarda il canale, ed è specifica per le cose che questo articolo costruisce. Un header di autenticazione Basic porta la password in una forma che qualunque proxy, qualunque log e qualunque middlebox può leggere, ed è per questo che lo scheme è accettabile solo su TLS e per lo più obsoleto al di fuori delle integrazioni legacy. Un data URI in HTML porta l'immagine, e se l'immagine è uno SVG fornito dall'utente, porta qualunque cosa lo SVG porti, ed è per questo che il caso SVG-in-data-URI richiede la stessa cura di qualunque contenuto utente. E un valore Base64 in un URL è, letteralmente, nell'URL, il che significa che è nella cronologia del browser, nel log di accesso del server, nell'header referrer e nella cache del proxy, quindi i token che devono restare privati non stanno nelle query string, con padding o senza. L'encoder fa il suo lavoro onesto in tutti e tre i casi, trasformando byte in una stringa sicura da portare. La sicurezza sta in cosa porti, e dove, e il formato è un messaggero migliore della maggior parte, ma è un messaggero, non una cassaforte.

Le insidie in cui cadono gli encoder C#

Queste sono le trappole che continuano a comparire sul lato codifica del codice C#, e ognuna ha una causa concreta nel modo in cui il framework funziona:

  • Il charset che non hai scelto. Codificare una stringa con Encoding.Default produce Base64 diverso su .NET Framework (la code page ANSI di Windows) e su .NET (UTF-8). Gli output sono entrambi validi, entrambi si decodificano "correttamente" sulla loro piattaforma di casa, e non sono gli stessi byte. Fissa la codifica esplicitamente.
  • Doppia codifica. L'input era già Base64 (una configurazione che ha codificato un valore già codificato, un'API che ricodifica il suo input), e l'encoder, facendo esattamente quello che gli è stato detto, ha prodotto Base64-di-Base64. Il risultato ha un aspetto plausibile e si decodifica un livello alla volta, ed è così che un bug che richiede due decodifiche per essere corretto viene scoperto in produzione.
  • A capo nel posto sbagliato. La forma avvolta in MIME, con le sue coppie CRLF, finisce in una stringa JSON, in un segmento JWT, o in un parametro URL, dove il consumatore rigoroso si incaglia sugli spazi bianchi di cui non gli è mai stato detto di aspettarseli. Avvolgi per la posta, lascia stare dappertutto altrove, e se togli l'avvolgimento di qualcun altro, togli il \r oltre che il \n.
  • Alfabeto standard in un URL. Un + in una query string viene decodificato come spazio dalle regole di parsing dei form, quindi un valore Base64 standard messo in un URL torna con lettere dove c'erano i segni più. Usa l'alfabeto URL-safe, o fai percent-encoding dell'intero valore, e mai entrambe le cose.
  • Il disallineamento del padding. Il tuo output è con padding, il consumatore vuole compatto, o il contrario, e nessuno dei due lati ha torto - semplicemente non sono d'accordo. La correzione è l'aritmetica della sezione sul padding, applicata sul lato che conosce l'aspettativa del consumatore, che di solito è il lato che scrive il token.
  • Memoria non prevista. La stringa codificata è due byte per carattere in memoria, quindi un file da 10 MB diventa una stringa di 13 milioni di caratteri che pesa circa 27 MB in memoria gestita, e un loop che costruisce tali stringhe una alla volta compare nel profiler come churn di allocazioni senza causa visibile. Dimensiona i buffer con gli helper di lunghezza, fai lo streaming dei grandi, riutilizza i buffer nei loop caldi.
  • La transform che non avvolge. ToBase64Transform emette una lunga riga. Un codice che fa scorrere un allegato "pronto per MIME" attraverso di essa e poi lo manda per posta produce una riga di 120.000 caratteri che qualche trasporto riavvolgerà in mezzo a un gruppo, che è esattamente la corruzione che la regola dei 76 caratteri era progettata per prevenire.
  • Codificare la codifica. Passare una stringa Base64 all'encoder perché "i dati sono già testo" produce un secondo livello. L'encoder non sa, e non gli importa, che il suo input sembra Base64; codifica quanti caratteri la stringa ha per caso, e il decoder dall'altra parte ottiene una stringa Base64 dove si aspettava i tuoi dati.

Come è cresciuto l'encoder: un giro tra le versioni

Il lato codifica dell'API ha la sua linea del tempo, e va dal secondo rilascio .NET a quello attualmente in preview:

  • .NET Framework 1.1, aprile 2003. Arrivano Convert.ToBase64String e ToBase64CharArray, l'intera famiglia classica in un singolo rilascio, con i sovraccarichi a fetta già inclusi, il che è un piccolo miracolo di lungimiranza per un'API del 2003.
  • .NET 2.0, 2005. Base64FormattingOptions e il valore InsertLineBreaks si uniscono alla famiglia, portando l'avvolgimento di righe MIME nel framework e chiudendo un'era di loop Substring fatti a mano nel codice email.
  • .NET Core 2.1, 2018. L'era degli span. Convert ottiene la codifica basata sugli span e TryToBase64Chars, e la nuova classe System.Buffers.Text.Base64 arriva con il suo contratto OperationStatus e il gonfiaggio in loco, costruita per il mondo a zero allocazioni.
  • .NET 5, 2020. Spediscono i fratelli hex (Convert.ToHexString e soci), lo stesso pattern di design applicato a un alfabeto di 16 simboli, e il pattern della classe di conversione diventa uno stile di casa.
  • .NET 7, 2022. X509Certificate2.ExportCertificatePem fa produrre il PEM al framework per te, marker dell'armatura, avvolgimento a 64 caratteri e corpo Base64 inclusi, il che fa ritirare in silenzio una classe di codice di formattazione dei certificati fatto a mano.
  • .NET 9, novembre 2024. System.Buffers.Text.Base64Url entra in dotazione dopo anni di richieste della comunità, con il pacchetto Microsoft.Bcl.Memory che lo retroporta su .NET Framework 4.6.2 e successivi, e il comportamento senza padding che il codice JWT aveva costruito a mano per tutto il tempo.
  • .NET 11, in preview al momento della scrittura. Il prossimo rilascio, atteso per la fine del 2026, aggiunge altre API di comodità Base64 e sovraccarichi ai tipi esistenti, continuando la marcia verso una superficie più ergonomica.

Il formato in sé ha una biografia più vecchia, ed è la ragione per cui l'API C# ha l'aspetto che ha. Il primo uso standardizzato di ciò che oggi chiamiamo MIME Base64 è stato il protocollo Privacy-Enhanced Mail nel 1987 (RFC 989), la specifica MIME ha fissato la forma con a capo da 76 caratteri nel 1993, e la RFC 4648 nel 2006 ha dato al formato la sua specifica moderna, sensibile all'alfabeto, inclusa la variante URL-safe per cui C# ha avuto un encoder di prima classe solo nel 2024. Tre decenni di convenzioni email e web sono la ragione per cui a capo, padding e i due alfabeti esistono tutti, e l'encoder C# è il posto dove tutti e tre si incontrano.

Piccole meraviglie

  • Il minimo di quattro caratteri. Il più piccolo output Base64 non vuoto possibile è di quattro caratteri, perché il formato pensa in gruppi di quattro anche quando gli dai un byte. Un byte di qualunque cosa si codifica in due lettere e due segni =, e quella forma, due caratteri di dati con un cappello di padding, è un'impronta che inizierai a riconoscere in configurazioni e token.
  • Gli zero sono i benvenuti. L'encoder non ha un'opinione su cosa significhino i byte, quindi un buffer pieno di zeri si codifica allegramente in una parete di caratteri A, e un file binario con i suoi byte NUL intatti fa l'andata e il ritorno senza perderne nemmeno uno. L'ansia "le stringhe non possono contenere il binario" spetta al lato stringhe del sistema di tipi, non all'encoder, che non vede mai una stringa.
  • La determinatezza come funzionalità. Gli stessi byte, le stesse opzioni, sempre la stessa stringa. Nessun timestamp, nessun sale casuale, nessuna variazione, ed è per questo che una stringa Base64 fa da impronta rapida e sporca usabile dei contenuti di un file: due file con lo stesso Base64 sono lo stesso file, e il controllo è un confronto di stringhe.
  • Due byte per carattere, a costo zero. Una stringa C# è UTF-16, quindi ogni carattere nel tuo output Base64 occupa due byte in memoria gestita. L'encoder non lo annuncia, la proprietà di lunghezza non lo riporta, e una stringa di 13 milioni di caratteri pesa semplicemente 26 MB, il numero da tenere in testa quando il payload è grande.
  • CRLF per eredità. L'avvolgimento MIME inserisce coppie ritorno a capo + a capo anche quando il tuo codice gira su Linux, perché la regola viene dalla specifica email, non dalla piattaforma. L'encoder è uno storico tanto quanto un convertitore, e conserva le fine riga del 1993 su una macchina del 2026.
  • Un sovraccarico a fetta fin dal primo giorno. ToBase64String(byte[], int, int) codifica una finestra su un array più grande dal 2003, quindici anni prima che gli span rendessero l'idea alla moda. I progettisti di API dell'era 1.1 hanno guardato buffer reali e aggiunto la forma offset-e-lunghezza, ed è ancora la mossa giusta quando i dati sono una sezione di una lettura più grande.
  • La riga di certificato da 64 caratteri. Il PEM avvolge a 64 caratteri, non 76, e ExportCertificatePem lo sa e avvolge di conseguenza, il che è uno dei dettagli silenziosi che rende "lascia fare al framework" il consiglio giusto per il lavoro con i certificati. Due larghezze di avvolgimento, una famiglia di formati, e il framework li tiene dritti.
  • Due alfabeti, due nomi. I 64 valori si chiamano "standard" in una parte dell'API e "URL-safe" in un'altra, e differiscono in esattamente due caratteri: la 62a e la 63a casella. + e / da una parte, - e _ dall'altra, e ogni bug di interop di questo articolo vive nel momento in cui qualcuno ha dato per scontato che i due lati fossero la stessa cosa.

Il cerchio si chiude

Questo è il lato encoder, ed è dove prendi le decisioni: l'alfabeto, il padding, gli a capo, il charset, il buffer. L'altra direzione, ricevere Base64 dagli altri, con le loro scelte di padding, i loro a capo, i loro alfabeti e i loro token, è dove vive la maggior parte del dolore, perché non si può negoziare con un payload. La decodifica Base64 in C#, dalla classica riga sola alle famiglie span e URL-safe, è coperta in profondità nell'articolo compagno linkato qui sotto, e tra i due l'intera materia sta nella tua memoria di lavoro, che è il punto di un formato così vecchio e così piccolo.

Ultimo aggiornamento: 2026-09-08

Articolo correlato: Decodifica Base64 in C# (CSharp): una guida completa