Skip to content
Browse tools

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. πŸ‘‡

πŸ”
Read it
Inspect & search

Length, Contains, IndexOf, ranges, null checks.

βš–οΈ
Decide
Compare

StringComparison β€” the one setting that prevents most string bugs.

πŸ› οΈ
Produce
Transform & build

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.

MemberWhat it doesExampleResult
LengthNumber of UTF-16 code units"Hello".Length5
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.IsNullOrEmptytrue for null or ""string.IsNullOrEmpty("")true
string.IsNullOrWhiteSpaceAlso true for spaces, tabs, newlinesstring.IsNullOrWhiteSpace(" \t")true
string.EmptyThe empty string, same as ""string.Empty.Length0
char.IsDigit / IsLetterClassify a single character"A7".All(char.IsLetterOrDigit)true
Enumerable.CountCount matching chars (LINQ)"banana".Count(c => c == 'a')3

πŸ›‘οΈ Guarding inputs in .NET 8

β–Έ C#
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.

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?

StringComparisonBehaviourUse for
OrdinalByte-for-byte on UTF-16 code units. Fastest, predictable.Keys, ids, file extensions, HTTP headers, JSON property names, tokens.
OrdinalIgnoreCaseOrdinal, but a equals A. Culture-independent.Your default for case-insensitive matching of anything technical β€” emails, usernames, file paths on Windows.
CurrentCultureLinguistic rules of the current user's culture.Sorting a list shown to that user.
CurrentCultureIgnoreCaseSame, 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.
MethodExampleResult
=="abc" == "abc"true
string.Equalsstring.Equals("a", "A", StringComparison.OrdinalIgnoreCase)true
a.Equals(b, …)"Admin".Equals("ADMIN", StringComparison.OrdinalIgnoreCase)true
string.Comparestring.Compare("apple", "Banana", StringComparison.OrdinalIgnoreCase)< 0
CompareTo"abc".CompareTo("abd")< 0 (culture-sensitive)
string.CompareOrdinalstring.CompareOrdinal("a", "B")> 0 ('a' is 97, 'B' is 66)

πŸ—‚οΈ Case-insensitive collections

Don't lowercase keys by hand β€” give the collection a comparer:

β–Έ C#
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.

MethodWhat it doesExampleResult
ToUpper / ToLowerCase, using current culture"Hello".ToUpper()"HELLO"
ToUpperInvariant / ToLowerInvariantCase, culture-independent"Order-ID".ToLowerInvariant()"order-id"
TrimRemoves leading/trailing whitespace" hi ".Trim()"hi"
Trim(chars)Removes specific chars from both ends"--hi--".Trim('-')"hi"
TrimStart / TrimEndOne end only"000123".TrimStart('0')"123"
PadLeft / PadRightPad to a total width"42".PadLeft(5, '0')"00042"
ReplaceReplaces 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"
RemoveDeletes from an index"Hello".Remove(1, 3)"Ho"
InsertInserts at an index"Hello".Insert(5, "!")"Hello!"
ReplaceLineEndingsNormalises \r\n, \r, \n (.NET 6+)"a\r\nb".ReplaceLineEndings("\n")"a\nb"
NormalizeUnicode normalization (NFC by default)"cafe\u0301".Normalize().Length4
Regex.ReplacePattern-based replaceRegex.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#

β–Έ 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

MethodExampleResult
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 linestext.Split('\n').Select(l => l.TrimEnd('\r'))lines, CRLF-safe
string.Joinstring.Join(", ", new[] { "a", "b", "c" })"a, b, c"
string.Join(char, …)string.Join('|', list)"x|y|z"
string.Concatstring.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.

FormatExampleResult (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"
Classicstring.Format("{0} of {1}", 3, 10)"3 of 10"
β–Έ C# β€” verbatim, raw and invariant strings
// 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.

MemberWhat 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 / ReplaceEdit the buffer in place
Length = 0 / Clear()Reuse the builder
ToString()Produce the final string
β–Έ C#
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:

β–Έ C#
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

  1. 🧊 Forgetting strings are immutable

    input.Trim(); discards its result. Assign it: input = input.Trim();. The same applies to Replace, ToUpper, PadLeft β€” every transform.

  2. πŸ‡ΉπŸ‡· Culture-sensitive case and comparison

    Under the Turkish culture, "file".ToUpper() returns "FΔ°LE" (dotted capital Δ°), so a check like ext.ToUpper() == "FILE" fails on a Turkish server. Use ToUpperInvariant() or, better, StringComparison.OrdinalIgnoreCase for anything that isn't displayed to a person.

  3. βš–οΈ == vs Equals vs object

    On two string-typed values, == compares contents (ordinal, case-sensitive) and handles null. But if either side is typed as object, == compares references: object a = "x"; object b = new string('x', 1); gives a == b β†’ false. And a.Equals(b) throws if a is null β€” static string.Equals(a, b, comparison) doesn't.

  4. πŸ”Ž Default comparisons differ between methods

    IndexOf(string), StartsWith(string), EndsWith(string), CompareTo and string.Compare(a, b) are culture-sensitive; ==, Equals, Contains and Replace are ordinal. Pass a StringComparison every time and you never have to remember which is which.

  5. πŸ” Concatenation in loops

    result += line inside a loop is quadratic in the total length. Use StringBuilder, or collect into a list and call string.Join once.

  6. πŸ˜€ Reversing and truncating by char

    new string(s.Reverse().ToArray()) and s[..10] operate on UTF-16 units and can split surrogate pairs, producing invalid text. For user-facing text, work with StringInfo text elements or s.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.

πŸ“Œ Written against .NET 8 and C# 12. Method availability notes (.NET 5, 6, 7, 8) mark when an API arrived; everything listed also exists in later versions. The official reference is the System.String documentation on Microsoft Learn.

C# string methods
10 min read