Updated on

To find a substring while ignoring case in C#, pass a StringComparison value: source.Contains(needle, StringComparison.OrdinalIgnoreCase). Without that second argument, Contains() is case-sensitive, and there is no other switch that changes it.

To download the source code for this article, you can visit our GitHub repository.

IndexOf() does the same job and also tells us where the match starts. Regular expressions and LINQ can be pressed into service too, and both come with a catch worth knowing about before we reach for them.

How Do We Make String.Contains() Ignore Case in C#?

String.Contains() ignores case when we pass a StringComparison value as its second argument, and StringComparison.OrdinalIgnoreCase is the one to reach for by default.

Without that argument the method is case-sensitive, and no setting changes it. Microsoft’s documentation for Contains(String) is blunt: “This method performs an ordinal (case-sensitive and culture-insensitive) comparison.”

OrdinalIgnoreCase compares the two strings by their raw Unicode code units and folds upper and lower case together. It never consults the current culture, so it returns the same answer on every machine, and it allocates nothing.

One catch decides whether this overload is available to us at all. Contains(string, StringComparison) arrived with .NET Core 2.1 and does not exist on .NET Framework, which is why older code reaches for IndexOf() or for uppercasing both strings first.

On a modern target, this is the whole answer. The rest of the article covers the cases where it is not enough.

Use StringComparison.OrdinalIgnoreCase

The StringComparison parameter accepts one of the StringComparison enumeration values, with six distinct enum values. In this discussion, we’ll focus on two enumeration values within the StringComparison parameter: Ordinal and OrdinalIgnoreCase.

The Ordinal enumeration leverages ordinal or binary sort rules to compare strings, examining the individual unicode character codes. C# compares characters based on their fundamental binary representation in this approach. As a result, it treats lowercase and uppercase letters as distinct entities, preserving their case-sensitive differentiation. We must keep in mind that if no StringComparison enum is provided to Contains(), StringComparison.Ordinal is employed as the default.

On the contrary, OrdinalIgnoreCase, as implied by its name, considers lowercase and uppercase letters as equivalent during comparison, enabling a case-insensitive assessment. We utilize this enum throughout the article.

Let’s take a look at an example that shows the concept in action:

var sourceString = "Code Maze";
var substringToSearch = "maze";

sourceString.Contains(substringToSearch, StringComparison.OrdinalIgnoreCase); // true

As we pass the StringComparison.OrdinalIgnoreCase enum as an argument to the Contains() method, it ignores case sensitivity. Consequently, this method returns true as the "maze" substring is present in the sourceString variable.

Use String.ToUpperInvariant() Method

This is the fallback for .NET Framework, where Contains(string, StringComparison) does not exist. On a modern target it is about 5 times slower than IndexOf() and allocates 272 B per call, so Contains() or IndexOf() with OrdinalIgnoreCase is the better call. We use the String.ToUpperInvariant() method as it transforms a string to uppercase while ensuring consistency across different cultures and locales.

It is advisable not to use the String.ToLowerInvariant() method because a small group of characters can’t make a round trip when transformed into lowercase. Take a look at normalizing strings to uppercase to learn more about round trips.

Let’s look at this in action:

var sourceString = "Code Maze";
var substringToSearch = "maze";

sourceString.ToUpperInvariant().Contains(substringToSearch.ToUpperInvariant()); // true

Firstly, we transform the values in the sourceString and the substringToSearch variables into uppercase letters. Then the String.Contains() method checks whether the sourceString contains the substringToSearch. The method returns true when it finds the substring.

Note that ToUpper() and ToLower() without Invariant follow the current culture, and under a Turkish culture they turn a search for "file" inside "FILE" into a miss.

Which StringComparison Value Should We Pass?

StringComparison has six values, and they answer two questions at once: should case matter, and should culture matter.

Ordinal and OrdinalIgnoreCase compare raw Unicode code units. They are the fastest, they allocate nothing, and they give the same answer on every machine. They are the right default for identifiers, file paths, URLs, dictionary keys and anything else a user did not type as prose.

CurrentCulture and CurrentCultureIgnoreCase follow whatever culture the machine is set to. Reserve them for text a person reads, where the result should match how their language sorts and folds letters.

InvariantCulture and InvariantCultureIgnoreCase apply linguistic rules with a fixed culture, so they are stable across machines but about 10 times slower than ordinal: on our benchmark string, 134.48 ns against 12.72 ns.

The choice is not cosmetic. Under a Turkish culture, "FILE".Contains("file", StringComparison.CurrentCultureIgnoreCase) returns false, because Turkish treats dotted and dotless i as different letters.

ValueCompares usingCaseReach for it when
Ordinalraw Unicode code unitssensitiveidentifiers, file paths, URLs, dictionary keys, protocol tokens
OrdinalIgnoreCaseraw Unicode code unitsignoredthe same, when case must not matter
CurrentCulturethe current culture's linguistic rulessensitivetext a person reads, sorted or matched the way their language does it
CurrentCultureIgnoreCasethe current culture's linguistic rulesignoredthe same, when case must not matter
InvariantCulturea fixed, culture-independent linguistic rulesetsensitivelinguistic comparison that must give the same answer on every machine
InvariantCultureIgnoreCasea fixed, culture-independent linguistic rulesetignoredthe same, when case must not matter

The default differs by method, which is the part that catches people out. Contains(string) is ordinal, while IndexOf(string) is culture-sensitive.

How Do We Use String.IndexOf() for a Case-Insensitive Search?

String.IndexOf() returns the zero-based position of the first match and -1 when there is none, so a case-insensitive containment check is source.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0.

The position is the reason to prefer it over Contains(). When we need to know where the match starts, IndexOf() hands us that in the same call instead of a second search.

There is a trap here, and it runs the opposite way to the one in Contains(). IndexOf(string) with no StringComparison performs a culture-sensitive search using the current culture, not an ordinal one. The two default overloads genuinely disagree: given a string holding a soft hyphen between Code and Maze, Contains("CodeMaze") reports no match while IndexOf("CodeMaze") reports one at index 0.

That is not an edge case: it is two methods on the same type, called the same way, returning different answers.

This is what the CA1310 analyzer exists to catch, though it is not on by default. Passing the comparison explicitly removes the question.

Let’s proceed with an illustrative example:

var sourceString = "Code Maze";
var substringToSearch = "maze";

sourceString.IndexOf(substringToSearch, StringComparison.OrdinalIgnoreCase); // 5

We pass the StringComparison.OrdinalIgnoreCase enum to ignore case sensitivity. As a result, the IndexOf() method returns 5, which represents the starting index of the substring in the source string. A positive value from the method signifies the successful discovery of the substring. When one position is not enough, we have a separate article on how to find every position of a substring, not just the first.

How Do We Search a Substring With a Case-Insensitive Regex?

Using the Regex.IsMatch() method, we determine whether a particular pattern exists within a specified input string. This method provides various overloads, and we utilize the IsMatch(String, String, RegexOptions) overload to locate the substring in this particular scenario.

The RegexOptions parameter opens up a world of possibilities with its 11 different enums. Some of the ones we frequently come across include IgnoreCase, CultureInvariant and IgnorePatternWhitespace. It’s worth noting that if we don’t specify any option, the RegexOptions parameter defaults to None.

Let’s look at this in action:

var sourceString = "Code Maze";
var substringToSearch = "maze";

Regex.IsMatch(sourceString,
    Regex.Escape(substringToSearch),
    RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); // true

Here, we employ the RegexOptions.IgnoreCase argument to ignore case sensitivity. The Regex.IsMatch() method returns true as the substring has been found.

Regex.Escape() is doing real work here. Without it, a search term such as "c.de" or "z*" is a pattern rather than literal text, so it matches strings that do not contain it, and a term containing an unmatched bracket throws a RegexParseException instead of returning false.

RegexOptions.CultureInvariant keeps IgnoreCase off the current culture’s casing rules, which is the same Turkish-i problem the table above describes.

If you’d like to learn more about regular expressions, check out our great article Introduction to Regular Expressions in C#.

Does LINQ With String.Equals() Find a Substring?

No, and the difference matters. Splitting a string and comparing each piece with String.Equals() matches whole separator-delimited words, not substrings.

"Code Maze".Split(' ').Any(w => w.Equals("aze", StringComparison.OrdinalIgnoreCase)) returns false. Every other method on this page returns true for the same two inputs, because "aze" genuinely is inside "Code Maze".

So this is not a fifth way to do the same job. It answers a neighbouring question: is one of these words exactly equal to my search term, ignoring case.

That question is a real one, and worth knowing the shape of. Matching a tag, a command name, or one field against a list of accepted words is exactly this, and StringComparer.OrdinalIgnoreCase reads better than an inline comparison when the list grows.

It is also the most expensive option on the page, because Split() allocates an array before any comparison happens.

For plain containment, stay with Contains() or IndexOf().

Let’s look at an example that utilizes the Equals(String, StringComparison) overload:

var sourceString = "Code Maze";
var substringToSearch = "maze";
var separator = ' ';

sourceString
    .Split(separator)
    .Any(word => word.Equals(substringToSearch, StringComparison.OrdinalIgnoreCase)); // true

Initially, we use the Split() method to divide the sourceString into a string array containing substrings delimited by a whitespace character, which we pass on as an argument to the method. It is worth knowing how String.Split() behaves and which overload to pick before relying on it here. Then, we use the Any() method to determine whether any string within the array satisfies the condition of the Equals() method.

We provide the StringComparison.OrdinalIgnoreCase enum as an argument to the Equals() method to disregard case sensitivity. If there is a string within the array that matches the substringToSearch, it will return true and, in turn, the Any() method will return true. While we are here, it is worth being clear about the difference between == and Equals(), because the two are not interchangeable for strings.

Which Case-Insensitive Search Is Fastest in C#?

Now that we’ve learned different techniques to perform case-insensitive substring search, let’s analyze the benchmark result to determine which performance shines the most.

To test our methods, we’re using a longer string "A quick brown fox jumps over the lazy dog. The lazy dog barks loudly. The brown fox runs away quickly." and the substring to search is "loudly".

With this, let’s take a look at the results:

BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update)
AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 10.0.302
  [Host]     : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3
  DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3

| Method                 | Mean      | Error    | StdDev    | Gen0   | Allocated |
|----------------------- |----------:|---------:|----------:|-------:|----------:|
| StringIndexOf          |  12.82 ns | 0.402 ns |  1.174 ns |      - |         - |
| StringContains         |  15.22 ns | 0.337 ns |  0.815 ns |      - |         - |
| StringToUpperInvariant |  70.57 ns | 2.033 ns |  5.962 ns | 0.0324 |     272 B |
| RegexIsMatch           | 106.80 ns | 2.014 ns |  2.155 ns |      - |         - |
| LinqStringEquals       | 296.72 ns | 6.040 ns | 17.522 ns | 0.1020 |     856 B |

String.IndexOf() comes out roughly 23 times faster than the LINQ approach, and the two ordinal calls are the only ones that allocate nothing at all. Bear in mind that the LINQ row is answering a different question, so its cost is not a like-for-like comparison.

We produced these figures with the same approach we use for benchmarking C# and ASP.NET Core projects, and the same benchmark treatment applied to removing characters from a string runs the exercise on a different string operation.

Conclusion

Through this article, we’ve gained insights into different approaches for performing case-insensitive substring search in C#. Additionally, we’ve gained an understanding of the performance characteristics of each approach. This newfound knowledge will undoubtedly improve our proficiency in conducting substring searches in the future.

Tested with .NET 10.0.10 and BenchmarkDotNet 0.15.8.