Base64 Decoding in C# (CSharp): A Complete Guide
You recognize it in an instant: a river of letters and digits, the occasional + or /, and maybe an = or two dangling off the end. Somewhere between an API response, an email attachment, a config file and a JWT, someone packed binary data into text, and now it is your job to open it. This is the decoding side of Base64 in C#, and the first piece of good news is that you need nothing but the framework. The decoder has lived in the System namespace for more than twenty years, and every modern .NET runtime still ships it, with more options and better performance than the original.
A quick refresher, because the main page of this site explains the format in full: four characters from a 64-symbol alphabet carry three bytes of data, and one or two = characters at the tail mark the leftover bytes. Decoding runs that trade in reverse, so the result is roughly three quarters the size of the input. With the shape of the problem in mind, let us open some packages.
The Decoder Family: Know Your Options
Before the first example, here is the whole family of decoding APIs you can reach for, and the situation each one is built for. Everything listed here is part of the .NET runtime itself, except for the URL-safe class on older frameworks, which rides in on a small NuGet package:
| API | Available since | What it is for |
|---|---|---|
Convert.FromBase64String(string) |
.NET Framework 1.1 (2003) | The classic. One string in, a fresh byte[] out. Skips ordinary whitespace, throws on anything else. |
Convert.FromBase64CharArray(char[], int, int) |
.NET Framework 1.1 (2003) | The same decode, reading from a slice of a character buffer you already own. |
Convert.TryFromBase64String, Convert.TryFromBase64Chars |
.NET Core 2.1 (2018) | Boolean instead of exceptions, writing into a span you provide. The friendly guard for untrusted input. |
System.Buffers.Text.Base64 |
.NET Core 2.1 (2018) | The strict span API: status codes instead of exceptions, in-place decoding, and IsValid pre-checks. |
System.Buffers.Text.Base64Url |
.NET 9 (2024) | The URL-safe alphabet (- and _ instead of + and /), with or without padding. On .NET Framework 4.6.2+ and .NET Standard 2.0: the Microsoft.Bcl.Memory NuGet package. |
FromBase64Transform + CryptoStream |
.NET Framework 1.1 (2003) | Streaming decoding: file to file, network to disk, chunk by chunk, without loading the whole payload. |
If your project targets a .NET version from 2018 onward, the first four rows are in the box. Base64Url needs .NET 9 or newer, or the Microsoft.Bcl.Memory package on anything older. And a forward note: the .NET 11 libraries, in preview at the time of writing with a general release expected in late 2026, add further Base64 convenience APIs and overloads to the existing types, so the family keeps growing. No other package in this article is required by anyone.
The Workhorse: Convert.FromBase64String
Ninety percent of decoding life in C# is a single call. Hand it a string, and it hands you back the exact bytes that were packed inside:
using System;
using System.Text;
string packed = "TWFu";
byte[] bytes = Convert.FromBase64String(packed);
string text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
// Man
Three details are worth fixing in your mind. First, the return value is bytes, not text: it is a byte[], and the decoder is byte-oriented end to end, and that is exactly what you want, because the payload could be a sentence, a PNG, a certificate or a hash, and none of them should be treated as special. The jump from bytes back to readable text is a separate, deliberate step through Encoding, and that step is where charset decisions live (more on that below). Second, the decoder allocates a fresh array every call, sized to the decoded length, so it never hands you a buffer with spare capacity. Third, the contract is small and honest: an empty string decodes to an empty array, a null reference throws ArgumentNullException, and anything that is not valid Base64 throws FormatException. Everything else is an elaboration of those three rules.
What It Forgives and What It Refuses
Here is where C#'s decoder has a personality, and a characterful one. It is generous about one thing and merciless about everything else: whitespace. The decoder skips exactly four characters wherever they appear in the string: the space (U+0020), the tab (U+0009), the line feed (U+000A) and the carriage return (U+000D). That policy is a deliberate nod to email, where Base64 payloads arrive wrapped in 76-character lines, and it means a MIME-wrapped attachment decodes with zero preprocessing. Anything outside the 64-symbol alphabet, anything that breaks the length rules, or anything with padding in the wrong place earns an exception. Watch the same decoder in action on a few different inputs:
| Input | Result |
|---|---|
"TWFu" |
Decodes to Man (3 bytes). |
"TWF\nFu" (a newline in the middle) |
Decodes to Man. Whitespace is invisible to the decoder. |
"TWFu\u00A0" (a non-breaking space at the end) |
FormatException. Only the four whitespace characters above are skipped; NBSP is not one of them. |
"TWE" (length 3, not a multiple of 4) |
FormatException. The payload length, ignoring whitespace, must be a multiple of 4. |
"TWFu=" (extra padding after the data) |
FormatException. At most two padding characters, and only at the very end. |
"-_88" (URL-safe alphabet) |
FormatException. The standard decoder only knows the 64 characters of the standard alphabet. |
null |
ArgumentNullException: Value cannot be null. (Parameter 's') |
One more quirk worth memorizing: every format crime gets the same single error message, The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters. The message lists all three possible causes and does not say which one you hit, and it does not tell you where. If you are debugging a failing payload, count the characters, check the alphabet, and check the padding in that order.
Decoding Without Exceptions: The Try APIs
Exception-driven control flow is a legitimate pattern, but for high-volume or untrusted input the Try family is the better citizen. It was added in .NET Core 2.1 and comes in two flavors: one that reads from a string and one that reads from a character span. Both write into a buffer you provide and report how much of it they filled:
using System;
using System.Text;
string payload = "TWFu"; // any payload, valid or not
Span<byte> buffer = stackalloc byte[4096];
if (Convert.TryFromBase64String(payload, buffer, out int written))
{
string text = Encoding.UTF8.GetString(buffer[..written]);
Console.WriteLine(text);
}
else
{
Console.WriteLine("Not a valid Base64 payload.");
}
Two behaviors make the Try variants feel like a different species. Invalid input returns false instead of throwing, so a stream of malformed payloads costs you a branch rather than an exception. And a null input also returns false quietly, so the same guard covers both "missing" and "broken" without any extra checking. The sibling method Convert.TryFromBase64Chars does the same job from a ReadOnlySpan<char>, which is handy when the payload lives in a larger character buffer and you do not want to slice off a substring first. Size the output buffer generously: the decoded length is at most three quarters of the (non-whitespace) input length, and the written out-parameter tells you exactly how much came out.
Span-Based Decoding with System.Buffers.Text.Base64
When you are counting allocations, or when you want the decoder to describe its failures instead of throwing them, the System.Buffers.Text.Base64 class is the tool. It is a static class in the standard library since .NET Core 2.1, and it works on spans rather than managed arrays. Its decode method returns an OperationStatus value with four moods: Done (success), DestinationTooSmall (your buffer was too small), NeedMoreData (the input is not a multiple of 4 yet, keep reading), and InvalidData (this is not Base64). The last boolean parameter, isFinalBlock, is what distinguishes those two: it tells the decoder whether more input is coming. Here is the one-shot form, sized with the class's own helper:
using System.Buffers;
using System.Buffers.Text;
using System.Text;
string payload = "TWFu";
byte[] input = Encoding.ASCII.GetBytes(payload);
byte[] output = new byte[Base64.GetMaxDecodedFromUtf8Length(input.Length)];
OperationStatus status = Base64.DecodeFromUtf8(input, output,
out int consumed, out int written, isFinalBlock: true);
if (status == OperationStatus.Done)
{
Console.WriteLine(Encoding.UTF8.GetString(output.AsSpan(0, written)));
// Man
}
Two more members of this class deserve a paragraph. The first is IsValid, which validates a payload without decoding it. It comes in byte-span and character-span flavors, and one overload reports the decoded length alongside the verdict, so you can size a buffer from a single check:
using System.Buffers.Text;
string payload = "TWFu";
if (Base64.IsValid(payload, out int decodedLength))
{
Console.WriteLine("Valid, decodes to " + decodedLength + " bytes.");
// Valid, decodes to 3 bytes.
}
else
{
Console.WriteLine("Rejecting payload before allocating anything.");
}
The second is DecodeFromUtf8InPlace, for the situation where the Base64 text already sits in a buffer you own and you do not mind overwriting it. Decoding shrinks the data, so the result is written to the front of the same buffer and the method reports how long it is:
using System.Buffers;
using System.Buffers.Text;
using System.Text;
byte[] data = Encoding.ASCII.GetBytes("TWFu");
OperationStatus status = Base64.DecodeFromUtf8InPlace(data, out int written);
if (status == OperationStatus.Done)
{
Console.WriteLine(Encoding.ASCII.GetString(data, 0, written));
// Man, now living in the first three bytes of the same buffer
}
One behavior difference from Convert.FromBase64String to keep in your pocket: this class also skips the four ordinary whitespace characters (space, tab, line feed, carriage return), so a line-wrapped payload decodes just as well. It is stricter in the ways that matter: a payload whose non-whitespace length is not a multiple of four is InvalidData when it is the final block, and characters outside the standard alphabet are rejected outright. There is no silent cleanup anywhere in this class.
URL-Safe Base64: The Base64Url Class
A second alphabet exists for the same 64 values, and you will meet it constantly in C# web work. In the standard alphabet, values 62 and 63 are + and /, two characters that cause trouble in URLs: a + in a query string is routinely decoded as a space, and / and = each need percent-encoding. RFC 4648, section 5, fixes this by swapping in - and _, which are plain letters in every URL context, and it makes the trailing = padding optional. The result is called base64url, and it is the alphabet of JWTs, API tokens, file upload IDs and a great many URLs (YouTube's 11-character video identifiers are base64url without padding).
Since .NET 9 the standard library ships a dedicated class for it: System.Buffers.Text.Base64Url. It is the URL-safe twin of the Base64 class, with its own decode, validate and length helpers:
using System.Buffers.Text;
using System.Text;
string token = "-__8";
byte[] bytes = Base64Url.DecodeFromChars(token);
Console.WriteLine(BitConverter.ToString(bytes));
// FB-FF-FC
Notice what that example would not have done with the classic API. The same three bytes encode as +//8 in the standard alphabet, and Convert.FromBase64String("+//8") works, but Convert.FromBase64String("-__8") throws, because the URL-safe characters are outside its alphabet. And base64url payloads commonly arrive without padding, which the classic decoder also rejects, because it insists on the full group of four. The Base64Url class handles both variants of the problem natively: it decodes TWE (three characters, no padding) to the two bytes Ma, and it decodes TWE= just as well.
If your project runs on an older runtime, there are two practical paths. On .NET Framework 4.6.2 and up, add the Microsoft.Bcl.Memory NuGet package, which Microsoft publishes specifically to backport Base64Url (along with a few other modern types):
dotnet add package Microsoft.Bcl.Memory
Or, without any package at all, normalize the payload before handing it to the classic decoder: swap the URL-safe characters back to their standard twins, and top up the missing padding. This little helper is the most common hand-rolled base64url decoder in C# code, and it is worth knowing because it works on every runtime since .NET Framework 1.1:
using System;
using System.Text;
string segment = "TWE";
segment = segment.Replace('-', '+').Replace('_', '/');
segment += new string('=', (4 - segment.Length % 4) % 4);
byte[] bytes = Convert.FromBase64String(segment);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// Ma
The (4 - length % 4) % 4 formula is the entire padding arithmetic: it adds zero, one or two = characters so the length lands on a multiple of four, and the outer modulo keeps already-padded input from gaining extra ones.
From Bytes to Words: Text, Unicode and Charsets
Decoding gets you bytes, and bytes are a perfectly neutral thing. They only become "text" when you choose a charset to read them as, and that choice is yours to make, because Base64 carries no information about which charset the original author used. In practice that means: assume UTF-8 unless you have a reason not to, and be explicit about it in code, because an explicit Encoding.UTF8 call is the difference between a program that is correct by accident and one that is correct by design:
using System;
using System.Text;
string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
byte[] decoded = Convert.FromBase64String(packed);
string restored = Encoding.UTF8.GetString(decoded);
Console.WriteLine(restored == original);
// True: h\u00e9llo \u4e16\u754c round-trips perfectly
The subtle trap is what happens when the bytes are not valid UTF-8, because the payload was really Latin-1, or binary, or just corrupted. By default, .NET's UTF-8 decoder replaces every malformed sequence with the Unicode replacement character (U+FFFD) and moves on. No exception, no warning: the data is simply gone, turned into question marks in your database. If you need to know when that happens, construct the encoding with a strict fallback, which turns silent replacement into a loud DecoderFallbackException:
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // the bytes FF FE, not valid UTF-8
Encoding strictUtf8 = Encoding.GetEncoding(
"utf-8",
new EncoderExceptionFallback(),
new DecoderExceptionFallback());
string text = strictUtf8.GetString(bytes);
// Throws DecoderFallbackException, because FF FE is not a UTF-8 sequence
For payloads where you would rather survive than fail, the replacement fallbacks are the gentler option, and you get to pick the replacement text yourself:
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // the bytes FF FE, not valid UTF-8
Encoding forgivingUtf8 = Encoding.GetEncoding(
"utf-8",
EncoderFallback.ReplacementFallback,
new DecoderReplacementFallback("[bad]"));
string text = forgivingUtf8.GetString(bytes);
Console.WriteLine(text);
// [bad][bad] instead of the silent U+FFFD replacement
One more C#-specific history lesson: Encoding.Default means different things on different runtimes. On .NET Framework on Windows it is the system's ANSI code page (often Windows-1252), while on .NET (Core) it is UTF-8 without BOM. Code that round-trips a payload through Encoding.Default can therefore produce different bytes on a 2010 machine and a 2025 machine, and Base64 will cheerfully encode whichever set you hand it. If you ever see a decoded string full of accented mojibake, Encoding.Default is the first place to look.
Files and Binary Payloads
Files are the most straightforward decoding target, because there is no charset question at all: the bytes you decode are the file, byte for byte, zeros and all. The pattern is two calls and a file, and it shows up in everything from image uploads to backup tools:
using System.IO;
string b64 = File.ReadAllText("payload.b64");
byte[] original = Convert.FromBase64String(b64);
File.WriteAllBytes("restored.bin", original);
Console.WriteLine("Restored " + original.Length + " bytes.");
Two practical notes. If the file may contain whitespace or line breaks (which, being a text file, it almost certainly does), the classic decoder handles it for free, as you saw earlier. And if the payload is large, do not go through a string at all: skip the file-to-string step and decode directly from the stream, which is the next section. For a binary payload that is text you happen to know the charset of, the file example is the whole solution, and the Encoding.UTF8.GetString step from the charset section slots right in between the decode and the use.
Decoding From a Stream: FromBase64Transform
The Convert methods are designed for payloads that fit in a string, and the official documentation says so in so many words: for streaming data, use the transform classes. FromBase64Transform has been part of System.Security.Cryptography since the first .NET release, and it plugs into CryptoStream, the framework's general-purpose pipe for transforming data as it flows. The whole file-to-file decode is a four-line setup:
using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("payload.b64");
using FromBase64Transform transform =
new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
using CryptoStream reader = new CryptoStream(source, transform, CryptoStreamMode.Read);
using FileStream target = File.Create("payload.bin");
reader.CopyTo(target);
Console.WriteLine("Done, " + target.Length + " bytes written.");
The constructor takes a mode, and the two modes are worth knowing by name. IgnoreWhiteSpaces (the default, matching the classic decoder's whitespace policy) skips the four ordinary whitespace characters as the stream flows, which is what you want for email-wrapped or newline-studded payloads. DoNotIgnoreWhiteSpaces is strict: the first non-alphabet character it meets throws a FormatException, which is what you want when a stray space in the payload should be a bug, not a shrug. Under the hood the transform processes input in groups of four characters and hands back the three bytes each group produces, with TransformFinalBlock handling the tail. You rarely call those methods yourself, because CryptoStream does it for you, but the group-of-four fact matters: if you ever feed the transform manually, feed it in multiples of four, or the last partial group will sit in the final block.
JWTs: Three Segments, One Dot
A JSON Web Token is the highest-traffic base64url payload in C# web development, and its shape is deceptively simple: three segments separated by dots. The first is the encoded header, the second the encoded payload (a.k.a. claims), and the third the signature. Each of the first two is base64url of a UTF-8 JSON document, without padding, per the JWS specification. Splitting and decoding is two lines of C#:
using System;
using System.Buffers.Text;
using System.Text;
using System.Text.Json;
string jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl";
string[] parts = jwt.Split('.');
string headerJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[0]));
string payloadJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1]));
using JsonDocument doc = JsonDocument.Parse(payloadJson);
Console.WriteLine(doc.RootElement.GetProperty("name").GetString());
// Ada
On runtimes before .NET 9, the same job goes through the normalization helper from the URL-safe section: swap - and _ back to + and /, pad the segment to a multiple of four, and decode with Convert.FromBase64String. Both approaches give you the same JSON; pick whichever matches your target framework.
One boundary to keep sharp: decoding a JWT is not verifying a JWT. The decode above will happily read the claims of a token with a garbage signature, because the signature is a separate cryptographic check over the first two segments. For production token work, do not parse by hand at all: the System.IdentityModel.Tokens.Jwt package (from the Microsoft.IdentityModel family) handles parsing, validation and expiration in one, and its base64url handling is exactly the alphabet this section describes. Decode by hand for debugging and small utilities; verify with the library for anything a user can reach.
Data URIs and Embedded Images
There is a whole class of C# code whose job is to receive a data: URI, because HTML, CSS and a great many web APIs use them to embed binary content inline. The scheme, standardized by RFC 2397, is data:[mediatype][;base64],payload: everything before the first comma is metadata (the MIME type and the ;base64 flag), everything after is the payload. When the ;base64 flag is present, the payload is a Base64 string, and splitting at the comma is the entire parse:
using System;
using System.Text;
string dataUri = "data:image/png;base64,iVBORw0KGgo=";
int comma = dataUri.IndexOf(',');
string mediaType = dataUri[..comma]; // data:image/png;base64
string b64 = dataUri[(comma + 1)..]; // iVBORw0KGgo=
byte[] imageBytes = Convert.FromBase64String(b64);
Console.WriteLine(imageBytes.Length);
// 8: the PNG signature bytes 89 50 4E 47 0D 0A 1A 0A
The iVBORw0KGgo= prefix in the example is the Base64 form of the eight-byte PNG magic number, and it is a useful fingerprint: any data URI for a real PNG starts that way, so it is a quick sanity check when you are parsing untrusted HTML. Two practical notes for C# developers. First, the Uri class understands data URIs natively on .NET: new Uri("data:text/plain;base64,TWFu") parses fine and reports Scheme == "data", so if your code routes on URIs, data URIs will show up in the pipeline and you should decide how to handle them. Second, remember what a data URI really is: a full copy of the file, inflated by a third, sitting inside your document. That is fine for a 4 KB favicon and painful for a 4 MB logo, so when you are the one generating them (the encoding article covers that side), size the image before you encode it.
HTTP: Basic Auth and API Exchanges
Base64 is woven into HTTP in at least one place you will touch in any API work: the Basic authentication scheme. The client sends Authorization: Basic followed by the Base64 encoding of username:password, joined by a colon. On the server side, decoding an incoming header is therefore: strip the Basic prefix, decode, and split on the first colon:
using System;
using System.Text;
string header = "Basic YWRhOnMzY3JldA==";
string encoded = header["Basic ".Length..].Trim();
string credentials = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
int colon = credentials.IndexOf(':');
string user = credentials[..colon];
string password = credentials[(colon + 1)..];
Console.WriteLine(user); // ada
Console.WriteLine(password); // s3cret
The UTF-8 step matters more than it looks: the specification says the credentials are encoded as UTF-8 before Base64, so a username with an accented character produces a different (and correct) byte string than the same username read as Latin-1. The decode side of Basic auth is the simple end of this pattern; in ASP.NET Core you will usually meet it through the authentication handlers rather than raw headers, but the same decode logic is what they run underneath, and it is exactly the kind of code you need when you write integration tests that fake an API server. The mirror operation, building the header on the client side, is a one-liner on the encoding side, and it gets a full example in the encoding article.
Email: MIME and Line-Wrapped Payloads
Email is where Base64 earned its reputation, and it is still the source of a lot of the payloads C# services receive. SMTP was originally a 7-bit protocol, so binary attachments cannot travel raw: the MIME specification (RFC 2045) encodes them as Base64 with a Content-Transfer-Encoding: base64 header, wraps the output at 76 characters, and separates the lines with carriage-return-line-feed pairs. A real attachment body therefore looks like a column of 76-character lines, and the good news for C# is that the classic decoder already knows how to read that: because it skips whitespace anywhere in the string, you can hand it the entire wrapped body, newlines and all, and it decodes it as if the line breaks were never there:
using System;
using System.Text;
string attachmentBody = "TWFu\r\nTWFu\r\nTWFu";
byte[] bytes = Convert.FromBase64String(attachmentBody);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// ManManMan
For payloads that arrive through a stream rather than a string, the FromBase64Transform with its whitespace-ignoring mode is the same story in streaming clothing. And when you need to do more than decode the body, when you need to walk the MIME structure, parse headers, handle nested multipart sections, or extract every attachment from a real .eml file, the ecosystem answer in C# is the MimeKit package: it is the standard MIME library for .NET, it handles the Base64 and quoted-printable content transfer encodings internally, and it is the tool to reach for the moment "just decode the body" stops describing your problem. The framework's own MailMessage class will decode simple attachments for you, but its MIME support is deliberately modest by modern standards.
PEM Certificates
PEM is the armored format of the TLS world: a Base64 body between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- markers, wrapped at 64 characters, as specified by RFC 7468. C# developers meet it as the certificate files behind every HTTPS endpoint, and the decode story here is better than you might expect, because since .NET 6 the framework parses PEM for you, Base64 body and all:
using System.IO;
using System.Security.Cryptography.X509Certificates;
string pem = File.ReadAllText("server.pem");
X509Certificate2 certificate = X509Certificate2.CreateFromPem(pem);
Console.WriteLine(certificate.Subject);
// CN=server.example.com
No manual Base64 anywhere in that: CreateFromPem finds the markers, unwraps the body, decodes it, and hands you back a live certificate. (The family has siblings for private keys and for the combined certificate-plus-key form, if your infrastructure hands you those.) If you are on an older runtime, or you need the raw DER bytes that sit inside the armor, the manual version is a two-step strip-and-decode, and it is worth knowing because the same pattern works for any PEM-armored thing:
using System;
using System.Text;
string pem = File.ReadAllText("server.pem");
string body = pem
.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("\r", "")
.Replace("\n", "");
byte[] der = Convert.FromBase64String(body);
Console.WriteLine(der.Length);
// The length of the DER certificate inside the armor
The pitfalls in this corner are all whitespace: PEM files carry CRLF line endings from most certificate tools, so strip both \r and \n before decoding, not just the line feeds. And do not confuse the certificate body with a private key body, which has different markers and different contents; a decoder will not save you from that one.
Configuration, Environment Variables and Databases
The third home of Base64 in C# applications is storage: config files, environment variables, and database columns. The pattern is the same everywhere. A binary or a secret value is encoded into a string on the way in, and decoded back to bytes on the way out. Environment variables are the most visible example, because they can only hold text:
using System;
using System.Text;
string encoded = Environment.GetEnvironmentVariable("API_KEY_B64");
byte[] keyBytes = Convert.FromBase64String(encoded);
string apiKey = Encoding.UTF8.GetString(keyBytes);
Console.WriteLine(apiKey.Length + " characters of API key, ready to use.");
In a database the same idea usually shows up as a byte[] property that you want stored in a text column for portability, and Entity Framework Core has a built-in mechanism for exactly this: a value converter that runs your encode and decode functions transparently on every read and write:
using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
.Property(a => a.ImageData)
.HasConversion(
v => Convert.ToBase64String(v),
v => Convert.FromBase64String(v));
That one converter is the whole database integration: ImageData stays a byte[] in your C# code, and the database sees a Base64 string. Two cautions belong with this section. First, a column sized for text holds a third less data than the same width as binary, because of the 4-chars-per-3-bytes tax, so size the column for the encoded length if it is a fixed width. Second, and this is the security one: Base64 in a config file is a convenience for keeping a value in a single line, not a protection for the value. Anyone who can read the config file can decode the key in one command, which is why real secrets belong in a secret store, and the Base64 there is just the transport format.
When the Payload Is Big
Base64 decoding has a pleasant property that encoding does not: the output is always smaller than the input, roughly three quarters of it. A 10 megabyte text payload decodes to about 7.5 megabytes of bytes, so a decode can never balloon your memory the way an encode can. The arithmetic, if you need to size a buffer up front, is one of two helper calls: Base64.GetMaxDecodedFromUtf8Length for the strict span class, or the plain division, length / 4 * 3 for the classic API, plus a fudge for whitespace if the input is wrapped. (The helper returns the maximum possible decoded length; for a fully padded payload the real length is exactly that, and for a partially padded tail it is one or two bytes less.)
When the payload is genuinely large, though, the right move is not a bigger buffer, it is no buffer at all: skip the string entirely and let FromBase64Transform stream the decode from source to target, as shown in the streams section. The only rule to respect is the group-of-four alignment: a Base64 stream can be sliced only at multiples of four characters (after whitespace is accounted for), so if you ever feed the transform by hand, read in chunks that are multiples of four and let TransformFinalBlock drain the remainder. For anything short of hundreds of megabytes, the one-shot decode is fast enough that this is an optimization, not a necessity, but the streaming form is also the one that behaves well under a memory limit, which is exactly the environments big payloads like to live in.
A Decoder in Your Terminal
There is a satisfying moment, in every language, where a 15-line console program becomes a command-line tool, and C#'s Base64 decoder is a good one to do it with, because reading from standard input makes it a drop-in for shell pipes. Here is the whole tool: it reads the Base64 payload from the pipe (or from an argument), decodes it, and writes the raw bytes to a file:
using System;
using System.IO;
using System.Text;
string input = args.Length > 0 ? File.ReadAllText(args[0]) : Console.In.ReadToEnd();
byte[] bytes = Convert.FromBase64String(input.Trim());
File.WriteAllBytes("output.bin", bytes);
Console.Error.WriteLine("Wrote " + bytes.Length + " bytes to output.bin.");
Build it once, and it sits next to the shell's own base64 utility for the days when you specifically want the .NET runtime's decoder: pipe a file through it, chain it with other tools, and the strict C# validation rules (whitespace-tolerant, alphabet-strict, padding-strict) become part of your pipeline. The Trim() is doing quiet work there, catching the trailing newline that text editors love to add, though to be fair the decoder would have ignored it anyway. For the URL-safe payloads that increasingly show up in API logs, the same skeleton with the Base64Url decode from the URL-safe section is the whole change.
Speed: What to Expect
Base64 in modern .NET is fast, and it has been getting faster. The runtime implementations of both the Convert methods and the System.Buffers.Text classes are optimized with SIMD vector instructions where the hardware supports them, and they process many characters per cycle. In practice that means multi-megabyte payloads decode in single-digit to low-double-digit milliseconds on an ordinary desktop machine, which is fast enough that Base64 decoding is effectively free in any application you will write. The practical performance advice is therefore about the shape of your code, not the decoder itself. Prefer the Try methods or the status-returning span methods on hot paths, where malformed input is possible and exceptions would be expensive. Reuse buffers with the in-place and span APIs when you are decoding thousands of small payloads in a loop, instead of allocating a fresh array per call. And never decode the same payload twice: once is the cost, and a second decode of a field you already decoded is pure waste that shows up in profiles as a mysterious second Base64 spike.
Security: What Base64 Does Not Do
The most important security fact about Base64 is the one beginners most often miss: it is encoding, not encryption. A Base64 string is readable by anyone, with any tool, in a fraction of a second, and C# makes reading it a one-liner, as this entire article has demonstrated. Base64 has no key, no algorithm parameter, and no weakness to exploit, because it was never trying to hide anything: it is a transport format, a way to make binary survive in text-only channels. Treat it accordingly. Never put a password, a token or a secret into a config file "protected" by Base64, because the protection is exactly one Convert.FromBase64String call deep. If the value must be secret, it needs real protection (a secret manager, an encrypted store, at minimum an operating-system access control), and the Base64 is just the shape it wears while traveling.
The second security note is about your own decode path. Every payload you decode is untrusted input until proven otherwise, and the two failure modes to design for are the loud one (invalid input, which the classic API answers with a FormatException you should catch and convert into a 400, not a 500) and the quiet one (valid Base64 that decodes to bytes that are not what you expected: not UTF-8, not the file type you asked for, or longer than you budgeted). Validate before you trust: check the length with IsValid or the Try family before you allocate, check the decoded bytes against an expected signature (the PNG magic, the PKCS header) before you hand them to an image or certificate parser, and size your buffers from the encoded length before you decode, not after. Base64 will decode anything that is well-formed; deciding what well-formed means for your application is your job.
Pitfalls Worth Knowing Before They Bite
These are the C#-specific traps that keep showing up in real code, and every one of them has a concrete cause in how the framework works:
- Binary through a string. A C#
stringis a sequence of UTF-16 code units, and decoded Base64 is not. The moment you stuff decoded bytes into a string variable (aConsole.WriteLineof a decoded PNG, a string concat with binary, a JSON library that serializes "text"), something downstream will mangle it. Keep decoded binary inbyte[]until it reaches a place that actually wants bytes. - The Encoding.Default split. Code that reads decoded bytes with
Encoding.Defaultproduces different text on .NET Framework (the Windows ANSI code page) and on .NET (UTF-8). The same payload, two different outputs, no exception. Pin your encoding explicitly. - JWT segments and the classic decoder. Feeding a raw JWT segment to
Convert.FromBase64Stringfails in two ways at once: the-/_characters are outside the standard alphabet, and the missing padding breaks the length rule. Normalize first, or useBase64Url. - Whitespace you can see and whitespace you cannot. The decoder skips space, tab, line feed and carriage return, and skips nothing else. A non-breaking space, a Unicode line separator, or a vertical tab in a payload (all of which survive copy-paste from some web pages) is a
FormatException, not a shrug. - One error message for every crime. The
FormatExceptionfrom the classic decoder does not say which rule broke or where. Debug by checking length, then alphabet, then padding, in that order, or switch toTryFromBase64StringandIsValidfor a boolean answer. - Silent UTF-8 replacement.
Encoding.UTF8.GetStringturns malformed byte sequences into U+FFFD without complaint. If the payload might not be valid UTF-8, use the strict fallback from the charset section or you will be investigating missing data weeks after it happened. - Stream slicing at the wrong place. A Base64 stream can only be cut at multiples of four characters. Chunk a streaming decode at any other boundary and the last partial group lands in
TransformFinalBlock, where it either belongs or it breaks your alignment accounting. - PEM line endings. Certificate files carry CRLF. Strip
\ras well as\nwhen you unwrap the armor manually, or the first line of your "decoded" DER is a carriage return wearing a data byte's clothes. - Double-encoding. If a payload was already Base64 when it reached you (a config that Base64'd a Base64 string, an API that encoded the output of another encoder), one decode gives you more Base64, not your data. The round trip only closes after as many decodes as there were encodes, and the encoder side of that bug is the subject of the encoding article.
A Short History of Base64 in C#
The Base64 story in C# is also a story of the .NET platform growing up, and it runs longer than most people expect:
- .NET Framework 1.1, February 2003.
Convert.FromBase64Stringand its siblings arrive, and they carry the design that still defines the API: strict about the alphabet, generous about the four whitespace characters, blunt about its errors. For most of the next two decades this one method is "the" Base64 decoder in C#. - .NET 2.0, 2005. The
Base64FormattingOptionsenum joinsConvert, bringing the MIME-style line breaks to the encoding side (and the matching whitespace tolerance to the decode side, where it is already quietly at work). - .NET Core 2.1, 2018. The Span era.
Convertgains theTrymethods and a span-based encode, and the newSystem.Buffers.Text.Base64class arrives with itsOperationStatuscontract, in-place decoding andIsValid, built for the zero-allocation world of the memory-focused rewrite. - .NET 5, 2020. The hex siblings (
Convert.ToHexStringand friends) ship, the same design pattern as Base64 applied to a 16-symbol alphabet, a sign that the conversion-class pattern had become a house style. - .NET 6, 2021.
X509Certificate2.CreateFromPemmakes PEM a first-class input, and a whole class of manual armor-stripping code becomes optional on modern runtimes. - .NET 9, November 2024.
System.Buffers.Text.Base64Urlfinally lands in the box after years of community requests, and theMicrosoft.Bcl.Memorypackage backports it to .NET Framework 4.6.2 and up for the legacy codebases that still run everything. - .NET 11, in preview at the time of writing. The next release, expected in late 2026, adds further Base64 convenience APIs and overloads to the existing types, continuing the slow march toward a more ergonomic surface.
Worth keeping in mind: the encoding itself is far older than any of this. The first standardized use of what we now call MIME Base64 was the Privacy-Enhanced Mail protocol in 1987 (RFC 989), MIME standardized the 76-character line-wrapped form in 1995, and RFC 4648 in 2006 gave the format its modern, alphabet-aware specification, including the URL-safe variant. C# inherited all of it: every line-wrapping and padding quirk you meet in a 30-year-old email format is a quirk the C# decoder was designed to absorb.
Curious C# Facts
- The smallest smoke test.
"TWFu"decodes toMan. Three bytes, no padding, no excuses. It is the hello-world of Base64 debugging in C#, and it exercises the whole happy path in four characters. - A decoder with a postal history. The whitespace tolerance is not an accident of implementation, it is a design decision inherited from MIME: an entire 76-character line-wrapped email body, with all its CRLF pairs, is a valid single argument to
Convert.FromBase64String. The decoder was built to eat the format email has used for thirty years. - One error, three causes. The classic
FormatExceptionmessage lists all three failure modes it might be reporting (bad character, too much padding, misplaced padding) and does not say which one fired. It is the only error message in the API surface that works like a multiple-choice question. - A namespace that lies a little.
System.Buffers.Textsounds like it is about text processing, but it is really the home of binary-to-text conversion in general: theUtf8ParserandUtf8Formatterthat parse numbers and dates straight into UTF-8 live right next door to the Base64 classes. - Padding is optional on one side of the family. The
Base64Urlclass decodesAQIDBA(six characters, no padding) andAQIDBA==(the same bytes with padding) to the same four bytes, while the classic decoder accepts only the padded form. Two decoders, two contracts, one runtime. - Strings that should not exist. A C# string can legally contain NUL bytes, so
Encoding.UTF8.GetStringof decoded binary can produce a "string" full of control characters that the console, your CSV writer and half the JSON libraries on the planet will each handle differently. The type system allows it; the ecosystem mostly does not. - A 1.1 relic in good standing.
Convert.FromBase64CharArrayhas had the same three-parameter signature since February 2003, surviving the generics revolution, the Span revolution and the URL-safe revolution without a single overload added. The char-array era of C# is not gone; it is just resting. - Eleven characters, eight bytes. YouTube's video identifiers are base64url without padding: 11 characters that decode to 8 bytes.
Base64Url.GetMaxDecodedLength(11)tells you the 8, and the decode is a one-liner, which is a nice way to end the day if you are the kind of person who writes that kind of thing.
The Other Direction
That is the decoder side, and it is where most of the pain lives, because decoding is where you meet other people's data: their padding choices, their line breaks, their alphabets, their tokens. The opposite direction, taking your own bytes and packing them into Base64, is a calmer problem with its own set of decisions to make, and its own set of traps. Base64 encoding in C#, from the 76-character question to URL-safe tokens, is covered in depth in the companion article linked below, and it is a short, satisfying read once you know what to look for.
Last updated: 2026-08-30
Related article: Base64 Encoding in C# (CSharp): A Complete Guide