Compare strings in C# sounds simple until you hit a bug at 2 AM: two
strings that look identical in the debugger return false from
==, or a sort order breaks for users in Turkey, or a config key lookup
silently fails because someone used CurrentCulture instead of
Ordinal. C# gives you six distinct methods to compare strings
in C# — each with different return types, culture rules, and performance
characteristics. This guide covers all of them precisely, with copy-paste code examples,
the StringComparison enum fully mapped, the Turkish İ problem explained,
and the one debugging technique most tutorials skip. For a multi-language overview, see the
string comparison across languages guide first;
the C# section here goes considerably deeper.
Why C# String Comparison Trips Up Developers
The C# runtime stores strings as sequences of UTF-16 code units. Every character is
one or two char values. The comparison problem is not storage — it is that
.NET's string comparison methods have historically defaulted to culture-sensitive behavior,
meaning the same code can produce different results on machines with different regional
settings. A string sort that works on an English-language developer laptop can silently
break when the same application runs on a Turkish server.
The root issue: C# string comparison methods do not all share the same default.
== uses ordinal (culture-independent) comparison. string.CompareTo()
uses CurrentCulture by default. string.Compare() static method
also uses CurrentCulture unless you pass a StringComparison
argument. This inconsistency has caused real production bugs across the .NET ecosystem.
Microsoft's official "Best Practices for Comparing Strings"
guidance is direct: always specify a StringComparison value rather than
relying on defaults. The rest of this article maps each method to the right use case so
you can follow that guidance without memorizing the entire MSDN page. If you come from
Java, the Java string equality guide
covers how .equals() and compareTo() compare to these C#
equivalents.
Here is a quick orientation before the deep dives:
- Need a
boolequality check → use==orstring.Equals() - Need case-insensitive equality → use
string.Equals(a, b, StringComparison.OrdinalIgnoreCase) - Need ordering (for sorting) → use
string.Compare()with an explicitStringComparison - Need the fastest ordinal int comparison → use
String.CompareOrdinal() - Using
IComparableor LINQOrderBy→ be awareCompareTo()defaults toCurrentCulture
Method 1: The == and != Operators
In C#, the == operator on string types is not a
reference comparison — it is overloaded by the String class to perform
an ordinal, case-sensitive value comparison. This is one of the most
important facts to internalize when you compare strings in C#: unlike
Java (where == compares references), C# == on strings compares
the actual character sequences.
string a = "hello";
string b = "hello";
string c = "Hello";
Console.WriteLine(a == b); // True — same characters, ordinal
Console.WriteLine(a == c); // False — 'h' != 'H', ordinal, case-sensitive
Console.WriteLine(a != c); // True
// Reference equality (rarely what you want)
Console.WriteLine(object.ReferenceEquals(a, b)); // True (interning) or False
Under the hood, the C# compiler lowers == on strings to
String.Equals(a, b), which performs a length check followed by an ordinal
character scan. Because it is ordinal, there is no culture table lookup —
== is the fastest equality test for non-null strings.
One edge case: if either operand is null, == returns
false without throwing (null-safe). But calling
a.Equals(b) on a null a throws
NullReferenceException. For null-safe instance calls use the static
string.Equals(a, b) form instead.
string? x = null;
string y = "test";
Console.WriteLine(x == y); // False — no exception
// Console.WriteLine(x.Equals(y)); // NullReferenceException!
Console.WriteLine(string.Equals(x, y)); // False — null-safe static form
When to use ==: simple equality checks where both operands
are non-null and you want ordinal, case-sensitive comparison. It is the most readable
form and the compiler knows exactly what it means. For C++ developers: this is similar to
how std::string equality works in the
C++ string compare guide, except in C# the
behavior is guaranteed and does not vary by implementation.
Method 2: string.Equals() and StringComparison
string.Equals() is the most flexible comparison method in C# because it
accepts a StringComparison enum value that gives you explicit, documented
control over culture sensitivity and case sensitivity in a single call.
Two forms: instance and static. Prefer the static form for null safety.
string a = "Straße";
string b = "strasse";
// Instance form — throws if a is null
bool r1 = a.Equals(b); // False (ordinal by default)
bool r2 = a.Equals(b, StringComparison.CurrentCulture); // True in de-DE culture (ß == ss)
bool r3 = a.Equals(b, StringComparison.Ordinal); // False (byte values differ)
// Static form — null-safe
bool r4 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // False
bool r5 = string.Equals("hello", "HELLO", StringComparison.OrdinalIgnoreCase); // True
Microsoft's recommendation for C# string comparison
of non-linguistic strings (identifiers, file paths, config keys, URLs, dictionary keys):
use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase.
These are immune to culture bugs, and they are faster than any culture-sensitive option.
For user-visible text that must sort or compare according to human language rules — for
example, displaying a sorted list of names to a French user — use
StringComparison.CurrentCulture or
StringComparison.CurrentCultureIgnoreCase. That way the comparison respects
the regional settings of the machine running the application.
// Recommended pattern for identifiers, paths, keys
bool keysMatch = string.Equals(configKey, "MaxRetries", StringComparison.OrdinalIgnoreCase);
// Recommended pattern for user-visible text sort
bool namesMatch = string.Equals(lastName1, lastName2, StringComparison.CurrentCultureIgnoreCase);
Method 3: string.Compare() for Sorting and Ordering
string.Compare() is the static method to reach for when you need an
ordering result — the three-valued int that LINQ's OrderBy,
custom IComparer implementations, and SortedDictionary all
expect. It returns a negative integer if the first string is less, zero if equal, and a
positive integer if greater. Like string.Equals(), it takes a
StringComparison overload that you should always use explicitly.
int result1 = string.Compare("apple", "banana", StringComparison.Ordinal);
// result1 < 0 — "apple" sorts before "banana"
int result2 = string.Compare("banana", "apple", StringComparison.Ordinal);
// result2 > 0
int result3 = string.Compare("apple", "apple", StringComparison.Ordinal);
// result3 == 0 — equal
// Culture-sensitive (for sorting user-visible strings)
int result4 = string.Compare("café", "cafe", StringComparison.CurrentCulture);
// May be 0 or non-zero depending on culture — CurrentCulture on fr-FR treats accented e as equal
Only check the sign of the return value — do not rely on the exact
integer. The standard only guarantees negative, zero, or positive. Checking
result == -1 is a bug waiting to happen.
A real-world use case: implementing IComparer<string> for a
SortedSet with case-insensitive, culture-invariant ordering:
public class OrdinalIgnoreCaseComparer : IComparer<string>
{
public int Compare(string? x, string? y)
=> string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
}
var set = new SortedSet<string>(new OrdinalIgnoreCaseComparer());
set.Add("Banana");
set.Add("apple");
set.Add("cherry");
// Sorted: apple, Banana, cherry (ordinal ignore case)
string.Compare() also has an overload that accepts a
CultureInfo and a boolean ignoreCase parameter, giving you
precise control for internationalized applications. See the
Microsoft Learn "How to compare strings (C# Guide)"
for the complete overload list. For JavaScript developers, the equivalent is
localeCompare() — see the JavaScript string equals guide for the parallel.
Method 4: string.CompareTo() and IComparable
string.CompareTo() is the instance method that the IComparable<string>
interface requires. It returns the same three-valued int as
string.Compare(), but with a critical default you must know:
CompareTo() always uses CurrentCulture.
There is no overload that accepts StringComparison.
string s1 = "apple";
string s2 = "banana";
int r = s1.CompareTo(s2);
// r < 0 — "apple" < "banana" — but comparison is CurrentCulture!
// Null argument returns 1 (any string is greater than null per IComparable contract)
int r2 = s1.CompareTo(null); // 1
The culture-sensitive default means CompareTo() can produce different sort
orders on machines with different regional settings. This is why Microsoft's best practices
guide advises against using CompareTo() directly in application code and
instead recommends passing an explicit StringComparer to collection
constructors and LINQ methods.
// RISKY — sort order depends on machine culture
list.Sort(); // calls CompareTo() internally
// SAFE — explicit, predictable, culture-independent
list.Sort(StringComparer.OrdinalIgnoreCase);
// SAFE — LINQ with explicit comparer
var sorted = list.OrderBy(x => x, StringComparer.Ordinal).ToList();
The CompareTo() method exists primarily for implementing
IComparable<T> on custom types where string is a field. When you
implement that interface, call string.Compare() with an explicit comparison
internally rather than delegating to CompareTo(). For a detailed look at
how compareTo works in Java — which shares the same method name but
different culture semantics — see the compareTo deep dive.
Method 5: String.CompareOrdinal() — Fast and Culture-Free
String.CompareOrdinal() is the most direct ordinal comparison available —
it compares strings by their raw UTF-16 code unit values with no culture table lookups
whatsoever. It returns a signed integer (<0, 0,
>0), making it suitable for ordering as well as equality.
int r1 = String.CompareOrdinal("apple", "Apple");
// r1 > 0 — lowercase 'a' (97) > uppercase 'A' (65) in UTF-16
int r2 = String.CompareOrdinal("test", "test");
// r2 == 0
int r3 = String.CompareOrdinal("abc", "abd");
// r3 < 0
// Substring overload — compare substrings without allocation
int r4 = String.CompareOrdinal("config.json", 7, "json", 0, 4);
// r4 == 0 — "json" == "json"
string.CompareOrdinal is the best choice when:
- You are implementing a hash map bucket comparison or binary search where performance matters.
- You are comparing machine-readable identifiers, file extensions, or protocol tokens.
- You need substring comparison without constructing a temporary string — the
(string1, int offset1, string2, int offset2, int count)overload handles this.
Note: String.CompareOrdinal() is not null-safe — passing null throws
ArgumentNullException. Guard nulls explicitly or use
string.Compare(a, b, StringComparison.Ordinal) which handles nulls gracefully
(null is considered less than any non-null string).
The StringComparison Enum: All 6 Values Explained
The StringComparison enum is the central concept for correct
C# string comparison. Every time you call string.Equals(),
string.Compare(), string.IndexOf(),
string.StartsWith(), or string.Contains() with a string
argument, you should pass one of these six values.
| Value | Culture | Case | Speed | Best for |
|---|---|---|---|---|
Ordinal | None (raw UTF-16) | Sensitive | Fastest | File paths, identifiers, protocol strings, dictionary keys |
OrdinalIgnoreCase | None (ASCII case table) | Insensitive | Very fast | Case-insensitive identifiers, HTTP header names, file extensions |
CurrentCulture | Thread's current culture | Sensitive | Slow | User-visible text sorted for display on the current machine |
CurrentCultureIgnoreCase | Thread's current culture | Insensitive | Slowest | Case-insensitive user text search respecting local language rules |
InvariantCulture | Invariant (English-like, fixed) | Sensitive | Moderate | Persisted data that must sort consistently across all machines |
InvariantCultureIgnoreCase | Invariant (English-like, fixed) | Insensitive | Moderate | Persisted data, case-insensitive, cross-machine consistent |
Practical decision rule: if the string represents something a machine
reads (a key, a path, a token), use Ordinal or OrdinalIgnoreCase.
If the string is something a human reads and the comparison result must match human
expectations for a given locale, use CurrentCulture or
CurrentCultureIgnoreCase. If the string must compare consistently across
different machines (logging, stored configs, serialized data), use
InvariantCulture.
A StringComparer (note: different class from the enum) wraps these values as
objects you can pass to collection constructors. Every
StringComparison enum value has a corresponding
StringComparer static property:
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
dict["KEY"] = 1;
Console.WriteLine(dict["key"]); // 1 — case-insensitive lookup
Console.WriteLine(dict["Key"]); // 1 — same bucket
Case-Insensitive String Comparison in C#
Case-insensitive string comparison in C# has one correct pattern and several common wrong patterns. Here is the difference:
string a = "Hello";
string b = "hello";
// WRONG — allocates a new string, culture-dependent, harder to read
if (a.ToLower() == b.ToLower()) { }
// ALSO WRONG — ToUpper has the same problems, plus the "Turkey Test" pitfall
if (a.ToUpper() == b.ToUpper()) { }
// CORRECT — no allocation, explicit, fast
if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) { }
// ALSO CORRECT — for user-visible text on the current machine
if (string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase)) { }
Why is ToLower() wrong? Three reasons:
- It allocates.
ToLower()creates a new string on every call. In a loop or a hot path, this adds garbage-collector pressure.OrdinalIgnoreCasedoes not allocate. - It is culture-dependent.
string.ToLower()without arguments usesCurrentCulture. The result can differ between machines — a real problem when the lowercased string is then stored in a database or compared across services. - It breaks on non-ASCII. The Turkish İ problem (see the next section) means
ToUpper("i")produces"İ"(dotted I) in Turkish culture, causing equality checks to fail in ways that are extraordinarily hard to debug.
For string.StartsWith(), string.EndsWith(), and
string.Contains(), apply the same pattern — always pass a
StringComparison value:
string path = "/Users/Pavel/Documents/report.PDF";
bool isPdf = path.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase); // True
bool isDoc = path.Contains("documents", StringComparison.OrdinalIgnoreCase); // True
bool isUser = path.StartsWith("/users", StringComparison.OrdinalIgnoreCase); // True
The Turkish İ Problem and Culture Pitfalls
The classic culture pitfall in .NET is known as the Turkish I problem (sometimes written
the "Turkey Test"). In Turkish, the uppercase form of the lowercase dotless i
is a dotted İ (U+0130), and the lowercase form of uppercase dotless
I is a dotless ı (U+0131). This is linguistically correct for
Turkish — but it breaks any C# code that compares strings containing the letter "i" or "I"
using culture-sensitive methods on a machine with Turkish locale settings.
// On a machine with tr-TR (Turkish) culture:
string s1 = "file";
string s2 = "FILE";
// BREAKS under Turkish culture — ToUpper("i") = "İ", not "I"
bool wrong = string.Compare(s1, s2, ignoreCase: true) == 0;
// wrong may be FALSE even though "file" and "FILE" should be case-insensitively equal
// ALWAYS SAFE — ordinal case-insensitive uses a fixed ASCII table
bool correct = string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase);
// correct == True — always, on every culture
The problem is not hypothetical. Several well-known open-source libraries have shipped
this bug. The .NET Framework itself had internal string comparisons that failed when
deployed to Turkish Azure regions. Microsoft's "Best Practices for Comparing Strings"
documentation explicitly cites this scenario and recommends Ordinal or
OrdinalIgnoreCase for any string that represents a programming artifact
(identifiers, file names, XML element names, HTTP headers, configuration keys, URLs).
The rule of thumb: if a human wrote the string as part of UI content, consider
CurrentCulture. If a developer or a protocol defined the string, use
Ordinal. The Java char comparison guide
covers the analogous UTF-16 character issues that affect Java's Character.toUpperCase()
for the same reasons.
Performance: Which C# String Comparison Is Fastest?
The following performance characterizations hold across .NET 8 on x64 with JIT optimization enabled. The exact numbers vary by string length, content, and CPU, but the relative ordering of methods is consistent:
| Method / Comparison Type | Short strings (<16 chars) | Long strings (1 KB+) | Relative cost |
|---|---|---|---|
== (ordinal, case-sensitive) | Fastest | Fastest | Baseline |
String.CompareOrdinal() | Fastest | Fastest | ~1x baseline |
StringComparison.Ordinal | Fastest | Fastest | ~1x baseline |
StringComparison.OrdinalIgnoreCase | Fast (ASCII table lookup) | Fast | ~1.2–1.5x |
StringComparison.InvariantCulture | Moderate | Moderate | ~3–5x |
StringComparison.InvariantCultureIgnoreCase | Moderate | Moderate | ~3–5x |
StringComparison.CurrentCulture | Slow (loads culture tables) | Slowest | ~5–10x |
StringComparison.CurrentCultureIgnoreCase | Slowest | Slowest | ~5–10x |
Why is Ordinal fastest? It compares raw UTF-16 code units sequentially,
first checking lengths (immediate exit on mismatch), then walking the character arrays.
On modern .NET, the JIT emits SIMD instructions (SSE2 / AVX2) for the inner loop, making
long-string ordinal comparison extremely fast. CurrentCulture comparisons
must load collation weight tables, apply Unicode normalization, handle
locale-specific expansion (ß → ss in German), and check combining characters — a
fundamentally more expensive operation.
OrdinalIgnoreCase is fast because .NET's implementation handles ASCII
characters with a simple bit-mask operation (the difference between uppercase and
lowercase ASCII letters is exactly one bit: 0x20). Non-ASCII characters
fall back to a Unicode table, but for the vast majority of identifiers and file paths —
which are ASCII — this is negligible.
Practical guidance: for hot paths (tight loops, search indexes, hash bucket comparisons),
always use Ordinal or OrdinalIgnoreCase. Reserve
CurrentCulture for operations that occur once (rendering a sorted list for
display) rather than thousands of times.
Debugging Hidden String Mismatches: When Strings Look Identical but Compare Unequal
Here is a debugging scenario that every C# developer hits eventually. You
compare two strings in C#, == returns false,
but both strings print identically to the console. You check the lengths — they match.
You check the values in the debugger Watch window — they look the same. You stare at
the screen for twenty minutes.
The cause is almost always an invisible character. The most common culprits in C# are:
- Trailing
\r(carriage return, byte 13) — can occur when manually parsing or splitting files without accounting for line-ending normalization, or when using certain stream readers that don't strip terminators.File.ReadAllLines()andFile.ReadLines()both automatically remove CRLF line terminators, so the issue typically arises from custom text processing rather than the standard API. - Non-breaking space (U+00A0) — copied from a web page or a Word document. Renders identically to a regular space in most terminals and IDEs.
- UTF-8 BOM — a string read from a UTF-8 file that was saved with a BOM (
0xEF 0xBB 0xBF) may start with the BOM character (), invisible in most debug windows. - Unicode normalization mismatch (NFC vs NFD) — the character "é" can be encoded as a single precomposed code point U+00E9, or as the letter "e" (U+0065) followed by a combining acute accent (U+0301). Both render identically; ordinal comparison returns non-zero because the code unit sequences differ. This commonly happens when one string comes from a macOS file system (which normalizes to NFD) and another comes from a Windows API (which typically uses NFC).
- Zero-width space (U+200B) — invisible by definition, sometimes injected by word processors or CMS systems.
The fastest way to diagnose this in code is to print each character's integer code point value:
void PrintCharCodes(string s, string label)
{
Console.Write($"{label} (len={s.Length}): ");
foreach (char c in s)
{
if (c < 32 || c > 126)
Console.Write($"[U+{(int)c:X4}]");
else
Console.Write(c);
}
Console.WriteLine();
}
// Usage:
string s1 = "config";
string s2 = GetValueFromFile(); // suspect string
PrintCharCodes(s1, "s1");
PrintCharCodes(s2, "s2");
// Typical output:
// s1 (len=6): config
// s2 (len=7): config[U+000D] ← trailing \r revealed
The programmatic approach works well in unit tests and CI pipelines. For interactive debugging, a faster workflow is to paste both string values into Diff Checker, the browser-based diff tool. Paste the first string value from your debug output into the left pane and the second into the right pane. The tool runs the comparison in your browser and highlights the exact changed region within each line — so a line that looks identical in both panes is flagged as different, pointing you directly to where the invisible character sits. The "Ignore whitespace" toggle lets you rule out or confirm whitespace as the source. C# syntax highlighting (via Monaco editor) is available if you are comparing code strings rather than plain values.
This is the same approach the Compare Two Files in VS Code
guide uses for file-level diffs — the same principle applies when your diff is a single
string rather than a whole file. For command-line workflows on Unix systems, the
diff command in Unix guide covers
xxd and od for hex-dumping strings to spot invisible bytes.
Once you know the culprit, the fix is usually one of:
// Strip trailing whitespace and control chars
string clean = s2.Trim();
// Remove specific character
string noCarriageReturn = s2.Replace("\r", "");
// Normalize Unicode to NFC before comparison
string normalized = s2.Normalize(NormalizationForm.FormC);
// Compare with normalization
bool equal = string.Equals(
s1.Normalize(NormalizationForm.FormC),
s2.Normalize(NormalizationForm.FormC),
StringComparison.Ordinal);
Common Mistakes and How to Avoid Them
These are the five most common errors when developers compare strings in C#, each with the wrong pattern and the correct replacement.
Mistake 1: Using == when you needed OrdinalIgnoreCase
// WRONG — case-sensitive, misses "Config", "CONFIG"
if (key == "maxRetries") { }
// CORRECT — case-insensitive, culture-safe
if (string.Equals(key, "maxRetries", StringComparison.OrdinalIgnoreCase)) { }
Mistake 2: ToLower() or ToUpper() before comparison
// WRONG — allocates, culture-sensitive, Turkish I bug risk
if (s.ToLower() == "accept") { }
// CORRECT — no allocation, ordinal, immune to culture bugs
if (string.Equals(s, "accept", StringComparison.OrdinalIgnoreCase)) { }
Mistake 3: Relying on the exact int value from Compare()
// WRONG — only sign is defined by the spec, not the value
if (string.Compare(a, b, StringComparison.Ordinal) == -1) { }
// CORRECT — check sign only
if (string.Compare(a, b, StringComparison.Ordinal) < 0) { }
Mistake 4: Using CurrentCulture for machine strings
// WRONG — sort order of file extensions differs by culture
files.Sort(); // CompareTo() uses CurrentCulture internally
// CORRECT — deterministic, culture-free sort
files.Sort(StringComparer.Ordinal);
Mistake 5: ToUpper() in a loop for repeated comparisons
// WRONG — allocates a new string on every iteration
foreach (var item in largeList)
{
if (item.ToUpper() == "ACTIVE") { process(item); }
}
// CORRECT — zero allocation, same speed for every iteration
foreach (var item in largeList)
{
if (string.Equals(item, "ACTIVE", StringComparison.OrdinalIgnoreCase)) { process(item); }
}
Best C# String Comparison Methods Compared
Use this table as a quick reference for choosing the right method when you need a csharp string compare for any scenario. For a multi-language view covering Python, Go, JavaScript, and Java alongside C#, the string compare overview is the best starting point. For the C++ perspective on the same problem space, see the C++ string compare guide.
| Method | Return type | Default culture | Case option | Best for |
|---|---|---|---|---|
== / != | bool | None (ordinal) | Sensitive only | Simple equality, non-null strings |
string.Equals(a, b, cmp) | bool | Ordinal (default) or via StringComparison | Any StringComparison | Equality with explicit culture/case control; null-safe static form |
string.Compare(a, b, cmp) | int (<0, 0, >0) | CurrentCulture (avoid default) | Any StringComparison | Ordering, sorting, custom IComparer |
a.CompareTo(b) | int (<0, 0, >0) | CurrentCulture (no override) | None | Implementing IComparable<string> — use with caution |
String.CompareOrdinal(a, b) | int (<0, 0, >0) | None (ordinal) | Sensitive only | Fastest ordering, internal lookups, substring comparison |
StringComparer.OrdinalIgnoreCase | Used as IComparer | None (ordinal) | Insensitive | Passing to Dictionary, SortedSet, LINQ |
Frequently Asked Questions
What is the difference between == and string.Equals() in C#?
In C#, == on string types performs an ordinal, case-sensitive value
comparison — it is overloaded by the String class and does not compare
references (unlike Java). string.Equals() does the same thing by default,
but its key advantage is the overload that accepts a StringComparison
enum value, giving you explicit, documented control over culture sensitivity and case
sensitivity. Use == for quick, readable equality checks on non-null strings.
Use string.Equals(a, b, StringComparison.OrdinalIgnoreCase) when you need
case-insensitive or explicitly culture-controlled comparison, or when either string could
be null (the static form handles nulls without throwing).
How do I do case-insensitive string comparison in C#?
The correct pattern is string.Equals(a, b, StringComparison.OrdinalIgnoreCase)
for non-linguistic strings (identifiers, file paths, HTTP header names, config keys) or
StringComparison.CurrentCultureIgnoreCase for user-visible text that must
respect the active locale. Avoid ToLower() or ToUpper() before
comparing — that approach allocates a new string on every call, is culture-dependent by
default, and can fail under the Turkish İ locale bug. Passing a
StringComparison value directly is zero-allocation, explicit, and immune to
culture-related breakage.
Should I use StringComparison.Ordinal or InvariantCulture?
Use StringComparison.Ordinal for machine-readable strings: file paths, URLs,
configuration keys, identifiers, protocol tokens, dictionary keys. Ordinal is the fastest
option and is completely immune to culture-related bugs including the Turkish İ problem.
Use StringComparison.InvariantCulture when you need linguistically plausible
comparison results that are consistent across all machines — for example, sorting strings
that will be stored and compared across different servers with different regional settings.
Microsoft's official guidance
recommends Ordinal as the default for all non-user-facing string comparisons.
Why does my C# string comparison fail when the strings look the same?
The most common causes are invisible characters: a trailing \r from a
custom stream reader or manual string splitting that does not strip Windows line endings
(note: File.ReadLines() strips CRLF automatically), a non-breaking space
(U+00A0) copied from a web page, a UTF-8 BOM () at the start of a
file-sourced string, or a Unicode normalization difference (NFC vs NFD — the same accented
character encoded as one precomposed code point versus a base letter plus a combining
accent). Print each character's integer code point with a diagnostic loop or paste both
string values into a visual diff tool to pinpoint the exact position of the hidden
character.
What is the fastest way to compare strings in C#?
String.CompareOrdinal() and == (which uses ordinal comparison
internally) are the fastest options. Both bypass Unicode collation tables entirely and
compare raw UTF-16 code unit values, with a length short-circuit that exits immediately
if lengths differ. string.Equals(a, b, StringComparison.Ordinal) is
equally fast. OrdinalIgnoreCase is close behind — it uses a fixed ASCII
case-folding table for the common case. CurrentCulture and
CurrentCultureIgnoreCase are measurably slower because they load collation
weight tables and apply Unicode normalization. Characterizations hold across .NET 8 on
x64 with JIT optimization enabled.
Spot Invisible String Differences Instantly — Free
When your C# csharp string compare returns false but
both strings look identical, paste them into Diff Checker. The comparison runs in your
browser and highlights the exact changed region — trailing \r,
non-breaking spaces, BOM characters, and Unicode normalization mismatches are all
flagged, even when they are invisible in your IDE's Watch window.