Use text.Contains("value"). It returns a bool, and it is case-sensitive. To ignore case, pass a comparison: text.Contains("value", StringComparison.OrdinalIgnoreCase) (.NET Core 2.1 and later). On .NET Framework, which lacks that overload, write text.IndexOf("value", StringComparison.OrdinalIgnoreCase) >= 0.
string.Contains is the obvious answer, and for an exact, case-sensitive check it is the right one. The questions start when case shouldn't matter: ToLower() on both sides looks fine and breaks on a Turkish machine, and the StringComparison enum offers six options whose names don't say which one you want. The sections below cover Contains, ignoring case, Ordinal vs InvariantCulture, finding the position with IndexOf, checking for any of several words, and whole-word matches with Regex. Every example runs on this page: hit Run, then edit the code and run it again.
1string.ContainsRecommended
text.Contains(value) returns true when value appears anywhere in text. It compares characters exactly (an ordinal comparison), so it is case-sensitive and doesn't depend on the machine's culture. There is also a Contains(char) overload for a single character. Two edge cases are worth knowing before they surprise you: the empty string is contained in every string, and a null on either side throws.
Output
Prints True, False, then False for "Failed" because the log says failed. The ':' check prints True, the if prints Found an error in the build log, and Contains("") is True. A null argument throws ArgumentNullException: Value cannot be null. (Parameter 'value'). A null receiver would throw NullReferenceException, so log?.Contains("error") ?? false turns "no log" into False (more on that in checking for null). Note that Contains on a List<string> is a different method: it asks whether the list has an element equal to the value, not whether any element contains it.
2Case-insensitive Contains with StringComparison
Since .NET Core 2.1, Contains takes a StringComparison. StringComparison.OrdinalIgnoreCase is the one you want for keywords, file names, URLs, identifiers and most search boxes: it ignores case without consulting any culture. .NET Framework has no such overload, and the portable form is IndexOf(value, comparison) >= 0, which every .NET version supports. Wrap it in an extension method if you write it often. What you should not do is lowercase both strings.
Output
Prints False for the plain call, then True five times: both OrdinalIgnoreCase calls, the IndexOf form, the extension method and, here, ToLower(). The last three lines show why the ToLower() version is a bug anyway. Lowercased with Turkish rules, CONFIG.INI becomes confıg.ını with a dotless ı, so it no longer contains config (False). Plain ToLower() uses the current culture, so the same code passes on your machine and fails on a server set to tr-TR. OrdinalIgnoreCase printed True because it doesn't use culture rules at all. It also skips the two temporary strings that ToLower() allocates.
3Ordinal vs InvariantCulture vs CurrentCulture
StringComparison has three families, each with an IgnoreCase twin. Ordinal compares the UTF-16 code units, so two strings match only if they hold the same characters. InvariantCulture and CurrentCulture compare linguistically: text that a reader would see as the same can match even when the characters differ, and invisible characters such as a soft hyphen are ignored. Invariant uses fixed rules. Current uses the culture of the thread, so its result can change from one machine to the next.
Output
The grid shows where the families differ. Case alone: only the IgnoreCase columns say True. A precomposed é against e plus a combining accent, and a word with a hidden soft hyphen: False under Ordinal, True under Invariant, because the culture rules treat them as the same text. None of the four modes matches Straße with STRASSE. Then the default trap: Contains("coop") is ordinal and prints False, but IndexOf("coop") with no comparison uses the current culture and prints 0, a match. Pass StringComparison.Ordinal and it is -1. The Turkish lines print -1 and 0: under tr-TR rules, I pairs with ı and İ pairs with i. This sandbox runs with the invariant culture, so IndexOf's default could differ on your machine. Pick Ordinal or OrdinalIgnoreCase for anything a program reads (keys, paths, protocol text) and a culture-aware mode only for text a person wrote and will read.
4IndexOf, StartsWith and EndsWith
When you need where the substring is, use IndexOf. It returns the zero-based position of the first match, or -1 when there is none. LastIndexOf searches from the end, and both take a start index for searching on from an earlier match. When the substring must be at one end, StartsWith and EndsWith say so directly. The string overloads of these four methods use the current culture by default, so pass a StringComparison every time. Code analysis rule CA1310 flags the calls that don't.
Output
Prints 9 and 13 for the first and last app, -1 for tmp, and 4 for the next / after index 1. The range path[(slash + 1)..] (C# 8+) cuts out app.log. StartsWith and the case-insensitive EndsWith both print True. The helper finds "the" at [0, 15, 28], count 3. Because it restarts the search one character after each match, overlapping matches count: aa occurs 3 times in aaaa. The Split trick counts non-overlapping matches instead: 4 for at (cat, sat, mat, hat), but only 2 for aa in aaaa. The char overloads, like IndexOf('/'), are always ordinal.
5Check for any of several substrings
To test a string against a list of words, combine LINQ with Contains: Any for "at least one", All for "every one", and FirstOrDefault to learn which word matched. That is fine for a handful of words. For a long list searched many times, .NET 9 added SearchValues<string>: you build it once from the words and it scans the text for all of them in a single pass. For single characters, IndexOfAny(char[]) works everywhere and SearchValues<char> (.NET 8+) is the faster modern form.
Output
Prints True (the comment has SPAM), True (it has both post and link) and spam, the first banned word that matched. The SearchValues lines print True and 20: SPAM, the position and the text as it appears in the comment. SearchValues<string> accepts only Ordinal and OrdinalIgnoreCase; any other comparison throws an ArgumentException. In the file name, IndexOfAny finds the : at 6, and SearchValues<char> prints True for that name and False for report-2026.txt. In real code keep a SearchValues in a static readonly field: building it is the expensive part.
6Whole words and patterns with Regex
Contains("cat") also matches category and concatenated. When you mean the whole word, or the needle is a pattern rather than a fixed string, use Regex.IsMatch from System.Text.RegularExpressions. \b marks a word boundary, RegexOptions.IgnoreCase ignores case, and Regex.Escape makes user input safe to put inside a pattern.
Output
Contains prints True because cat sits inside category, while the whole-word pattern prints False for that sentence and True for I have a Cat.. The underscore in cat_id counts as a word character, so it isn't a boundary either. Regex.Match pulls out #A-10427. Regex.Escape turned the input into \b1\+1=2\ \(maybe\), which matches (True). The unescaped input prints False, because + and the parentheses are regex syntax. The shared Regex prints True for the sentence ending in a cat and False for concatenate. On .NET 7+ in a regular project, the [GeneratedRegex] attribute builds the matcher at compile time instead of at run time.
7Which should you use?
| Method | Case | Comparison | Best for |
|---|---|---|---|
| s.Contains(x) | Sensitive | Ordinal | The default check |
| s.Contains(x, StringComparison.OrdinalIgnoreCase) | Ignored | Ordinal | Keywords, file names, search boxes |
| s.IndexOf(x, comparison) >= 0 | Your choice | Your choice | .NET Framework, or when you need the position |
| s.Contains(x, StringComparison.InvariantCulture) | Your choice | Linguistic | Text people wrote: accents, hidden characters |
| s.ToLower().Contains(x.ToLower()) | Ignored, per culture | Culture-dependent | Avoid: breaks on tr-TR, allocates |
| s.StartsWith(x, comparison) | Your choice | Your choice | Prefixes and suffixes (EndsWith) |
| words.Any(w => s.Contains(w, …)) | Your choice | Your choice | A few needles |
| s.AsSpan().ContainsAny(searchValues) | Your choice | Ordinal only | Many needles, hot paths (.NET 9+) |
| Regex.IsMatch(s, @"\bword\b") | RegexOptions.IgnoreCase | Pattern | Whole words, patterns |
Frequently asked questions
How do I make string.Contains case-insensitive in C#?
Pass a StringComparison: text.Contains("value", StringComparison.OrdinalIgnoreCase). That overload exists on .NET Core 2.1 and later, including every .NET 5+ version. On .NET Framework use text.IndexOf("value", StringComparison.OrdinalIgnoreCase) >= 0, which does the same thing. Avoid text.ToLower().Contains(value.ToLower()): it allocates two strings and follows the current culture, so in Turkish "CONFIG.INI" lowercases to "confıg.ını" and no longer contains "config".
What is the difference between StringComparison.Ordinal and InvariantCulture?
Ordinal compares the characters as numbers: two strings match only if they contain the same UTF-16 code units. InvariantCulture compares linguistically with fixed, culture-neutral rules, so a precomposed "é" matches "e" plus a combining accent, and invisible characters such as a soft hyphen are ignored. Use Ordinal or OrdinalIgnoreCase for identifiers, keys, file paths and protocol text, which is most code. Use a culture-aware comparison only for text that people write and read, and CurrentCulture when the result should follow the language of the person using the program.
Is string.Contains culture-sensitive?
No. Contains(string) is ordinal and case-sensitive. That makes it different from IndexOf(string), StartsWith(string) and EndsWith(string), which use the current culture when you pass no comparison. So "co\u00ADoperate".Contains("coop") is false while "co\u00ADoperate".IndexOf("coop") is 0: the culture rules skip the soft hyphen. Passing a StringComparison to every call removes the guesswork.
How do I check if a List<string> contains a string, ignoring case?
Pass a comparer to the LINQ Contains: list.Contains("csharp", StringComparer.OrdinalIgnoreCase). Plain list.Contains("csharp") uses exact equality and returns false for "CSharp". For frequent lookups, build a new HashSet<string>(list, StringComparer.OrdinalIgnoreCase) once. To ask whether any element contains a substring instead, use list.Any(s => s.Contains(value, StringComparison.OrdinalIgnoreCase)).
Why does Contains("") return true?
Every string contains the empty string, at position 0, so "abc".Contains("") is true and "abc".IndexOf("") is 0. If an empty search term should match nothing, check for it first with string.IsNullOrEmpty(term). A null search term is different: Contains(null) throws ArgumentNullException.
How do I check for a whole word instead of a substring?
Use a regular expression with word boundaries: Regex.IsMatch(text, @"\bcat\b", RegexOptions.IgnoreCase) matches "a cat" but not "category" or "cat_id", because the underscore counts as a word character. If the word comes from user input, wrap it in Regex.Escape first so characters such as + and ( are matched literally.