Back to Blog
C#

C# StringComparison: Choosing the Right Comparison Mode

c# stringcomparison: Understand C# StringComparison modes, why culture affects results, and how to choose ordinal or culture-aware comparison to avoid subtle bugs.

StringComparisonCulture-sensitive comparisonOrdinal comparisonString equality.NET
Editorial illustration of C# string comparison showing ordinal and culture-aware paths leading to different comparison outcomes.

String comparison in C# is not a single operation. The result of comparing two strings depends on the StringComparison mode you pass, and choosing the wrong mode produces bugs that only appear when the environment changes. Understanding how c# stringcomparison works means knowing what each mode does and when to use it.

What the Six StringComparison Values Mean

The StringComparison enum in C# controls how two strings are compared. The six values fall into two families: ordinal comparisons that work directly on character code units, and culture-aware comparisons that apply linguistic rules from a specific culture.

ValueBehaviorTypical use
OrdinalCase-sensitive, compares code units directlyFile paths, identifiers, configuration keys
OrdinalIgnoreCaseCode-unit comparison with simple case foldingCase-insensitive identifiers, headers
CurrentCultureCulture-aware rules from the current thread cultureUser-facing text that must sort by language rules
CurrentCultureIgnoreCaseCulture-aware, case-insensitiveUser-facing case-insensitive matching
InvariantCultureCulture-aware rules from a fixed English-like culturePersisted or exchanged data needing stable results
InvariantCultureIgnoreCaseInvariant culture, case-insensitiveCross-system case-insensitive data matching

The distinction matters because the same pair of strings can compare equal under one mode and unequal under another. A comparison that passes in your development environment can fail in production if the thread culture differs.

Why Culture-Sensitive Comparison Produces Unexpected Results

Culture-aware comparison does not simply fold uppercase and lowercase letters. It consults the culture's casing tables and sorting rules, which can treat characters differently than English-based assumptions suggest.

The Turkish-I problem is the classic example. In Turkish, the uppercase counterpart of i is İ (dotted capital I), and the lowercase counterpart of I is ı (dotless small i). The following code returns false when the current culture is Turkish:

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR"); bool equal = string.Equals("FILE", "file", StringComparison.CurrentCultureIgnoreCase);

In an English culture the same comparison returns true, because I and i are a simple case pair. The same code therefore produces different results on machines with different regional settings.

German provides another example: the sharp s (ß) can be treated as equivalent to ss in some culture-aware comparisons, so "straße" and "strasse" may compare equal under CurrentCulture in German but not under Ordinal. These rules are deliberate, but they are easy to forget when you are comparing strings that are not meant to be interpreted as human language.

Passing StringComparison to the Standard String APIs

Most string APIs have overloads that accept a StringComparison argument. Using them makes the comparison semantics explicit and removes the guesswork about which default applies.

bool equal = string.Equals("apple", "APPLE", StringComparison.OrdinalIgnoreCase); int order = string.Compare("apple", "banana", StringComparison.CurrentCulture); bool contains = "The quick brown fox".Contains("QUICK", StringComparison.OrdinalIgnoreCase); bool starts = "config.json".StartsWith("CONFIG", StringComparison.OrdinalIgnoreCase); int index = "a-b-c".IndexOf("B", StringComparison.OrdinalIgnoreCase);

The Contains, StartsWith, EndsWith, IndexOf, and LastIndexOf overloads that accept StringComparison are available from .NET Core 2.1 onward. On .NET Framework, string.Contains(string) has no such overload; use IndexOf and check for a non-negative result instead:

bool contains = "The quick brown fox".IndexOf("QUICK", StringComparison.OrdinalIgnoreCase) >= 0;

Choosing Between Ordinal and Culture-Aware Comparison

The decision depends on what the strings represent, not on how the code is written.

Use Ordinal or OrdinalIgnoreCase for strings that are not user-facing language: file paths, URLs, environment variable names, enum names, cache keys, and any internal identifier. These strings have a fixed byte representation, and comparing them by code units is both deterministic and fast.

Use CurrentCulture or CurrentCultureIgnoreCase for text that will be displayed or sorted for the user in their own language. A product catalog sorted by name should respect the user's language rules, including special characters and accent ordering.

Use InvariantCulture for data that crosses system boundaries, such as values written to a database or sent over an API, when you need the comparison result to be stable regardless of the machine running the code. Invariant culture is based on English rules but does not change when the thread culture changes.

The general rule: if the string is an identifier, compare ordinally; if it is human language, compare with the culture that matches the audience.

Performance and Allocation Behavior

Ordinal comparison works directly on the underlying character values. It performs no culture lookup and applies no linguistic rules, so it is the cheapest comparison mode. Culture-aware comparison must load culture-specific casing and sorting data and apply those rules to the input, which is more work per comparison.

A common anti-pattern is normalizing case before comparing:

if (input.ToLower() == "admin") { // ... }

ToLower() allocates a new string and, by default, uses the current culture's casing rules. Replacing it with an explicit comparison avoids the allocation and makes the semantics deterministic:

if (string.Equals(input, "admin", StringComparison.OrdinalIgnoreCase)) { // ... }

The performance difference is not about micro-optimization in most applications; it is about avoiding repeated allocations and culture lookups in hot paths, and about making the behavior predictable.

Common Pitfalls That Create Inconsistent Behavior

The == operator for strings performs an ordinal comparison. Mixing == with culture-aware method calls in the same codebase produces inconsistent semantics for the same pair of strings. Prefer one comparison mode throughout a code path.

Default overloads are not consistent with each other. string.Compare(a, b) defaults to CurrentCulture, while string.Equals(a, b) defaults to ordinal. string.IndexOf(string) was culture-sensitive on .NET Framework and is ordinal on .NET Core. Relying on defaults therefore couples your behavior to the runtime version and the thread culture.

Case-insensitive comparison of file paths is another trap. Windows file systems are case-insensitive, while Linux file systems are case-sensitive. Comparing paths with Ordinal on Linux will treat Config.json and config.json as different files, which can cause subtle bugs when the same code runs on both operating systems. Decide explicitly whether paths are compared case-insensitively and apply that decision consistently.

Keeping Comparison Semantics Consistent in a Codebase

When comparison semantics are implicit, the same logical comparison can be implemented differently in different layers of an application. A validation layer using OrdinalIgnoreCase and a lookup layer using CurrentCultureIgnoreCase will disagree on the same input.

Make the choice explicit at every call site by passing StringComparison. For domain-specific keys, consider centralizing the comparison in a single helper or a custom comparer so the rule is defined once. This is a maintainability concern more than a correctness concern: the bug only appears when the environment changes, which is why it tends to surface in production rather than in tests running on a developer machine.

c# stringcomparison: Practical Usage and Code Examples | RYUSLOG DEV