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
| Method | What it does |
|---|---|
Left | Gets the first n characters. |
Right | Gets the last n characters. |
Truncate | Cuts to a maximum length, keeping the start. |
TruncateFromBeginning | Cuts to a maximum length, keeping the end. |
TruncateWithPostfix | Cuts to a maximum length and ends with "..." or your own marker. |
EnsureStartsWith | Adds a character at the start if it is missing. |
EnsureEndsWith | Adds a character at the end if it is missing. |
RemovePreFix | Removes the first matching prefix. |
RemovePostFix | Removes the first matching suffix. |
ReplaceFirst | Replaces only the first occurrence of a value. |
Between | Gets the text between two delimiters. |
NthIndexOf | Finds the index of the nth occurrence of a character. |
Split | Splits by a whole string separator. |
SplitToLines | Splits on \r\n, \r and \n on every platform. |
NormalizeLineEndings | Converts every line ending to Environment.NewLine. |
Mask | Hides the middle of a value such as a card number. |
MaskEmail | Hides the local part of an e-mail address. |
Indent | Puts spaces or a prefix in front of every line. |
Dedent | Removes the common leading margin, or a fixed number of characters, from every line. |
Center | Centers text in a field of a given width. |
NormalizeWhitespace | Collapses whitespace runs to one space and trims the ends. |
Repeat | Repeats a string or a character n times. |
TrimLines | Removes 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.
string invoiceNumber = "INV-2026-000123";
string series = invoiceNumber.Left(3); // → "INV"
string sequence = invoiceNumber.Right(6); // → "000123"
string none = invoiceNumber.Left(0); // → ""
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.
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.
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.
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:
string status = "Cancelled";
string three = status.TruncateWithPostfix(3); // → "..."
string two = status.TruncateWithPostfix(2); // → ".."
string zero = status.TruncateWithPostfix(0); // → ""
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.
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:
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.
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:
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:
string fileName = "report.PDF".RemovePostFix(StringComparison.OrdinalIgnoreCase, ".pdf"); // → "report"
string handler = "getUser".RemovePreFix(StringComparison.OrdinalIgnoreCase, "GET"); // → "User"
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.
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"
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.
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:
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.
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 "::".
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"]
.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.
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.
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
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.
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:
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.
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(); // → "************"
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).
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.
string body = "Line 1\nLine 2";
string indented = body.Indent(4); // → " Line 1\n Line 2"
string message = "Thanks for the order.\n\nWe ship today.";
string quoted = message.Indent("> ");
> Thanks for the order.
> We ship today.
Windows line endings are kept:
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.
string sql = " SELECT Id, Total\n FROM Orders\n WHERE Status = 'Paid'";
string clean = sql.Dedent();
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.
string config = " total: 10\n tax: 2\n\tdue: 12\nnote";
string shifted = config.Dedent(2); // → " total: 10\ntax: 2\ndue: 12\nnote"
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.
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.
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.
string pattern = "-=".Repeat(5); // → "-=-=-=-=-="
string line = '-'.Repeat(10); // → "----------"
string none = "ab".Repeat(0); // → ""
Together with Center this is enough for a simple text banner:
string rule = '='.Repeat(20);
string heading = "INVOICE #1042".Center(20);
string header = $"{rule}\n{heading}\n{rule}";
====================
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.
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
- String predicates: checks on strings that return
trueorfalse. - String casing:
ToKebabCase,ToTitleCaseand other case conversions. - Text and humanize: templates, pluralizing, word wrapping and word-boundary truncation with
TruncateWords. - Console output: tables, trees and boxes for terminal output.