Werk je met Base64-indeling? Dan is deze site perfect voor jou! Gebruik onze handige online tool om je gegevens te coderen of te decoderen.

Base64-codering in C# (CSharp): een complete gids

Je hebt de bytes. Een PNG die een JSON-antwoord door moet reizen, een token dat in een URL moet passen, een regel tekst die binnenkort een systeem ingaat dat alleen letters en cijfers accepteert. Ergens tussen de byte[] in je hand en het kanaal dat die moet oversteken, biedt C# een menu van Base64-encoders, en kiezen uit hen is de echte vaardigheid van dit onderwerp. De klassieke éénregelaar zit al sinds 2003 in de standaardbibliotheek, de span-gebaseerde en URL-safe opties kwamen met de moderne runtimes, en elk maakt andere beloftes over grootte, regelafbrekingen en alfabet. Dit artikel loopt het hele menu door, met werkende voorbeelden voor elke echte klus waar een encoder om wordt gevraagd.

Eerst de huisregels, in één adem, want de startpagina van deze site legt het formaat diep uit: de encoder pakt elke drie bytes en schrijft vier tekens uit een alfabet van 64 symbolen, en vult de staart op met één of twee =-tekens, zodat je data ongeveer 33 procent vetter vertrekt dan ze arriveerde. Dat getal, niet de code, is het belangrijkste feit in dit artikel, en alles hieronder gaat over het slim betalen ervan.

Het encodermenu: kies je gereedschap

Hier is de complete familie van encodings-API's in de .NET-wereld, met de situatie waarvoor elk ervan is gemaakt. Alles zit in de runtime zelf, behalve de URL-safe klasse op oudere frameworks, die er via een klein NuGet-pakket bijkomt:

API Beschikbaar sinds Waar het voor is
Convert.ToBase64String(byte[]) .NET Framework 1.1 (2003) Het klassieke. Hele array erin, gepadde string eruit. Geen opties, geen verrassingen.
Convert.ToBase64String(byte[], int, int) .NET Framework 1.1 (2003) Een stuk van een grotere array encoderen, zonder het eerst uit te kopiëren.
Convert.ToBase64String(byte[], Base64FormattingOptions) .NET 2.0 (2005) Het klassieke met een knop: optioneel een regelafbreking invoegen elke 76 tekens, op de MIME-manier.
Convert.ToBase64String(ReadOnlySpan<byte>, Base64FormattingOptions) .NET Core 2.1 (2018) De span-versie: een weergave van een buffer encoderen, geen arraykopie, geen stuk-allocatie.
Convert.ToBase64CharArray(byte[], int, int, char[], int) .NET Framework 1.1 (2003) Schrijven naar een karakterbuffer die je bezit, en terugkrijgen hoeveel tekens er werden gebruikt.
Convert.TryToBase64Chars(ReadOnlySpan<byte>, Span<char>, out int, ...) .NET Core 2.1 (2018) Een booleaanse waarde in plaats van uitzonderingen: encodeer als de buffer past, rapporteer false als dat niet zo is.
System.Buffers.Text.Base64.EncodeToUtf8, EncodeToUtf8InPlace .NET Core 2.1 (2018) De strikte span-familie: statuscodes in plaats van uitzonderingen, en opzwellen in-place voor buffers die je al bezit.
System.Buffers.Text.Base64Url.EncodeToString en zustervormen .NET 9 (2024) Het URL-safe alfabet, uitgegeven zonder padding. Op .NET Framework 4.6.2+ en .NET Standard 2.0: het Microsoft.Bcl.Memory NuGet-pakket.
ToBase64Transform + CryptoStream .NET Framework 1.1 (2003) Streamen: een bestand encoderen terwijl het stroomt, de hele payload nooit in het geheugen houden.

Als je project op een .NET-versie vanaf 2018 doelt, staan de eerste zeven rijen en het streamende paar onderaan er al in de standaardbibliotheek. Base64Url heeft .NET 9 of nieuwer nodig, of het Microsoft.Bcl.Memory-pakket op alles ouder. En een vooruitblik: de .NET 11-bibliotheken, bij het schrijven in preview met een algemene release verwacht in eind 2026, voegen verdere Base64-gemak-API's en overloads toe aan de bestaande types, dus het menu blijft groeien. Geen ander pakket in dit artikel is vereist.

De standaardaanroep: Convert.ToBase64String

Negentig procent van het encoderingsleven in C# is een enkele aanroep. Geef hem bytes, en hij geeft je de string terug die ze draagt:

using System;
using System.Text;

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

Merk de twee-stapsvorm op, want dit is de meest gangbare vraag "waarom komt mijn Base64 niet overeen" in C#. Er is geen overload die een string direct neemt, en dat is zo ontworpen: een C#-string is UTF-16, en de framework weigert te raden welke bytes je bedoelde toen je "encodeer deze tekst" zei. Je kiest eerst de bytevertegenwoordiging, met Encoding.UTF8.GetBytes (of welke charset de data in werkelijkheid is), en pas daarna gebeurt de Base64-stap. De rest van de klassieke familie is dezelfde aanroep met een nauwere taille: de (byte[], int, int)-overload encodeert een stuk van een buffer zonder het stuk uit te kopiëren, en de span-overload doet hetzelfde vanaf een ReadOnlySpan<byte>, wat het juiste gereedschap is wanneer de data een venster is in een grotere leesbuffer. Nog een eigenschap van de klassieke encoder verdient een platte uitspraak: hij faalt nooit en vraagt nooit. Hij geeft altijd het standaardalfabet uit, bevat altijd padding, en geeft je altijd dezelfde string voor dezelfde input, dus een Base64-string is een betrouwbare vingerafdruk van de bytes die hem produceerden.

De 76-karaktersvraag: regelafbrekingen en Base64FormattingOptions

Er is één knop op de klassieke encoder, en die is er al sinds .NET 2.0: Base64FormattingOptions. Zet hem op InsertLineBreaks en de encoder voegt een regelafbreking in na elke 76 tekens aan output, de regellengte die de MIME-specificatie gebruikt voor e-mailbijlagen. Zet hem op None, of gebruik de overloads zonder de optie, en je krijgt één lange, onafgebroken string:

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, één regelafbreking toegevoegd na karakter 76

Twee details over die knop tellen in de praktijk. Eerst is de regelafbreking die hij invoegt het Windows-paar, carriage return plus line feed, niet een kale line feed. De afgebroken output bevat dus \r\n-sequenties, en elke code die de string later "opruimt" door alleen \n te verwijderen, zal met verdwaalde carriage returns zitten die in de data schuilen. Ten tweede gebeurt het afbreken op 76 tekens aan geëncodeerde output, en daarom kon de MIME-standaard garanderen dat e-mailtransport, met zijn regellimieten van 76 of 78 tekens, nooit een groep van vier Base64-tekens over regels zou splitsen: 76 is een veelvoud van vier, dus elke regel eindigt op een groepsgrens. Je wilt de afgebroken vorm wanneer je e-maillichamen produceert, PEM-stijl tekstblokken, of iets dat een legacy-mailpipeline zal dragen. Je wilt de niet-afgebroken vorm overal anders: JSON-payloads, URL-tokens, API-antwoorden, en bestanden die door een strikte parser gedecodeerd worden die verrassingen niet leuk vindt. En binnen een JWT wil je de afgebroken vorm nooit, waar de specificatie regelafbrekingen, witruimte en zelfs padding expliciet verbiedt.

Zelf de output bezitten: char-buffers en de Try-API's

Soms is de string niet het doel, de buffer wel. Je voegt toe aan een karakterarray van vaste grootte, je schrijft in een protocolframe, of je wilt simpelweg niet dat de runtime de output voor je alloceert. Voor die momenten heeft de encoder een char-buffer-modus gehad sinds de 1.1-dagen, en een Try-modus sinds het span-tijdperk. De char-buffermethode schrijft naar een array die je aanlevert en vertelt je hoeveel tekens hij gebruikte, dus het afmeten van de buffer is jouw werk, en de standaardlibrary reikt je zelfs de afmetingsformule aan:

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

De Try-zuster doet hetzelfde werk vanaf spans en antwoordt met een booleaanse waarde. Ze encodeert de input in je bestemmingsspan, rapporteert het aantal tekens in de out-parameter, en geeft false terug als de bestemming te klein was, zonder iets te schrijven. Die laatste eigenschap maakt hem veilig te gebruiken met onvertrouwde ingangsgroottes: je krijgt nooit een halfgevulde buffer uit een mislukte aanroep:

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

Voor de strikte span-familie in System.Buffers.Text.Base64 bestaat dezelfde vorm met het OperationStatus-contract in plaats van een booleaanse waarde: EncodeToUtf8 vult een bytespan die je bezit en vertelt je, via de status, of hij klaar was, de ruimte op was, of meer input nodig heeft, en EncodeToUtf8InPlace is de waarnaar je grijpt wanneer de binaire data al in een buffer ligt die je bereid bent te laten groeien: encoderen blaast de data op, dus de methode schrijft de Base64-tekst over het einde van dezelfde buffer en rapporteert hoe lang het resultaat is. Ze delen allemaal één regel over afmeten: de output voor n inputbytes is altijd 4 * ceil(n / 3) tekens inclusief padding, en de GetMaxEncodedToUtf8Length- en Base64Url.GetEncodedLength-helpers implementeren die rekenkunde - de laatste voor de lengte zonder padding, die altijd de gepadde grootte of korter is - dus meet af vanaf de helpers en nooit vanaf een onthouden constante.

De URL-safe encoder: Base64Url

Het standaardalfabet heeft twee tekens die URLs niet liefhebben. De + in een query string wordt routinematig gedecodeerd als spatie door form-parseregels, en zowel / als = willen percent-encoding voordat ze in een path of een parameter kunnen rijden. De URL-safe variant van Base64, gedefinieerd in sectie 5 van RFC 4648, wisselt + en / om voor - en _, die nergens escaping nodig hebben, en maakt de hangende =-padding optioneel. Sinds .NET 9 heeft de runtime een speciale klasse voor het, System.Buffers.Text.Base64Url, en die heeft één gedrag dat mensen de eerste keer verast: hij geeft helemaal geen padding uit:

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

Dat verschil is het hele punt. Een JWT-segment, een upload-identifier, een token in een query string, een waarde in een URL-path: ze willen allemaal de URL-safe vorm zonder padding, en Base64Url.EncodeToString geeft die direct, met zowel het alfabet als de padding behandeld zoals die formaten het specificeren. De klasse heeft de volledige familie, encoderen naar string, naar characterspan, en naar UTF-8 bytespan, plus GetEncodedLength voor bufferafmeten en IsValid voor het valideren van input bij binnenkomst. Als je project op een oudere runtime draait, voeg je het Microsoft.Bcl.Memory-pakket toe, dat Microsoft publiceert om de klasse terug te poorten naar .NET Framework 4.6.2 en hoger:

dotnet add package Microsoft.Bcl.Memory

En als je het pakket niet kunt gebruiken, is de zelfgemaakte versie de klassieke encoder plus twee replaces en een trim, wat je in een grote hoeveelheid C#-codebases zult tegenkomen:

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 en zonder padding

De volgorde van bewerkingen in die keten verdient opmerking: de karakterwissels gebeuren op de standaardoutput, en de padding wordt als laatste weggeknipt, want eerst knippen zou niets veranderen maar de code lastiger leesbaar maken, en wisselen na het knippen zou ook werken maar zo komen subtiele bugs ter wereld. Gebruik deze vorm voor tokens, identifiers, en alles wat in een URL zal wonen, en bewaar het standaardalfabet voor e-maillichamen, JSON-payloads en bestanden, waar +, / en = zich perfect thuis voelen.

De encoder voeden: strings, charsets en de encodingkeuze

Elke encoderingsklus die begint bij tekst begint met dezelfde stille beslissing: welke bytes wordt deze tekst? De Base64-stap is deterministisch en onschuldig, maar de Encoding-stap ervoor is waar outputs uiteenlopen, en dat uiteenlopen kan stil zijn. UTF-8 is de standaard-aannaming op het moderne web, en het is hier de juiste standaard: het rondreist elke taal, het is wat elk ander platform zal aannemen wanneer het je payload decodeert, en het is wat Encoding.UTF8 je in één aanroep geeft:

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

Zie nu hetzelfde karakter geëncodeerd via een andere charset, en zie waarom "dezelfde tekst" geen goed gedefinieerd ding is zonder eraan verbonden charset:

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

Twee verschillende Base64-strings voor hetzelfde euro-teken, allebei volstrekt geldig, en alleen één van hen zal aan de andere kant terug decoderen naar een euro-teken. De val met de grootste impact is Encoding.Default: op .NET Framework onder Windows is het de ANSI-codepagina van het systeem, terwijl het op .NET (Core) UTF-8 is, dus een programma dat encodeert met Encoding.Default produceert andere Base64 op een machine uit 2010 dan op een uit 2025, en beide outputs decoderen "correct" op hun thuisplatform. Als een gedecodeerde payload aankomt vol mojibake met accentletters, gebruikte de oorspronkelijke encoding een andere charset dan de decode aannam, en de fix zit aan deze kant van de pipe: fixeer de encoding expliciet, in beide richtingen, in code die langer zal leven dan het team dat haar schreef. En een laatste notitie over het typesysteem zelf: een C#-string is UTF-16, dus als je ooit rauwe UTF-16-code-eenheden aan de encoder geeft (door Encoding.Unicode.GetBytes aan te roepen), kost elk ASCII-karakter twee bytes en verdubbelt je output in grootte zonder voordeel, want de decoder aan de andere kant zal het lezen als UTF-16-tekst, niet als de bytes van je oorspronkelijke string. Base64 draagt de bytes die je hem geeft, en het maakt hem niet uit wat ze betekenen.

Bestanden: van schijf naar string

Bestanden zijn de meest gangbare encoderingspayload en de meest vergevingsgezinde, want er is geen charset-vraag: de bytes op schijf zijn de data, en de encoder maakt zich niks van of ze een woord of een golfvorm spellen. De rondreis is lezen, encoderen en schrijven, en de enige echte beslissing is waar het resultaat naartoe gaat:

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

De groottenrekening is het hele verhaal, en het loont om ze te doen voordat je een transport kiest. Eén megabyte bestand wordt 1.333.336 Base64-tekens, en omdat een C#-string twee bytes per teken opslaat, belegt dat geëncodeerde resultaat als string ongeveer 2,7 megabyte in het geheugen. Een bestand van tien megabyte wordt een string van dertien megabyte die in zesentwintig megabyte beheerd geheugen zit. Geen van dat is een probleem voor een foto of een config-blob, en het is een zeer goede reden om de streamende encoder hieronder te gebruiken wanneer de payload een video is. Het bovenstaande patroon is het waarnaar je grijpt voor alles dat comfortabel in het geheugen past, en het is ook het patroon dat elke "upload een bestand als Base64 in een JSON-lichaam"-functie stil gebruikt: het bestand lezen, het encoderen, de string in het JSON zetten, en de API-laag haar werk laten doen.

Afbeeldingen op het web: data-URIs bouwen

De meest zichtbare consument van geëncodeerde afbeeldingen is het web, en het formaat van het web voor "een afbeelding die in het document woont" is de data-URI: een data:-schema gevolgd door de MIME-type, een ;base64-flag, een komma, en de geëncodeerde bytes. Eentje bouwen in C# is string-concat, en de encoder doet al het echte werk:

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

Het iVBORw0KGgo-prefix in die output is een nuttig controlepunt: het is de Base64-vorm van de acht-byte PNG-signatuur, dus elke PNG die je encodeert begint zo, en elke PNG-data-URI die dat niet doet is geen PNG. Bij dit patroon horen drie praktische notities. Eerst is de data-URI een volledige kopie van de afbeelding, opgeblazen met een derde, ingebed in je HTML of CSS, dus hij ruilt een netwerkrequest in voor permanente paginagrootte, een koopje voor een favicon van 4 KB en een afrekening voor een banner-afbeelding van 4 MB, en de encoder onderhandelt niet over de 33 procent. Ten tweede, als de afbeelding groot is, hergroot of hercomprimeer hem dan voordat je hem encodeert, want elke byte van het origineel verschijnt in de pagina. Ten derde, wees voorzichtig met door gebruikers aangeleverde SVG in voor gebruikers zichtbare HTML: een SVG kan script dragen, dus hem inbedden - inline, of via <object>/<embed> - is een klassiek XSS-oppervlak. Een gewone <img>-data-URI zal hem in moderne browsers niet uitvoeren, maar dezelfde markup hergebruikt in die contexten wel. PNG, JPEG, GIF en WebP in data-URIs zijn inactief; SVG is de een die dat niet is.

Een JWT handmatig in elkaar zetten

Een JSON Web Token van scratch bouwen is een toetredingsritueel, en het is in C# een beter ritueel dan in de meeste talen, want de onderdelen zijn kort. Een JWT is drie base64url-segmenten die met punten zijn gelijmd: de geëncodeerde header, de geëncodeerde payload, en de signatuur. De eerste twee zijn UTF-8 JSON-documenten, en de signatuur wordt berekend over de eerste twee segmenten die met een punt zijn gelijmd. Hier is de hele assemblage, met een vervangende signatuur, want de cryptografische stap hoort bij je ondertekeningssleutel en niet bij het Base64-verhaal:

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"; // vervanger van de echte HMAC- of ECDSA-waarde

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

Twee eigenschappen van Base64Url.EncodeToString doen stil werk in dat voorbeeld. Hij geeft het URL-safe alfabet uit, dus noch + noch / kan in het token verschijnen, en hij laat de padding weg, dus verschijnt er ook nooit een =, wat precies is wat de JWS-specificatie vereist, en precies wat Convert.ToBase64String zonder hulp niet zou doen. Als je op een runtime vóór .NET 9 zit, loopt hetzelfde werk door de standaardencoder plus de herstelketen uit de URL-safe-sectie: encoderen, de twee tekens wisselen, de padding wegnippen. De volgorde van de segmenten telt voor de signatuur, die wordt berekend over header plus een punt plus payload als kale ASCII-bytes, dus zet de twee segmenten eerst in elkaar en teken hun exacte concatenatie, niet een heropgezette versie van het JSON. En een grens om scherp te houden: voor alles waartoe een gebruiker kan komen, zet je JWTs helemaal niet handmatig in elkaar. Het System.IdentityModel.Tokens.Jwt-pakket handelt bouwen, ondertekenen, validatie en vervaldatum voor je af, en zijn base64url-behandeling is precies dit alfabet en deze paddingregel. Handmatige assemblage is voor tests, demos, en de dag dat je exact moet begrijpen wat de library doet.

HTTP-headers: Basic auth

Base64 verschijnt in gewoon HTTP in het Basic-authenticatieschema, en de encoderingskant is een van de kortste headerbouwers in het protocol: lijm de gebruikersnaam en het wachtwoord met een dubbele punt, encodeer het resultaat als UTF-8, zet het in Base64, en voeg de schemanaam als prefix toe:

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

De charset is het knagende deel: RFC 7617 laat de standaardcharset van het Basic-schema ongedefinieerd voor achterwaartse compatibiliteit en biedt alleen een adviserende UTF-8-aanwijzing, maar dat is precies wat elke moderne server verwacht, dus een gebruikersnaam met een accentletter moet door Encoding.UTF8, niet door wat ook maar de platformstandaard is, anders decodeert de server een andere bytestring en weigert de inlog. De Base64-stap is de enige encoding in de header: percent-encodeer het resultaat niet, URL-encodeer het niet, en dubbel-Base64 het niet. Elke van die "helpful" extra stappen is een bekende bug, en de dubbele-codering is de meest gangbare, want de inloggegevens komen soms al geëncodeerd aan van een laag die ze al in Base64 had gestopt, en een tweede encode produceert een header die plausibel lijkt en stil faalt op de server. Twee waarschuwingen over het schema zelf, zodat ze hier landen in plaats van in de beveiligingssectie waar ze zouden worden verwaterd: Basic auth zendt het wachtwoord door in een vorm die met één commando leesbaar is, dus is het alleen aanvaardbaar over TLS, en zelfs dan is het het verkeerde gereedschap voor de meeste API-werk, en daarom namen bearer-tokens en JWTs over. Het werk van de encoder in al dit is het kleine, eerlijke: zet de met een dubbele punt gelijmde inloggegevens om naar een header-veilige string, en niets meer.

E-mail: MIME en waarom ToBase64Transform niet afbreekt

E-mail is de historische thuis van Base64, en het is nog steeds de plek waar de 76-karakters-regel vandaan komt: de MIME-specificatie breekt geëncodeerde lichamen af op 76 tekens met CRLF tussen de regels, zodat geen enkele SMTP-hop een reden heeft ze opnieuw af te breken. C# geeft je twee encoders voor deze klus, en ze maken verschillende beloftes, wat de moeite waard is om te begrijpen voordat je er één kiest. De eerste is de klassieke Convert.ToBase64String met InsertLineBreaks, die je zag in de regelafbreking-sectie, en het is exact de MIME-vorm, afgebroken op 76 met CRLF, klaar om te plakken onder een Content-Transfer-Encoding: base64-header. De tweede is ToBase64Transform, de streamende neef, en hier is de verrassing: hij voegt geen regelafbrekingen in. Hij heeft geen mode daarvoor, geen optie, geen constructor-flag, en zijn output is één lange niet-afgebroken stream:

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

Dus de praktische regel is: voor kleine tot middelgrote e-mailpayloads, lees de bytes en gebruik de afbrekende klassieke encoder, want je krijgt de MIME-vorm direct. Voor grote bijlagen, stream met ToBase64Transform om het geheugen vlak te houden, en brek het resultaat zelf af als het transport écht regels van 76 tekens nodig heeft, waarbij je de output splitst op groepsgrenzen (elke 76 tekens, wat altijd een groepsgrens is, zoals de regelafbreking-sectie uitlegde). De transform doet het goede door niet af te breken: hij verwerkt de input in groepen van drie bytes, en regelafbrekingen zijn een formateringsbeslissing die hoort bij de laag die het transport kent, niet bij de laag die in een pipe bytes naar tekens zet.

Streamen: grote bestanden encoderen zonder ze twee keer te lezen

Wanneer de payload een video is, een backup, of iets dat je je zou schamen te houden in een string, is de streamende encoder de hele oplossing. Het patroon is de spiegel van de streaming aan de decoderingskant: een CryptoStream over het bronbestand, met ToBase64Transform in leesmodus, en een CopyTo naar het doel. Het bestand stroomt binnen, de Base64 stroomt eruit, en het enige geheugen dat het proces vasthoudt is de buffer die de stream intern gebruikt:

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

Twee feiten over dit patroon zijn de moeite waard om vast te houden. Eerst is de grootte van de output volledig bepaald door de grootte van de input, 4 tekens per 3 bytes, dus je kunt de doelimiet reserveren, de lengte vooraf berekenen voor een content-length-header, of een schijfkwota budgetteren voordat er ook maar één byte stroomt. Ten tweede verwacht de transform zijn input in groepen van drie bytes, en CryptoStream handelt die uitlijning voor je af, en voedt de transform precies met wat hij wil terwijl het bestand voorbijstroomt. Als je de transform ooit handmatig aanstuurt met TransformBlock, voed hem dan in veelvouden van drie, en laat TransformFinalBlock de staart afvoeren, de één of twee resterende bytes die de laatste partiële groep worden met hun één of twee padtekens. Voor de meeste applicaties is de CopyTo-vorm alles wat je ooit zult schrijven, en het is de vorm die zich goed gedraagt onder een geheugenlimiet, en dat is precies waar grote bestanden graag leven.

Configuratie, omgevingsvariabelen en databases

De andere gangbare encoderingsklus in C#-applicaties is de opslagklus: een geheim of een binaire blob pakken en in een plek stoppen die alleen tekst accepteert. Omgevingsvariabelen zijn het zichtbare voorbeeld, want een omgevingsvariabele is per definitie een string:

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 databases verschijnt hetzelfde idee meestal als een byte[]-eigenschap die een tekstkolom moet bevatten, en Entity Framework Core heeft hiervoor een ingebouwd mechanisme, een value converter die je encodeer- en decodeerfuncties uitvoert bij elke lees- en schrijfbewerking:

using Microsoft.EntityFrameworkCore;

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

Die converter is de hele database-integratie: de C#-code ziet een byte[], de kolom ziet een Base64-string, en de rondreis is op de aanroeplocatie onzichtbaar. Bij deze sectie horen twee waarschuwingen. Eerst betaalt de kolom de 33-procent-belasting: een tekstkolom die is afgepast op de geëncodeerde lengte houdt een derde minder data aan dan dezelfde breedte als binair, dus als je een kolom met vaste breedte hebt, pas hem dan af op de Base64-lengte, en als je een varchar(max) of equivalent hebt, is de belasting alleen een facturatiekwestie. Ten tweede, en dit is wat steeds terugkomt, Base64 in een configbestand is een vorm, geen schild. Het houdt de waarde op één regel, houdt hem uit de weg van teksteditors, en is met één commando leesbaar voor iedereen die het bestand kan lezen. Geheimen hebben echte bescherming nodig, een geheimenkluis, een sleutelkluis, op z'n minst bestandsrechten, en de Base64 is gewoon het transportformaat dat het geheim draagt terwijl het in de configuratie zit.

Vanaf de command line

Elke encoder verdient een consoleleven van 15 regels, en de C#-ene is aangenaam, want de output is een kale string waar standaardoutput voor is gemaakt. Hier is de hele tool: hij neemt een bestandspad of standaardinput, encodeert het, en schrijft de Base64 naar de terminal waar elke shell-pipeline hem kan oppakken:

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

Bouw hem één keer en hij zit ernaast aan de eigen base64-utility van de shell voor de dagen dat je specifiek het gedrag van de .NET-encoder wilt: hetzelfde alfabet, dezelfde padding, en de UTF-8-behandeling van de C#-runtime van wat de pipe hem ook toespeld. Voor binaire bestanden is hetzelfde skelet met File.ReadAllBytes in plaats van File.ReadAllText de hele verandering, en beschrijft de output dan de exacte bytes van het bestand in plaats van de tekstinterpretatie. De tool is ook een goede test: pipe een bestand erdoorheen, pipe de output terug door de decoder uit het decoderingsartikel, en vergelijk de twee bestanden met diff, wat een bevredigende eind-tot-eind-check is dat beide kanten van de pipe het over elke byte eens zijn.

Padding, of de hangende equals

De laatste =-tekens van een Base64-string zijn de boekhouding van het formaat, en C#'s encoders zijn het erover niet eens, wat de bron is van een specifieke en gangbare compatibiliteitsbug. De klassieke Convert.ToBase64String padt altijd, want de klassieke decoder waarmee hij is gekoppeld verwacht dat altijd. Base64Url.EncodeToString padt nooit, want de URL-safe consumenten die hij beoogt, JWTs en token-API's, verwachten altijd de compacte vorm. Wanneer je output het gebied insteekt van een wereld met de tegenovergestelde verwachting, is de fix rekenwerk, en het is hetzelfde rekenwerk dat het decoderingsartikel toonde voor de omgekeerde richting:

using System;

string padded = Convert.ToBase64String(new byte[] { 1, 2 });
Console.WriteLine(padded);           // AQI=
Console.WriteLine(padded.TrimEnd('=')); // AQI, wat een URL-safe consument wil

string compact = "AQI";
string restored = compact + new string('=', (4 - compact.Length % 4) % 4);
Console.WriteLine(restored);         // AQI=, wat een klassieke decoder wil

De formule (4 - length % 4) % 4 is het hele paddinguniversum: hij voegt nul, één of twee tekens toe zodat de lengte op een veelvoud van vier belandt, en de buitenste modulo houdt al gepadde input tegen extra tekens bij te dragen. Twee waarschuwingen over padding, want hier gaat goedbedoelde code fout. Behandel het =-teken nooit als data: het draagt geen informatie, dus een string encoderen die al padding bevat alsof het payload is, of het =-teken in een query string URL-encoderen naar %3D, zijn allebei manieren om output te produceren die er goed uitziet en slecht decodeert. En wees je bewust van het kleine gezin legacy-payloads waar de padding als een ander karakter was geschreven, in sommige oudere systemen een punt, in plaats van de standaard =: als een waarde die je ontvangt een punt gebruikt waar je padding verwacht, normaliseer hem dan terug naar = voordat je decodeert, of stuur hem de URL-safe-weg door zonder padding.

Hoe snel draait het

Base64-encoderen in moderne .NET is snel, en het interessante deel is het geheugenverhaal, niet het CPU-verhaal. De runtime-implementaties zijn geoptimaliseerd met SIMD-vectorinstructies waar de hardware ze ondersteunt, en inputs van meerdere megabytes encoderen in enkele cijfers tot lage dubbele cijfers milliseconden op een gangbare desktopmachine, snel genoeg dat de encoder effectief gratis is in elke applicatie die je zult schrijven. Het prestatieadvies dat écht code verandert gaat over vorm. De output is een C#-string, en een C#-string slaat twee bytes per teken op, dus de geheugenkosten van een geëncodeerd resultaat zijn ruwweg 2,7 bytes per inputbyte (4 tekens per 3 inputbytes, bij 2 bytes per teken), en dat is een getal dat de moeite waard is om te kennen wanneer de payload in de megabytes loopt. Wanneer je duizenden kleine payloads in een lus encodeert, gebruik bij voorkeur de span- en char-buffer-API's, die schrijven naar buffers die je hergebruikt, in plaats van de string-API's, die bij elke aanroep een verse beheerde string alloceren. Wanneer je één groot bestand encodeert, sla de string helemaal over en gebruik de streamende transform, want de kosten van 2 bytes per teken van een string van 13 megabyte vasthouden is pure verspilling wanneer een CopyTo de werkset in streambuffers had gehouden. En wanneer je MIME-omwikkelde output produceert, onthoud dan dat de omwikkeling een tweede rit over de data is, dus wikkel alleen wanneer het transport het nodig heeft, niet als standaard.

Het beveiligingsgesprek

De encoderingskant van Base64 heeft één beveiligingsles, en die is de omgekeerde van die van de decoder: jij bent degene die de keuze maakt om leesbare data bloot te stellen, en het formaat zal je niet tegenhouden. Base64 is encoderen, niet versleutelen. Het heeft geen sleutel, geen algoritme, en geen geheimhouding van enige soort, en de output van je ToBase64String-aanroep ligt op één commando van de input, op elke machine, in elke taal, door iedereen. De eerste regel gaat dus over wat je kiest om te encoderen: stop nooit een wachtwoord, een token of een geheim in een configbestand dat door Base64 "wordt beschermd", want de bescherming is precies één decode-aanroep diep, en de persoon die de configuratie leest heeft het commando. Als de waarde geheim moet zijn, heeft het echte bescherming nodig, en is de Base64 gewoon de vorm die het draagt terwijl het in het tekstveld zit.

De tweede les gaat over het kanaal, en die is specifiek voor de dingen die dit artikel bouwt. Een Basic auth-header draagt het wachtwoord in een vorm die elke proxy, elk log, en elke middlebox kan lezen, en daarom is het schema alleen aanvaardbaar over TLS en grotendeels verouderd buiten legacy-integraties om. Een data-URI in HTML draagt de afbeelding, en als de afbeelding een door de gebruiker aangeleverde SVG is, draagt hij wat de SVG draagt, en daarom heeft het SVG-in-data-URI-geval dezelfde zorg nodig als elke gebruikerscontent. En een Base64-waarde in een URL is, letterlijk, in de URL, wat betekent dat hij in de browsergeschiedenis zit, in het server-accesslog, in de referrer-header, en in de proxy-cache, dus tokens die privé moeten blijven horen niet in query strings, gepad of niet. De encoder doet in alle drie gevallen zijn eerlijke werk, bytes omzetten naar een veilige string om mee te dragen. De beveiliging zit in wat je meedraagt, en waar, en het formaat is een betere boodschapper dan de meeste, maar het is een boodschapper, geen kluis.

Valkuilen waarin C#-encoders vallen

Dit zijn de valkuilen die doorlopend weer opduiken aan de encoderingskant van C#-code, en elke één heeft een concrete oorzaak in de werking van de framework:

  • De charset die je niet koos. Een string encoderen met Encoding.Default produceert andere Base64 op .NET Framework (de Windows-ANSI-codepagina) dan op .NET (UTF-8). De outputs zijn allebei geldig, decoderen allebei "correct" op hun thuisplatform, en ze zijn niet dezelfde bytes. Fixeer de encoding expliciet.
  • Dubbele codering. De input was al Base64 (een config die een geëncodeerde waarde had geëncodeerd, een API die zijn input opnieuw encodeert), en de encoder, die precies deed waar hij om gevraagd was, produceerde Base64-van-Base64. Het resultaat lijkt plausibel, en het decodeert laag voor laag, en zo wordt een bug die twee decodes kost om te fixen ontdekt in productie.
  • Regelafbrekingen op de verkeerde plek. De MIME-afgebroken vorm, met zijn CRLF-paren, belandt in een JSON-string, een JWT-segment, of een URL-parameter, waar de strikte consument stikt in de witruimte waar hij nooit om is gevraagd. Wikkel voor mail, laat het overal anders onaangetast, en als je iemands afbreking stript, strip dan het \r ook, naast het \n.
  • Standaardalfabet in een URL. Een + in een query string wordt gedecodeerd als spatie door form-parseregels, dus een standaard Base64-waarde die in een URL wordt gestopt komt terug met letters waar de plustekens waren. Gebruik het URL-safe alfabet, of percent-encodeer de hele waarde, en nooit allebei.
  • De padding-mismatch. Je output is gepad, de consument wil compact, of omgekeerd, en geen van beide kanten heeft fout - ze zijn het alleen niet eens. De fix is het rekenwerk uit de paddingsectie, toegepast aan de kant die de verwachting van de consument kent, en dat is meestal de kant die het token schrijft.
  • Geheugen dat niet was begroot. De geëncodeerde string is twee bytes per teken in het geheugen, dus een bestand van 10 MB wordt een string van 13 miljoen tekens die ruwweg 27 MB weegt in beheerd geheugen, en een lus die zulke strings één voor één bouwt zal in de profiler opduiken als allocatie-omzet zonder zichtbare oorzaak. Meet buffers af met de lengte-helpers, stream de grote, hergebruik buffers in de hete lussen.
  • De transform die niet afbreekt. ToBase64Transform geeft één lange regel uit. Code die een "MIME-klare" bijlage erdoorheen streamt en hem daarna per mail stuurt, produceert een regel van 120.000 tekens die sommige transporten in het midden van een groep opnieuw zal afbreken, wat precies de corruptie is die de 76-karaktersregel was ontworpen om te voorkomen.
  • De codering encoderen. Een Base64-string aan de encoder geven omdat "de data al tekst is" produceert een tweede laag. De encoder weet niet, en maakt zich er ook niks van, dat zijn input er als Base64 uitziet; hij encodeert zoveel tekens als de string toevallig heeft, en de decoder aan de andere kant krijgt een Base64-string waar hij jouw data verwachtte.

Hoe de encoder groeide: een rondleiding door de versies

De encoderingskant van de API heeft zijn eigen tijdlijn, en die loopt van de tweede .NET-release tot de momenteel in preview zijnde:

  • .NET Framework 1.1, april 2003. Convert.ToBase64String en ToBase64CharArray arriveren, de hele klassieke familie in één release, met de stuk-overloads al inbegrepen, wat een klein wonder van vooruitziendheid is voor een API uit 2003.
  • .NET 2.0, 2005. Base64FormattingOptions en de InsertLineBreaks-waarde komen bij de familie, en brengen de MIME-regelafbreking in de framework en beëindigen een era van zelfgemaakte Substring-lussen in e-mailcode.
  • .NET Core 2.1, 2018. Het span-tijdperk. Convert krijgt de span-gebaseerde encode en TryToBase64Chars, en de nieuwe System.Buffers.Text.Base64-klasse arriveert met zijn OperationStatus-contract en de in-place-opzwelling, gebouwd voor de nul-allocatiewereld.
  • .NET 5, 2020. De hex-zusters (Convert.ToHexString en maten) verschijnen, hetzelfde ontwerppatroon toegepast op een alfabet van 16 symbolen, en het conversie-klasse-patroon wordt een huisstijl.
  • .NET 7, 2022. X509Certificate2.ExportCertificatePem laat de framework PEM voor je produceren, pantsermarkeringen, 64-tekens-afbreking en Base64-lichaam inbegrepen, wat een hele klasse handmatig certificaat-formaterende code in stilte overbodig maakt.
  • .NET 9, november 2024. System.Buffers.Text.Base64Url landt in de framework na jaren aan verzoeken van de community, met het Microsoft.Bcl.Memory-pakket dat het terug poort naar .NET Framework 4.6.2 en hoger, en het padding-weggedrag dat JWT-code al die tijd zelf had gemaakt.
  • .NET 11, bij het schrijven in preview. De volgende release, verwacht in eind 2026, voegt verdere Base64-gemak-API's en overloads toe aan de bestaande types, en zet de mars naar een ergonomischer oppervlak voort.

Het formaat zelf heeft een oudere biografie, en dat is de reden dat de C#-API eruitziet zoals het doet. Het eerste gestandaardiseerde gebruik van wat we nu MIME Base64 noemen was het Privacy-Enhanced Mail-protocol in 1987 (RFC 989), de MIME-specificatie fixeerde de op 76 tekens afgebroken vorm in 1993, en RFC 4648 in 2006 gaf het formaat zijn moderne, alfabetbewuste specificatie, inclusief de URL-safe variatie waarvoor C# pas in 2024 een eersteklas encoder kreeg. Drie decennia e-mail- en webconventies zijn de reden dat de regelafbrekingen, de padding, en de twee alfabetten allemaal bestaan, en de C#-encoder is de plek waar alle drie elkaar ontmoeten.

Kleine wonderen

  • Het vier-tekensminimum. De kleinste mogelijke niet-lege Base64-output is vier tekens, want het formaat denkt in groepen van vier, ook wanneer je het één byte geeft. Een byte van wat dan ook encodeert naar twee letters en twee =-tekens, en die vorm, twee datatekens die een opvulmuts dragen, is een vingerafdruk die je gaat herkennen in configs en tokens.
  • Nullen zijn welkom. De encoder heeft geen mening over wat de bytes betekenen, dus een buffer vol nullen encodeert met plezier naar een muur van A-tekens, en een binair bestand met zijn NUL-bytes intact maakt de rondreis zonder er ook maar één te verliezen. De "strings kunnen geen binair houden"-angst hoort bij de stringkant van het typesysteem, niet bij de encoder, die nooit een string ziet.
  • Determinisme als eigenschap. Dezelfde bytes, dezelfde opties, altijd dezelfde string. Geen timestamp, geen willekeurige salt, geen variatie, en daarom maakt een Base64-string een bruikbare snelle-en-rommelige vingerafdruk van de inhoud van een bestand: twee bestanden met dezelfde Base64 zijn hetzelfde bestand, en de check is een stringvergelijking.
  • Twee bytes per teken, gratis. Een C#-string is UTF-16, dus elk teken in je Base64-output bezet twee bytes in beheerd geheugen. De encoder kondigt dit niet aan, de lengte-eigenschap rapporteert het niet, en een string van 13 miljoen tekens weegt simpelweg 26 MB, en dat is het getal dat in je hoofd moet zitten wanneer de payload groot is.
  • CRLF van oorsprong. De MIME-afbreking voegt carriage-return-line-feed-paren in, ook wanneer je code op Linux draait, want de regel komt uit de e-mailspecificatie, niet uit het platform. De encoder is een historicus evenzeer als een converter, en hij behoudt de regeleinden van 1993 op een machine uit 2026.
  • Een stuk-overload vanaf dag één. ToBase64String(byte[], int, int) encodeert al sinds 2003 een venster in een grotere array, vijftien jaar voordat spans het idee populair maakten. De API-ontwerpers uit het 1.1-tijdperk keken naar echte buffers en voegden de offset-en-lengte-vorm toe, en het is nog steeds de juiste keuze wanneer de data een sectie is van een grotere read.
  • De 64-tekenscertificaatregel. PEM breekt af op 64 tekens, niet 76, en ExportCertificatePem weet dit en breekt dienovereenkomstig af, wat één van de stille details is die "laat de framework het doen" tot het goede advies maakt voor certificaatwerk. Twee afbrekbreedtes, één formatfamilie, en de framework houdt ze netjes uit elkaar.
  • Twee alfabetten, twee namen. De 64 waarden heten "standard" in het ene deel van de API en "URL-safe" in het andere, en ze verschillen in precies twee tekens: de 62ste en 63ste plek. Aan de ene kant + en /, aan de andere - en _, en elke compatibiliteitsbug in dit artikel zit in het moment dat iemand aannam dat de twee kanten hetzelfde waren.

De cirkel wordt rond

Dat was de encoderingskant, en dat is waar je de beslissingen neemt: het alfabet, de padding, de regelafbrekingen, de charset, de buffer. De andere richting, Base64 ontvangen van anderen, met hun paddingkeuzes, hun regelafbrekingen, hun alfabetten en hun tokens, is waar het grootste deel van de pijn zit, want je kunt niet onderhandelen met een payload. Base64 decoderen in C#, van de klassieke éénregelaar tot de span- en URL-safe-families, is diep behandeld in het bijbehorende artikel hieronder gelinkt, en samen vatten de twee het hele onderwerp in je werkgeheugen, en dat is het punt van een formaat dat zo oud en zo klein is.

Laatst bijgewerkt: 2026-10-06

Gerelateerd artikel: Base64-decodering in C# (CSharp): een complete gids