C# example

How to convert a byte array to a hex string in C#

8 min read Updated Oct 2026 Runs in an isolated runtime
Quick answer

Use Convert.ToHexString(bytes) (.NET 5+): it turns [0x00, 0x0F, 0xA5, 0xFF, 0x10] into "000FA5FF10", two uppercase digits per byte. Convert.ToHexStringLower(bytes) (.NET 9+) gives "000fa5ff10", and Convert.FromHexString(hex) turns either one back into the bytes. On .NET Framework, use BitConverter.ToString(bytes).Replace("-", "").

Hex is how bytes get shown to people: hashes, keys, checksums, packet dumps and binary columns in logs. For most of .NET's history there was no direct method for it, which is why the old Stack Overflow answers are full of BitConverter tricks, StringBuilder loops and hand-tuned lookup tables. Since .NET 5, Convert does both directions in one call. This page covers that first, then the older forms you still meet, custom layouts such as 00 0F A5, the errors parsing throws, and the related job of turning a single int into hex and back. Each example runs on this page: hit Run, then edit the code and run it again.

1Convert.ToHexStringRecommended

Convert.ToHexString writes two characters per byte, uppercase, with no separators and no 0x prefix, in one pass and one string allocation. Convert.FromHexString is its inverse and accepts upper, lower or mixed case. Both arrived in .NET 5; the lowercase twin, ToHexStringLower, came in .NET 9. There are overloads for part of an array (offset and length) and for a ReadOnlySpan<byte>, so you can format a slice without copying it.

Program.cs

Output

Prints 000FA5FF10, then 000fa5ff10. Every byte becomes exactly two digits, so 0x00 stays 00 and the string is always twice the array length. The round trip prints True and 0, 15, 165, 255, 16, from lowercase input. The slices print A5FF (offset 2, length 2) and 0FA5FF. The SHA-256 digest of "hello" is 32 bytes, so its hex is 64 characters: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824, the lowercase form that tools such as sha256sum print. The [0x00, …] array syntax is a C# 12 collection expression; new byte[] { 0x00, … } does the same on older compilers.

2BitConverter.ToString on older frameworks

Before .NET 5, the built-in answer was BitConverter.ToString, which every .NET version has, including .NET Framework. It writes uppercase pairs separated by dashes, which is handy for reading a dump and rarely the format you want to store. Hence the most-copied hex one-liner in C#: .Replace("-", ""). There is no matching method that parses the dashed form back.

Program.cs

Output

Prints 00-0F-A5-FF-10, then 000FA5FF10 and 000fa5ff10 once the dashes are gone, and 0F-A5 for the two bytes from offset 1. Handing the dashed text to FromHexString fails with FormatException: The input is not a valid hex string as it contains a non-hex character. Strip the dashes and it reads all 5 bytes; splitting on - and parsing each pair with Convert.ToByte(p, 16) also gives 0, 15, 165, 255, 16. On .NET 5+, prefer Convert.ToHexString: the BitConverter route builds the dashed string and then a second string without the dashes.

3Format each byte with "x2"

The other classic answer formats one byte at a time. The format string "x2" means hex, lowercase, at least two digits; "X2" is the uppercase version. It is slower than Convert.ToHexString for the plain case, but it is the way to get any other layout: spaces between bytes, a 0x on each one for a C array literal, or colons like a MAC address.

Program.cs

Output

The StringBuilder loop prints 000fa5ff10 and the LINQ version 000FA5FF10. The third line, 0fa5ff10, is the bug to watch for: plain "x" writes 0x00 as 0 and 0x0F as f, so the output is shorter than it should be and can't be split back into bytes. The custom layouts print 00 0F A5 FF 10, 0x00, 0x0F, 0xA5, 0xFF, 0x10 and 00:0f:a5:ff:10. Interpolation holes take the same format after a colon, as in $"{b:X2}".

4Hex string back to a byte array

Convert.FromHexString is strict: an even number of hex digits and nothing else. A 0x prefix, spaces, dashes or a stray newline all throw FormatException, so clean up the formats you expect before parsing. On .NET 9+ there is also an overload that writes into a buffer you supply and returns an OperationStatus instead of throwing. On .NET Framework, parse two characters at a time with Convert.ToByte(pair, 16).

Program.cs

Output

Both cases of "48656C6C6F" give [72, 101, 108, 108, 111], the UTF-8 bytes of Hello. "ABC" fails because its length is not a multiple of 2, and "0x4865", "48 65 6C" and "zz" because it contains a non-hex character. After removing the prefix and spaces, the pasted text parses to 5 bytes. The buffer overload prints Done: 5 bytes, and on bad input InvalidData: 2 bytes: it stops at the first bad pair and tells you how much it wrote. The fallback helper prints 72, 101, 108, 108, 111; to turn those bytes into text, see converting between strings and byte arrays.

5An int as a hex string

For a single number, use the "X" format: n.ToString("X"), or {n:X} inside an interpolated string. A number after it sets the minimum width, padded with zeros. Convert.ToString(n, 16) does the same in lowercase and also does base 2 and 8. If what you actually want is the hex of the int's bytes, mind the byte order: BitConverter.GetBytes uses the machine's, and BinaryPrimitives lets you pick.

Program.cs

Output

2748 prints as ABC, abc, 00000ABC and 0x0ABC; the 0x is yours to add, the format never writes it. Convert.ToString gives abc and 101010111100 (on .NET 8+, the "B" format prints binary too). Negative values show their two's-complement bits rather than a minus sign: -1 is FFFFFFFF as an int and FFFFFFFFFFFFFFFF as a long, and -2748 is FFFFF544. A byte 200 is C8. The byte lines print True, then 78563412 for 0x12345678, because little-endian machines store the lowest byte first, and 12345678 once WriteInt32BigEndian puts it in network order.

6A hex string back to an int

Going the other way, pass NumberStyles.HexNumber to int.Parse or int.TryParse, or call Convert.ToInt32(s, 16). They differ on the prefix: Convert.ToInt32 skips a leading 0x, while int.Parse wants hex digits only and throws on it. For input you don't control, strip an optional prefix yourself and use TryParse. The rules for decimal text are in converting a string to an int.

Program.cs

Output

The first four lines all print 2748, and int.Parse with the prefix throws FormatException: The input string '0xABC' was not in a correct format. The TryParse loop prints True 255 for "ff" and "0xff", and False 0 for "fg" and the empty string. Then the sign trap: "FFFFFFFF" fills all 32 bits, so as an int it is -1, while uint and long both read 4294967295. That is the round trip of the previous section, so negative numbers come back intact. A ninth digit overflows: OverflowException: Value was either too large or too small for an Int32.

7Which should you use?

MethodOutputAvailableBest for
Convert.ToHexString(bytes)"000FA5FF10".NET 5+Almost everything: the default
Convert.ToHexStringLower(bytes)"000fa5ff10".NET 9+Hashes and tokens in lowercase
Convert.FromHexString(hex)byte[].NET 5+Parsing hex back; throws on bad input
BitConverter.ToString(bytes)"00-0F-A5-FF-10"Every version.NET Framework, readable dumps
b.ToString("x2") per byteAny layout you buildEvery versionSpaces, colons, 0x per byte
n.ToString("X")"ABC" for 2748Every versionOne int, long or byte as hex
int.Parse(s, NumberStyles.HexNumber)intEvery versionHex digits without a prefix
Convert.ToInt32(s, 16)intEvery versionHex text that may start with 0x

Frequently asked questions

What is the fastest way to convert a byte array to a hex string in C#?

On .NET 5 and later, Convert.ToHexString(bytes). It is part of the runtime, makes one pass and allocates only the result string, so it beats the classic BitConverter.ToString(bytes).Replace("-", ""), which builds two strings, and per-byte ToString("x2") loops, which format every byte separately. The hand-written lookup-table versions in old answers were a workaround for the missing API; on current .NET, use the built-in one.

Why can't I find Convert.ToHexString?

It was added in .NET 5, so it does not exist in .NET Framework 4.x, .NET Core 3.1 or .NET Standard 2.0 libraries. There, use BitConverter.ToString(bytes).Replace("-", "") to write hex and a loop over Convert.ToByte(hex.Substring(i * 2, 2), 16) to read it. Convert.ToHexStringLower needs .NET 9; before that, call Convert.ToHexString(bytes).ToLowerInvariant(), which turns 0xAB into "ab".

How do I convert a hex string to text, like "48656C6C6F" to "Hello"?

Hex holds bytes, so it takes two steps: parse the hex to bytes, then decode the bytes with the encoding they were written in. Encoding.UTF8.GetString(Convert.FromHexString("48656C6C6F")) gives "Hello", and Convert.ToHexString(Encoding.UTF8.GetBytes("Hi")) goes the other way and gives "4869". Characters outside ASCII take more than one byte: "é" in UTF-8 is "C3A9". Add using System.Text; for Encoding.

Why does int.Parse("0xFF", NumberStyles.HexNumber) throw a FormatException?

NumberStyles.HexNumber allows hex digits and surrounding whitespace (int.Parse(" FF ", NumberStyles.HexNumber) is 255), but not the 0x prefix, so "0xFF" fails with The input string '0xFF' was not in a correct format. Remove the prefix first, for example s[2..] after a StartsWith("0x", StringComparison.OrdinalIgnoreCase) check, or use Convert.ToInt32(s, 16), which accepts the prefix but not whitespace: Convert.ToInt32(" FF ", 16) throws Could not find any recognizable digits.

How do I parse a negative hex number like "-FF"?

Neither parser accepts a minus sign in hex: int.Parse("-FF", NumberStyles.HexNumber) throws a FormatException, and Convert.ToInt32("-FF", 16) throws ArgumentException: String cannot contain a minus sign if the base is not 10. Hex text for a negative number is normally two's complement, so int.Parse("FFFFFFFF", NumberStyles.HexNumber) is -1. If your text really has a sign, strip it, parse the digits and negate: -int.Parse("FF", NumberStyles.HexNumber) is -255.

Why does BigInteger.Parse("FF", NumberStyles.HexNumber) return -1?

BigInteger has no fixed width, so it treats the top bit of the first hex digit as the sign: "FF" is the two's-complement byte for -1. Put a 0 in front to keep it positive: BigInteger.Parse("0FF", NumberStyles.HexNumber) is 255. int, uint and long don't have this problem until the text fills all their bits.

Should I store binary data as hex or Base64?

Hex is easier to read and compare by eye and is the convention for hashes, but it doubles the size: a 32-byte SHA-256 digest is 64 hex characters and 44 Base64 characters with Convert.ToBase64String. Use hex for hashes, IDs and debugging output, and Base64 when size matters, such as tokens in URLs, JSON payloads or data stored in text columns.

Run it yourself

Open any of these in the full C# editor to tweak, run and share.

C# playground