A string cannot be changed in place, so every apparent edit allocates, and the routines that hide that cost are the ones worth knowing. Culture is the other half: the plain overloads read the current culture, which makes correctness depend on the machine the code runs on.
Interpolate instead of lining up placeholders
An interpolated string keeps each value where it appears in the result, so the arguments cannot drift out of order the way {0} and {1} holes do in a call to string.Format. The language reference puts it plainly: interpolation is easier to read than composite formatting.
Preferred
$"Version {major}.{minor}.{build}"Avoid
string.Format("Version {0}.{1}.{2}", major, minor, build)Corpus: $ - string interpolation · String interpolation in C# · Composite formatting
A loop that builds text uses StringBuilder
A string cannot be appended to in place, so each + or += in a loop copies the text so far and leaves the previous copy for the collector. StringBuilder appends into one buffer and produces the string once, and AppendFormat runs composite formatting into that buffer with the provider and format string you hand it. For a fixed, small set of values, + and interpolation are clearer and the number of copies is bounded.
Preferred
var sb = new StringBuilder();
foreach (var row in rows)
{
sb.AppendFormat(CultureInfo.InvariantCulture, "{0}={1};", row.Key, row.Value);
}Avoid
string text = "";
foreach (var row in rows)
{
text += row.Key + "=" + row.Value + ";";
}Corpus: Using the StringBuilder Class in .NET · Concatenate strings in C# · Strings in C#
Join writes the separators for you
string.Join puts the separator between the elements and nowhere else, which is exactly the part a hand-written loop has to fix afterwards. string.Concat is the same call with no separator, and both take a collection directly.
Preferred
string csv = string.Join(", ", names);Avoid
string csv = "";
foreach (string name in names)
{
csv += name + ", ";
}Corpus: Concatenate strings in C# · Strings in C#
Slice with AsSpan, not SubstringCA1846
Substring allocates a string and copies the characters, and Split allocates one string per piece. ReadOnlySpan<char> is a window over the same characters, and the span-based TryParse and MemoryExtensions search overloads take it, so a slice that never leaves the method copies nothing.
Preferred
int.TryParse(line.AsSpan(7), out int value);Avoid
int.TryParse(line.Substring(7), out int value);Corpus: CA1846: Prefer AsSpan over Substring · String operations: pattern matching, performance, and span-based search · Memory-related and span types
Say which comparison you meanCA1862
The plain overloads disagree with each other: Equals and Contains compare ordinally, while StartsWith, EndsWith, IndexOf, and Compare use the current culture, so the same call can match different text on different machines. Pass StringComparison.Ordinal or OrdinalIgnoreCase for tokens, paths, keys, and anything else that is not natural language, and leave CurrentCulture to the sorts and displays a person reads. Casing both sides before comparing is that same problem plus an allocation, which is what CA1862 flags.
Preferred
if (header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))Avoid
if (header.ToUpperInvariant().StartsWith("BEARER "))Corpus: Best Practices for Comparing Strings in .NET · CA1307: Specify StringComparison for clarity · CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
Normalize keys with ToUpperInvariantCA1308
ToUpper and ToLower with no culture read the current culture, so a key normalized on one machine can differ from the same key on another. Uppercase is the direction that round-trips, because some characters lose information when they are lowercased, and the invariant culture takes the machine out of the decision. ToUpperInvariant is both of those things at once.
Preferred
string key = headerName.ToUpperInvariant();Avoid
string key = headerName.ToLowerInvariant();Corpus: CA1308: Normalize strings to uppercase · CA1311: Specify a culture or use an invariant version · Changing case in .NET
Give string-keyed collections the comparer
A dictionary compares its keys with the equality comparer it was constructed with, and the sorted collections are culture-sensitive by default, so their invariants can change when the culture does. Pass StringComparer.Ordinal for keys that are identifiers and StringComparer.OrdinalIgnoreCase when case does not distinguish them, rather than folding the key at every read and write.
Preferred
var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);Avoid
var headers = new Dictionary<string, string>();
headers[name.ToUpperInvariant()] = value;Corpus: Best Practices for Comparing Strings in .NET · Comparisons and Sorts Within Collections
Invariant culture for what a machine readsCA1305
Formatting and parsing without a provider use the current culture, so a value written on one machine can parse to a different number on another, or fail to parse at all. Text that is stored in a file, a database, or a protocol carries no reader, so pass CultureInfo.InvariantCulture in both directions and keep the current culture for values that are shown to a person.
Preferred
string row = string.Create(CultureInfo.InvariantCulture, $"{id},{price}");
double parsed = double.Parse(text, CultureInfo.InvariantCulture);Avoid
string row = $"{id},{price}";
double parsed = double.Parse(text);Corpus: CA1305: Specify IFormatProvider · CA1304: Specify CultureInfo · Best practices for displaying and persisting formatted data in .NET
TryParse where the text may not be a number
Parse throws FormatException when the input is not a number, which turns ordinary validation into exception handling. TryParse returns a bool and writes the value to an out parameter, sets that parameter to zero when the text does not fit the type, and the documentation calls it the more efficient of the two.
Preferred
if (!int.TryParse(text, out int page))
{
return BadRequest();
}Avoid
int page = 0;
try { page = int.Parse(text); }
catch (FormatException) { return BadRequest(); }Corpus: How to determine whether a string represents a numeric value · How to convert a string to a number · Convert strings to types
Round-trip formats for values that leave memory
A value that is written out and read back has to survive the trip, and the general formats do not promise that: the plain date and time form carries neither the offset nor the Kind. "o" is the round-trip specifier for DateTime and DateTimeOffset, "R" is the round-trip specifier for the floating-point types and BigInteger, and the documentation recommends "G17" for a double and "G9" for a float.
Preferred
string stamp = timestamp.ToString("o");
string digits = value.ToString("G17", CultureInfo.InvariantCulture);Avoid
string stamp = timestamp.ToString();Corpus: Standard date and time format strings · Standard numeric format strings · How to: Round-trip Date and Time Values
Implement IFormattable when a type has several text forms
A type whose value can be written in more than one way implements IFormattable, whose single method takes the format string and the provider. That is the method composite formatting calls: ToString("F", provider), string.Format, StringBuilder.AppendFormat, and Convert.ToString all reach the same implementation.
Preferred
public string ToString(string? format, IFormatProvider? provider)
=> format == "F"
? string.Create(provider, $"{Fahrenheit:F1} F")
: string.Create(provider, $"{Celsius:F1} C");Avoid
public override string ToString() => $"{Celsius} C";Corpus: Overview: How to format numbers, dates, enums, and other types in .NET · Custom numeric format strings
Literal forms that do not need escapes
A verbatim literal (@"...") treats backslashes as characters, which is what a Windows path or a regular expression needs. A raw literal goes further: quotes and line breaks stay as written, and the column of the closing delimiter is the indent the compiler strips, so JSON, SQL, or XML can sit in the source unchanged.
Preferred
string json = """
{ "id": 1, "name": "Ada" }
""";Avoid
string json = "{ \"id\": 1, \"name\": \"Ada\" }";Corpus: Raw string literals in C# · Strings · Raw string literals - """
One character is a char, not a stringCA1847
A one-character string handed to Contains, StartsWith, EndsWith, IndexOf, LastIndexOf, or StringBuilder.Append runs the string overload, which searches for a string where a single character would do. The char overload is the faster one, and for StartsWith, EndsWith, and IndexOf it is also the ordinal one: a one-character string search follows the current culture.
Preferred
if (id.Contains('_'))Avoid
if (id.Contains("_"))Corpus: CA1847: Use char literal for a single character lookup · CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char · CA1834: Use StringBuilder.Append(char) for single character strings
Null, empty, and blank are one checkCA1820
string.IsNullOrEmpty answers the null and empty cases in one test, and the framework documents it as the faster and safer replacement for comparing against the empty string. string.IsNullOrWhiteSpace covers the case user input actually needs, text that only looks blank, which a comparison against "" would accept.
Preferred
if (string.IsNullOrWhiteSpace(name))
{
return BadRequest(nameof(name));
}Avoid
if (name == null || name == "")
{
return BadRequest(nameof(name));
}Corpus: CA1820: Test for empty strings using string length · Strings