Skip to main content

String Manipulation and Layout

These methods change the shape of a string: take part of it, add or remove a prefix, find text between delimiters, split it into lines, hide sensitive parts, and indent or center text. Reach for them when you would otherwise write Substring, IndexOf and length checks by hand.

All methods live in the System namespace, so you do not need a using directive. Comparisons are ordinal unless you pass a StringComparison.

At a glance​

MethodWhat it does
LeftGets the first n characters.
RightGets the last n characters.
TruncateCuts to a maximum length, keeping the start.
TruncateFromBeginningCuts to a maximum length, keeping the end.
TruncateWithPostfixCuts to a maximum length and ends with "..." or your own marker.
EnsureStartsWithAdds a character at the start if it is missing.
EnsureEndsWithAdds a character at the end if it is missing.
RemovePreFixRemoves the first matching prefix.
RemovePostFixRemoves the first matching suffix.
ReplaceFirstReplaces only the first occurrence of a value.
BetweenGets the text between two delimiters.
NthIndexOfFinds the index of the nth occurrence of a character.
SplitSplits by a whole string separator.
SplitToLinesSplits on \r\n, \r and \n on every platform.
NormalizeLineEndingsConverts every line ending to Environment.NewLine.
MaskHides the middle of a value such as a card number.
MaskEmailHides the local part of an e-mail address.
IndentPuts spaces or a prefix in front of every line.
DedentRemoves the common leading margin, or a fixed number of characters, from every line.
CenterCenters text in a field of a given width.
NormalizeWhitespaceCollapses whitespace runs to one space and trims the ends.
RepeatRepeats a string or a character n times.
TrimLinesRemoves trailing whitespace from every line.

Taking part of a string​

Left and Right​

Left(len) returns the first len characters and Right(len) returns the last len characters. Use them when you know the string is long enough, for example when you take apart a fixed-format code.

Splitting an invoice number
string invoiceNumber = "INV-2026-000123";

string series = invoiceNumber.Left(3); // → "INV"
string sequence = invoiceNumber.Right(6); // → "000123"
string none = invoiceNumber.Left(0); // → ""
warning

len must be between 0 and the string's length. A negative value, or a value longer than the string, throws ArgumentOutOfRangeException (parameter len). A null receiver throws ArgumentNullException. If the input may be shorter than len, use Truncate or TruncateFromBeginning instead.

Truncate​

Truncate(maxLength) cuts the string to at most maxLength characters and keeps the start. A string that is already short enough is returned unchanged.

Fitting a product name into a column
string productName = "Wireless Noise-Cancelling Headphones";

string shortName = productName.Truncate(8); // → "Wireless"
string unchanged = "Mouse".Truncate(8); // → "Mouse"
string empty = productName.Truncate(0); // → ""

string? missing = null;
string? stillMissing = missing.Truncate(8); // → null

A null receiver returns null. A negative maxLength throws ArgumentOutOfRangeException (parameter maxLength).

TruncateFromBeginning​

TruncateFromBeginning(maxLength) cuts the string to at most maxLength characters and keeps the end. Use it when the end carries the meaning, such as the file name at the end of a long path.

Keeping the file name of a log path
string logPath = "/var/log/orders/2026-11-04.log";

string fileName = logPath.TruncateFromBeginning(14); // → "2026-11-04.log"
string unchanged = "orders.log".TruncateFromBeginning(14); // → "orders.log"

A null receiver returns null. A negative maxLength throws ArgumentOutOfRangeException.

TruncateWithPostfix​

TruncateWithPostfix(maxLength) cuts the string and ends it with "..." when it was cut. The overload TruncateWithPostfix(maxLength, postfix) uses your own marker. The result is never longer than maxLength, because the postfix counts toward the length.

Shortening order notes for a list view
string note = "Order delayed due to weather";

string preview = note.TruncateWithPostfix(16); // → "Order delayed..."
string fits = "Paid".TruncateWithPostfix(16); // → "Paid"

string payment = "Payment received from Contoso";
string custom = payment.TruncateWithPostfix(17, "…"); // → "Payment received…"

When maxLength is not larger than the postfix, the result is the postfix cut to maxLength:

Very small limits
string status = "Cancelled";

string three = status.TruncateWithPostfix(3); // → "..."
string two = status.TruncateWithPostfix(2); // → ".."
string zero = status.TruncateWithPostfix(0); // → ""
note

A null receiver returns null. A null postfix throws ArgumentNullException (parameter postfix), and a negative maxLength throws ArgumentOutOfRangeException. Lengths count UTF-16 characters, so a cut can split a surrogate pair such as an emoji. To cut at a word boundary instead, see TruncateWords.

Prefixes and suffixes​

EnsureStartsWith and EnsureEndsWith​

EnsureStartsWith(c) adds the character c at the start if the string does not already start with it. EnsureEndsWith(c) does the same at the end. Use them to build URLs and paths without doubling a separator.

Building an API URL
string baseUrl = "https://api.example.com".EnsureEndsWith('/'); // → "https://api.example.com/"
string route = "orders".EnsureStartsWith('/'); // → "/orders"
string sameRoute = "/orders".EnsureStartsWith('/'); // → "/orders"
string root = "".EnsureEndsWith('/'); // → "/"

Both take an optional comparisonType (default StringComparison.Ordinal) that decides whether the existing character counts as a match:

Case-insensitive check
string ignoreCase = "abC".EnsureEndsWith('c', StringComparison.OrdinalIgnoreCase); // → "abC"
string ordinal = "abC".EnsureEndsWith('c'); // → "abCc"

The value to add is a single char, not a string. A null receiver throws ArgumentNullException (parameter str).

RemovePreFix and RemovePostFix​

RemovePreFix(params string[] preFixes) removes the first prefix in the list that the string starts with. RemovePostFix(params string[] postFixes) does the same for suffixes. Only one value is removed: the method tries the values in order and stops at the first match.

Turning type names into resource names
string service = "OrderService".RemovePostFix("Service", "Controller"); // → "Order"
string controller = "InvoicesController".RemovePostFix("Service", "Controller"); // → "Invoices"
string query = "GetUserById".RemovePreFix("Get", "Set"); // → "UserById"
string noMatch = "FindUser".RemovePreFix("Get", "Set"); // → "FindUser"

The order of the values matters when more than one could match:

First match wins
string suffixRemoved = "abc".RemovePostFix("c", "bc"); // → "ab"
string prefixRemoved = "abc".RemovePreFix("a", "ab"); // → "bc"

Each method has an overload that takes a StringComparison before the values:

Removing a file extension regardless of case
string fileName = "report.PDF".RemovePostFix(StringComparison.OrdinalIgnoreCase, ".pdf"); // → "report"
string handler = "getUser".RemovePreFix(StringComparison.OrdinalIgnoreCase, "GET"); // → "User"
note

These methods never throw for null. A null or empty receiver is returned as is, a null array of values returns the input unchanged, and null or empty entries in the list are skipped. The default comparison is ordinal.

Finding and replacing​

ReplaceFirst​

ReplaceFirst(search, replace) replaces only the first occurrence of search, while string.Replace replaces every occurrence. Pass null as replace to remove the match. The optional comparisonType defaults to StringComparison.Ordinal.

Filling the first placeholder only
string template = "Order {id} for user {id}";

string filled = template.ReplaceFirst("{id}", "1042"); // → "Order 1042 for user {id}"
string removed = "Total: USD 250".ReplaceFirst("USD ", null); // → "Total: 250"
string noMatch = "Total: 250".ReplaceFirst("EUR", "USD"); // → "Total: 250"

string greeting = "Hello hello".ReplaceFirst("HELLO", "Bye", StringComparison.OrdinalIgnoreCase);
// → "Bye hello"
warning

An empty search throws ArgumentException (parameter search). Version 1.x inserted the replacement at position 0 instead; see Upgrading from 1.x. A null receiver or a null search throws ArgumentNullException.

Between​

Between(start, end) finds the first start delimiter, then the next end delimiter after it, and returns the text in between. It returns null when either delimiter is missing. Delimiters can be longer than one character.

Reading an order number from a message
string message = "Order [ORD-1042] shipped";

string? orderNumber = message.Between("[", "]"); // → "ORD-1042"
string? firstBold = "<b>bold</b> <b>x</b>".Between("<b>", "</b>"); // → "bold"
string? empty = "a[]b".Between("[", "]"); // → ""
string? missing = "Order ORD-1042 shipped".Between("[", "]"); // → null
string? wrongOrder = "end] before [start".Between("[", "]"); // → null

The optional comparisonType (default StringComparison.Ordinal) applies to both delimiters:

Case-insensitive delimiters
string? value = "<B>x</b>".Between("<b>", "</B>", StringComparison.OrdinalIgnoreCase); // → "x"

A null receiver, start or end throws ArgumentNullException with that parameter's name. Check the result for null before you use it. An empty string means the delimiters were found with nothing between them.

NthIndexOf​

NthIndexOf(c, n) returns the zero-based index of the nth occurrence of the character c. Counting starts at 1. It returns -1 when the string has fewer than n occurrences.

Taking the year and month from a date string
string date = "2026-11-04";

int secondDash = date.NthIndexOf('-', 2); // → 7
int thirdDash = date.NthIndexOf('-', 3); // → -1
string yearMonth = date.Left(secondDash); // → "2026-11"

n less than 1 throws ArgumentOutOfRangeException (parameter n). A null receiver throws ArgumentNullException. The match is an exact character comparison.

Splitting and line endings​

Split​

Split(separator) and Split(separator, options) split the string on a whole string separator, such as "::".

Splitting a list of SKUs
string[] skus = "SKU-1::SKU-2::SKU-3".Split("::");
// → ["SKU-1", "SKU-2", "SKU-3"]

string[] noEmpty = "SKU-1::::SKU-2".Split("::", StringSplitOptions.RemoveEmptyEntries);
// → ["SKU-1", "SKU-2"]
info

.NET Standard 2.1 and .NET Core 2.0 and later give string its own instance method Split(string separator, StringSplitOptions options). C# always prefers an instance method over an extension method, so the calls above bind to the built-in method. The result is the same. Only the static call StringExtensions.Split(text, "::") reaches this extension, which throws ArgumentNullException (parameter str) for a null input instead of NullReferenceException.

SplitToLines​

SplitToLines() splits the string into lines. It recognizes \r\n, \r and \n on every operating system, so text from Windows, Linux or classic Mac files splits the same way. The overload SplitToLines(options) takes StringSplitOptions.

Reading the lines of an uploaded order file
string upload = "line 1\nline 2\r\nline 3\rline 4";

string[] lines = upload.SplitToLines();
// → ["line 1", "line 2", "line 3", "line 4"]

string[] withBlanks = "a\n\nb\r\n".SplitToLines();
// → ["a", "", "b", ""]

string[] withoutBlanks = "a\n\nb\r\n".SplitToLines(StringSplitOptions.RemoveEmptyEntries);
// → ["a", "b"]

A \r\n pair counts as one line break. A trailing line break produces a trailing empty line unless you pass StringSplitOptions.RemoveEmptyEntries. A null receiver throws ArgumentNullException.

NormalizeLineEndings​

NormalizeLineEndings() converts every \r\n, \r and \n to Environment.NewLine. Use it before you compare text, or before you write a file that should use the platform's line ending.

Normalizing a customer note
string note = "Line 1\r\nLine 2\nLine 3";

string normalized = note.NormalizeLineEndings();
// → "Line 1\r\nLine 2\r\nLine 3" on Windows
// → "Line 1\nLine 2\nLine 3" on Linux and macOS
note

The result depends on the machine the code runs on. For a fixed ending, use string.Join("\n", text.SplitToLines()). A null receiver throws ArgumentNullException.

Masking sensitive values​

Mask​

Mask(keepStart = 0, keepEnd = 4, maskChar = '*') hides the middle of a value and keeps keepStart leading and keepEnd trailing characters visible. The result has the same length as the input. Use it to show a card number, account number or token in a UI or a log line.

Masking payment details
string cardNumber = "4111111111111111";

string lastFour = cardNumber.Mask(); // → "************1111"
string binAndLastFour = cardNumber.Mask(keepStart: 6, keepEnd: 4); // → "411111******1111"

string token = "tok_9f8e7d6c5b";
string maskedToken = token.Mask(keepStart: 4, keepEnd: 0, maskChar: '#'); // → "tok_##########"

If keepStart + keepEnd is equal to or larger than the length, nothing would be hidden, so every character is masked:

Short values are fully masked
string pin = "123".Mask(); // → "***"
string code = "abcd".Mask(2, 2); // → "****"
string empty = "".Mask(); // → ""

A null receiver throws ArgumentNullException (parameter str). A negative keepStart or keepEnd throws ArgumentOutOfRangeException with that parameter's name.

MaskEmail​

MaskEmail(maskChar = '*') hides the local part of an e-mail address except its first character. The domain stays visible, so support staff can still see which provider a customer uses.

Showing a customer's e-mail on a receipt
string email = "jane.doe@contoso.com";

string masked = email.MaskEmail(); // → "j*******@contoso.com"
string hashes = email.MaskEmail('#'); // → "j#######@contoso.com"
string oneChar = "a@contoso.com".MaskEmail(); // → "*@contoso.com"
string invalid = "not-an-email".MaskEmail(); // → "************"
note

The method splits on the last @, so "first@second@example.com" becomes "f***********@example.com". A local part of one character is masked completely. A value without @ is masked completely. An empty local part ("@example.com") is returned as is. The method does not validate the address. A null receiver throws ArgumentNullException (parameter email).

tip

To mask sensitive properties of a whole object before you log it, see JSON and safe logging.

Text layout​

The line-based methods in this section (Indent, Dedent, TrimLines) keep each line break exactly as it was: \r\n stays \r\n, and \n stays \n.

Indent​

Indent(count) puts count spaces in front of every line, including the first. Indent(prefix) puts any string in front of every line, for example "> " for a quoted reply. Lines that are empty or contain only whitespace get no prefix and stay as they are.

Indenting a nested block
string body = "Line 1\nLine 2";

string indented = body.Indent(4); // → " Line 1\n Line 2"
Quoting a customer message in a reply
string message = "Thanks for the order.\n\nWe ship today.";

string quoted = message.Indent("> ");
Output
> Thanks for the order.

> We ship today.

Windows line endings are kept:

Line endings are preserved
string windows = "a\r\nb\r\n".Indent(1); // → " a\r\n b\r\n"

A null text or null prefix throws ArgumentNullException. A negative count throws ArgumentOutOfRangeException (parameter count). An empty string returns an empty string.

Dedent​

Dedent() removes the leading whitespace that every non-blank line has in common, like Python's textwrap.dedent. Relative indentation is kept, and whitespace-only lines become empty. Use it for text from templates, files or string literals whose indentation follows the source code rather than the output.

Removing the code indentation from a SQL snippet
string sql = " SELECT Id, Total\n FROM Orders\n WHERE Status = 'Paid'";

string clean = sql.Dedent();
Output
SELECT Id, Total
FROM Orders
WHERE Status = 'Paid'

Dedent(count) removes up to count leading spaces or tabs from every line. A tab counts as one character. A line with less indentation loses all of it.

Removing a fixed two-character margin
string config = " total: 10\n tax: 2\n\tdue: 12\nnote";

string shifted = config.Dedent(2); // → " total: 10\ntax: 2\ndue: 12\nnote"
note

Dedent() counts only spaces and tabs as margin and compares them character by character. A tab is not equal to spaces, so "\t a\n\t b".Dedent() removes only the shared "\t " and returns " a\nb". If one non-blank line has no margin, nothing is removed: " a\nb" stays " a\nb". A null text throws ArgumentNullException, and a negative count throws ArgumentOutOfRangeException.

Center​

Center(width) centers the text in a field of width spaces. Center(width, padding) uses another padding character. When the padding cannot be split evenly, the extra character goes on the right. Text that is already as wide as the field, or wider, is returned unchanged.

Centering a report title
string title = "Invoice".Center(15, '='); // → "====Invoice===="
string odd = "hi".Center(7); // → " hi "
string tooWide = "Total".Center(3); // → "Total"
string blank = "".Center(3); // → " "

A null text throws ArgumentNullException. A negative width throws ArgumentOutOfRangeException (parameter width). Width counts UTF-16 characters, not display columns, so wide characters such as CJK text or emoji can look off-center in a terminal.

NormalizeWhitespace​

NormalizeWhitespace() collapses every run of whitespace (spaces, tabs, line breaks) into one space and trims both ends. Use it to clean up user input before you store or compare it.

Cleaning a shipping note
string raw = " Order #1042 \t shipped\r\n to Amman ";

string clean = raw.NormalizeWhitespace(); // → "Order #1042 shipped to Amman"
string onlySpaces = " ".NormalizeWhitespace(); // → ""
string paragraphs = "a\n\n\nb".NormalizeWhitespace(); // → "a b"

Whitespace is anything char.IsWhiteSpace accepts, so a non-breaking space ('\u00A0') also becomes a normal space. A null text throws ArgumentNullException.

Repeat​

Repeat(count) on a string repeats the text count times. Repeat(count) on a char builds a string of count copies of the character. Zero returns an empty string.

Drawing separators
string pattern = "-=".Repeat(5); // → "-=-=-=-=-="
string line = '-'.Repeat(10); // → "----------"
string none = "ab".Repeat(0); // → ""

Together with Center this is enough for a simple text banner:

An invoice header
string rule = '='.Repeat(20);
string heading = "INVOICE #1042".Center(20);

string header = $"{rule}\n{heading}\n{rule}";
Output
====================
INVOICE #1042
====================

A negative count throws ArgumentOutOfRangeException (parameter count). A null string receiver throws ArgumentNullException. If the total length does not fit in an int, the string overload throws OverflowException.

TrimLines​

TrimLines() removes the trailing whitespace from every line and keeps the line endings. Leading whitespace stays. Use it before you compare generated text with an expected value, or before you save text where trailing spaces cause noisy diffs.

Cleaning generated report text
string report = "Total: \r\nTax: \t\n Due \n";

string trimmed = report.TrimLines(); // → "Total:\r\nTax:\n Due\n"

A null text throws ArgumentNullException (parameter text).

See also​