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.

C# String Comparison Methods at a Glance Method Returns Culture default Best use == / != bool Ordinal (fast) Simple equality — non-null, case-sensitive string.Equals() bool Ordinal (default) Equality + explicit StringComparison; null-safe string.Compare() int CurrentCulture ⚠ Ordering / sort — always pass StringComparison a.CompareTo(b) int CurrentCulture ✗ IComparable impl — no StringComparison override String.CompareOrdinal() int None — raw UTF-16 Fastest ordering — IDs, paths, hash buckets Safe default Caution — specify StringComparison Avoid in app code

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 bool equality check → use == or string.Equals()
  • Need case-insensitive equality → use string.Equals(a, b, StringComparison.OrdinalIgnoreCase)
  • Need ordering (for sorting) → use string.Compare() with an explicit StringComparison
  • Need the fastest ordinal int comparison → use String.CompareOrdinal()
  • Using IComparable or LINQ OrderBy → be aware CompareTo() defaults to CurrentCulture

Method 1: The == and != Operators

How C# == Compares Strings Internally a == b String.op_Equality Length check a.Length == b.Length? differ return false (fast) match Ordinal UTF-16 scan SIMD char comparison return true / false Ordinal · case-sensitive · culture-INsensitive · no collation tables loaded Compiled to String.Equals(a, b) — same speed as String.CompareOrdinal() for equality

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

StringComparison Enum — 6 Values Matrix Case-Sensitive Case-Insensitive Ordinal Raw UTF-16 · no culture Ordinal Fastest · recommended for IDs paths, keys, tokens OrdinalIgnoreCase Very fast · ASCII bit-mask HTTP headers, file extensions InvariantCulture Fixed English-like rules InvariantCulture Persisted/serialized data · cross-machine consistent · moderate speed InvariantCulture IgnoreCase Cross-machine, case-insensitive stored data · moderate speed CurrentCulture Machine locale · variable CurrentCulture Display-only · locale-dependent Slowest · ⚠ never for identifiers CurrentCulture IgnoreCase User text search · local language rules · slowest option

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:

  1. It allocates. ToLower() creates a new string on every call. In a loop or a hot path, this adds garbage-collector pressure. OrdinalIgnoreCase does not allocate.
  2. It is culture-dependent. string.ToLower() without arguments uses CurrentCulture. The result can differ between machines — a real problem when the lowercased string is then stored in a database or compared across services.
  3. 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 Turkish İ Problem: "file" vs "FILE" Breaks Under tr-TR string s = "file" ToUpper() called — which culture? en-US / Ordinal tr-TR culture "FILE" i → I (U+0049, standard) Matches "FILE" — correct "FİLE" i → İ (U+0130, dotted İ) ≠ "FILE" — comparison FAILS "file" == "FILE" → true ✓ "file" == "FILE" → false ✗ Fix: string.Equals("file", "FILE", StringComparison.OrdinalIgnoreCase) → always true on every culture

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?

Relative Throughput: C# String Comparison Methods Higher bar = faster · relative characterization, -O release build · .NET 8 x64 JIT == / Ordinal CompareOrdinal OrdinalIgnore Case Invariant Culture InvariantCI CurrentCulture CurrentCulCI 25% 50% 75% 100% 120% 100% — baseline ~100% ~80% ~25% ~22% ~12% ~10% Fastest (no culture tables) Fast (ASCII table) Moderate (invariant) Slowest (locale tables)

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

Visual Diff: Spotting the Invisible Character Bug in C# Watch — Locals s1 "hello" s2 "hello" s1 == s2 false ← Look identical… but == returns false Trailing \r[13] hidden by IDE paste into Diff Checker Diff Checker LEFT (s1) hello RIGHT (s2) hello \r 1 difference found s2 has trailing \r (CR, byte 13) at pos 5 Custom stream reader, no line-ending strip Common invisible characters that cause C# == to return false: \r Trailing CR from ReadLines() BOM UTF-8 BOM (0xEF BB BF) U+00A0 Non-breaking space U+200B Zero-width space (invisible) NFC/NFD Unicode normalization mismatch

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() and File.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.

Add to Chrome — It's Free