Practical reference Β· C# & .NET 8
Every string method
you'll actually use,
with the result.
.NET's string type has well over a hundred members. You need about forty of them, plus a clear rule for comparisons. This page groups them by job β inspect, search, compare, transform, split, format, build β and shows each one with a real input and the exact output. π
Length, Contains, IndexOf, ranges, null checks.
StringComparison β the one setting that prevents most string bugs.
Trim, replace, split, join, interpolation and StringBuilder.
The one fact behind everything below: strings in .NET are immutable. ToUpper(), Trim() and Replace() never change the original β they return a new string. name.Trim(); on its own line does nothing useful; name = name.Trim(); does.
02 Β· Look before you touch
π Inspect: length, characters and emptiness
The cheapest operations β and the ones that stop a NullReferenceException before it happens.
| Member | What it does | Example | Result |
|---|---|---|---|
| Length | Number of UTF-16 code units | "Hello".Length | 5 |
| s[i] | The char at an index | "Hello"[1] | 'e' |
| s[^1] | Index from the end (C# 8+) | "Hello"[^1] | 'o' |
| s[a..b] | Range slice β start inclusive, end exclusive | "Hello"[1..3] | "el" |
| string.IsNullOrEmpty | true for null or "" | string.IsNullOrEmpty("") | true |
| string.IsNullOrWhiteSpace | Also true for spaces, tabs, newlines | string.IsNullOrWhiteSpace(" \t") | true |
| string.Empty | The empty string, same as "" | string.Empty.Length | 0 |
| char.IsDigit / IsLetter | Classify a single character | "A7".All(char.IsLetterOrDigit) | true |
| Enumerable.Count | Count matching chars (LINQ) | "banana".Count(c => c == 'a') | 3 |
π‘οΈ Guarding inputs in .NET 8
public Customer Create(string name, string? nickname) { ArgumentException.ThrowIfNullOrWhiteSpace(name); // .NET 8 var display = nickname?.Trim() ?? name.Trim(); // null-safe return new Customer(display); }
π‘ ArgumentException.ThrowIfNullOrEmpty arrived in .NET 7 and ThrowIfNullOrWhiteSpace in .NET 8. They throw ArgumentNullException for null and ArgumentException otherwise, with the parameter name filled in for you.
Length is not "characters a person sees". "π".Length is 2 because the emoji is a surrogate pair. For user-visible counts use new StringInfo(s).LengthInTextElements (grapheme-aware since .NET 5). See What are string utilities? for why.
03 Β· Find things
π Search: contains, starts with, index of
| Method | What it does | Example | Result |
|---|---|---|---|
| Contains | Substring or char present? | "Hello".Contains("ell") | true |
| Contains(β¦, comparison) | Same, with explicit rules | "Hello".Contains("ELL", StringComparison.OrdinalIgnoreCase) | true |
| StartsWith | Prefix check | "report.pdf".StartsWith("rep") | true |
| EndsWith | Suffix check | "report.pdf".EndsWith(".PDF", StringComparison.OrdinalIgnoreCase) | true |
| IndexOf | First position, or -1 | "hello world".IndexOf('o') | 4 |
| IndexOf(value, start) | Search from a position | "hello world".IndexOf('o', 5) | 7 |
| LastIndexOf | Last position, or -1 | "report.final.pdf".LastIndexOf('.') | 12 |
| IndexOfAny | First of several chars | "a+b-c".IndexOfAny(new[] { '+', '-' }) | 1 |
| Regex.IsMatch | Pattern search | Regex.IsMatch("ORD-2026", @"^ORD-\d{4}$") | true |
The defaults are inconsistent β this is the biggest trap in the API. Contains(string) and IndexOf(char) are ordinal. But IndexOf(string), StartsWith(string) and EndsWith(string) are culture-sensitive by default. On .NET 5+ (which uses ICU for globalization), "Hello\r\nWorld".IndexOf("\n") returns -1, because \r\n is treated as one unit linguistically. Pass StringComparison.Ordinal and it returns 6.
The fix is a habit, not a memory test: whenever a string method accepts a StringComparison, pass one. The .NET analyzers (CA1307, CA1309, CA1310) will flag the calls that don't.
04 Β· Equal or not?
βοΈ Compare: StringComparison explained
Two questions decide how strings should be compared: does case matter? and is this text for humans or for machines?
| StringComparison | Behaviour | Use for |
|---|---|---|
| Ordinal | Byte-for-byte on UTF-16 code units. Fastest, predictable. | Keys, ids, file extensions, HTTP headers, JSON property names, tokens. |
| OrdinalIgnoreCase | Ordinal, but a equals A. Culture-independent. | Your default for case-insensitive matching of anything technical β emails, usernames, file paths on Windows. |
| CurrentCulture | Linguistic rules of the current user's culture. | Sorting a list shown to that user. |
| CurrentCultureIgnoreCase | Same, ignoring case. | User-facing search boxes. |
| InvariantCulture(IgnoreCase) | Linguistic, but with fixed culture-neutral rules. | Rarely the right answer; persisted sort orders that must not vary by machine. |
| Method | Example | Result |
|---|---|---|
| == | "abc" == "abc" | true |
| string.Equals | string.Equals("a", "A", StringComparison.OrdinalIgnoreCase) | true |
| a.Equals(b, β¦) | "Admin".Equals("ADMIN", StringComparison.OrdinalIgnoreCase) | true |
| string.Compare | string.Compare("apple", "Banana", StringComparison.OrdinalIgnoreCase) | < 0 |
| CompareTo | "abc".CompareTo("abd") | < 0 (culture-sensitive) |
| string.CompareOrdinal | string.CompareOrdinal("a", "B") | > 0 ('a' is 97, 'B' is 66) |
ποΈ Case-insensitive collections
Don't lowercase keys by hand β give the collection a comparer:
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["Content-Type"] = "application/json" }; headers.ContainsKey("content-type"); // true var tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CSharp", "csharp" }; tags.Count; // 1 var distinct = names.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
Stop writing a.ToLower() == b.ToLower(). It allocates two new strings on every comparison and uses the current culture. string.Equals(a, b, StringComparison.OrdinalIgnoreCase) allocates nothing, is null-safe, and says exactly what you mean.
05 Β· Change it
π οΈ Transform: case, trim, pad, replace, cut
Every method here returns a new string.
| Method | What it does | Example | Result |
|---|---|---|---|
| ToUpper / ToLower | Case, using current culture | "Hello".ToUpper() | "HELLO" |
| ToUpperInvariant / ToLowerInvariant | Case, culture-independent | "Order-ID".ToLowerInvariant() | "order-id" |
| Trim | Removes leading/trailing whitespace | " hi ".Trim() | "hi" |
| Trim(chars) | Removes specific chars from both ends | "--hi--".Trim('-') | "hi" |
| TrimStart / TrimEnd | One end only | "000123".TrimStart('0') | "123" |
| PadLeft / PadRight | Pad to a total width | "42".PadLeft(5, '0') | "00042" |
| Replace | Replaces every occurrence (ordinal) | "a-b-c".Replace("-", "+") | "a+b+c" |
| Replace(β¦, comparison) | Case-insensitive replace | "Hi HI hi".Replace("hi", "yo", StringComparison.OrdinalIgnoreCase) | "yo yo yo" |
| Substring(start) | From an index to the end | "Hello World".Substring(6) | "World" |
| Substring(start, length) | Note: length, not end index | "Hello World".Substring(0, 5) | "Hello" |
| Remove | Deletes from an index | "Hello".Remove(1, 3) | "Ho" |
| Insert | Inserts at an index | "Hello".Insert(5, "!") | "Hello!" |
| ReplaceLineEndings | Normalises \r\n, \r, \n (.NET 6+) | "a\r\nb".ReplaceLineEndings("\n") | "a\nb" |
| Normalize | Unicode normalization (NFC by default) | "cafe\u0301".Normalize().Length | 4 |
| Regex.Replace | Pattern-based replace | Regex.Replace("a b c", @"\s+", " ") | "a b c" |
Substring throws instead of clamping. "Hi".Substring(0, 5) throws ArgumentOutOfRangeException. For "first N characters, or fewer", write s[..Math.Min(n, s.Length)] β and remember that can still split an emoji's surrogate pair.
π A simple slug in C#
static string Slugify(string input) { // split accented letters into letter + mark, then drop the marks var decomposed = input.Normalize(NormalizationForm.FormD); var sb = new StringBuilder(decomposed.Length); foreach (var c in decomposed) if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) sb.Append(c); var ascii = sb.ToString().ToLowerInvariant(); ascii = Regex.Replace(ascii, @"[^a-z0-9]+", "-"); return ascii.Trim('-'); } Slugify("Crème Brûlée: 10 Tips!"); // "creme-brulee-10-tips"
π‘ Compare the output against the Slug Generator while you tune the rules.
06 Β· Many from one, one from many
πͺ Split, join and concatenate
| Method | Example | Result |
|---|---|---|
| Split(char) | "a,b,,c".Split(',') | ["a", "b", "", "c"] |
| RemoveEmptyEntries | "a,b,,c".Split(',', StringSplitOptions.RemoveEmptyEntries) | ["a", "b", "c"] |
| TrimEntries (.NET 5+) | " a , b ".Split(',', StringSplitOptions.TrimEntries) | ["a", "b"] |
| Both options | "a, ,b".Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) | ["a", "b"] |
| Split with count | "key=a=b".Split('=', 2) | ["key", "a=b"] |
| Split(string) | "a::b".Split("::") | ["a", "b"] |
| Split lines | text.Split('\n').Select(l => l.TrimEnd('\r')) | lines, CRLF-safe |
| string.Join | string.Join(", ", new[] { "a", "b", "c" }) | "a, b, c" |
| string.Join(char, β¦) | string.Join('|', list) | "x|y|z" |
| string.Concat | string.Concat("a", "b", "c") | "abc" |
The count argument means "at most this many pieces", and the last piece keeps the rest. That's perfect for key=value lines where the value may contain =. JavaScript's split limit behaves differently β it discards the rest β which catches people moving between the two.
07 Β· Build readable output
π§Ύ Formatting, interpolation and raw strings
Interpolated strings ($"...") are the modern default; since C# 10 they compile to efficient handlers rather than string.Format calls.
| Format | Example | Result (en-US) |
|---|---|---|
| Thousands + decimals | $"{1234.5:N2}" | "1,234.50" |
| Fixed decimals | $"{3.14159:F2}" | "3.14" |
| Zero-padded integer | $"{42:D5}" | "00042" |
| Hex | $"{255:X4}" | "00FF" |
| Custom date | $"{new DateTime(2026, 10, 1):yyyy-MM-dd}" | "2026-10-01" |
| Right-align | $"[{"ab",5}]" | "[ ab]" |
| Left-align | $"[{"ab",-5}]" | "[ab ]" |
| Literal braces | $"{{id}} = {7}" | "{id} = 7" |
| Classic | string.Format("{0} of {1}", 3, 10) | "3 of 10" |
// verbatim: backslashes are literal, "" is a quote var path = @"C:\logs\app.txt"; // raw string literal (C# 11): no escaping at all, indentation trimmed var json = """ { "name": "Ana", "tags": ["a", "b"] } """; // interpolated raw string: $$ means {{ }} marks a hole, single { } stay literal var body = $$""" { "id": {{orderId}} } """; // culture-independent output for logs, files and APIs var line = FormattableString.Invariant($"{price:F2}"); var same = string.Create(CultureInfo.InvariantCulture, $"{price:F2}"); // .NET 6+
Formatting uses the current culture. $"{1234.5:N2}" is 1,234.50 in en-US and 1.234,50 in de-DE. If the string is going into a CSV, a log line, a URL or JSON, format with CultureInfo.InvariantCulture β a server whose culture changes can otherwise silently corrupt data.
08 Β· Performance
ποΈ StringBuilder and spans
Because strings are immutable, s += x in a loop copies the whole string every time β fine for five iterations, painful for five thousand. StringBuilder keeps one growable buffer.
| Member | What it does |
|---|---|
| Append(x) | Adds text (or any value's ToString()) to the end |
| AppendLine(x) | Adds text plus Environment.NewLine |
| Append($"β¦") | Interpolation writes straight into the builder (.NET 6+), no temporary string |
| AppendJoin(sep, items) | Like string.Join, but into the builder |
| Insert / Remove / Replace | Edit the buffer in place |
| Length = 0 / Clear() | Reuse the builder |
| ToString() | Produce the final string |
var sb = new StringBuilder(); sb.AppendLine("id,name"); foreach (var c in customers) sb.Append(c.Id).Append(',').AppendLine(c.Name); File.WriteAllText("customers.csv", sb.ToString());
πͺΆ Spans, briefly
ReadOnlySpan<char> is a view over part of a string without copying it. Most parsing APIs accept spans, so you can slice and parse without allocating substrings:
ReadOnlySpan<char> date = "2026-10-01"; int year = int.Parse(date[..4]); // 2026 β no substring allocated int month = int.Parse(date[5..7]); // 10 // .NET 8: SearchValues precomputes a fast lookup for IndexOfAny static readonly SearchValues<char> Separators = SearchValues.Create(",;|"); int at = line.AsSpan().IndexOfAny(Separators);
π‘ Reach for spans in hot paths β parsers, per-request middleware, tight loops. In ordinary business code, clear string methods are the better trade.
09 Β· Read before you ship
πͺ€ Gotchas that cause real bugs
-
π§ Forgetting strings are immutable
input.Trim();discards its result. Assign it:input = input.Trim();. The same applies toReplace,ToUpper,PadLeftβ every transform. -
πΉπ· Culture-sensitive case and comparison
Under the Turkish culture,
"file".ToUpper()returns"FΔ°LE"(dotted capital Δ°), so a check likeext.ToUpper() == "FILE"fails on a Turkish server. UseToUpperInvariant()or, better,StringComparison.OrdinalIgnoreCasefor anything that isn't displayed to a person. -
βοΈ
==vsEqualsvsobjectOn two
string-typed values,==compares contents (ordinal, case-sensitive) and handlesnull. But if either side is typed asobject,==compares references:object a = "x"; object b = new string('x', 1);givesa == bβ false. Anda.Equals(b)throws ifais null β staticstring.Equals(a, b, comparison)doesn't. -
π Default comparisons differ between methods
IndexOf(string),StartsWith(string),EndsWith(string),CompareToandstring.Compare(a, b)are culture-sensitive;==,Equals,ContainsandReplaceare ordinal. Pass aStringComparisonevery time and you never have to remember which is which. -
π Concatenation in loops
result += lineinside a loop is quadratic in the total length. UseStringBuilder, or collect into a list and callstring.Joinonce. -
π Reversing and truncating by
charnew string(s.Reverse().ToArray())ands[..10]operate on UTF-16 units and can split surrogate pairs, producing invalid text. For user-facing text, work withStringInfotext elements ors.EnumerateRunes().
Three rules cover most of it: assign the result, pass a StringComparison, and format machine-bound text with InvariantCulture. Try the transformations interactively in String Tools, then compare with the JavaScript string methods if you work across both.