Back to Blog
C#

C# String Format: Composite Formatting and Specifiers

c# string format: Learn C# string format with composite formatting, format specifiers, alignment, culture handling, and performance tradeoffs.

C#String.FormatComposite FormattingString InterpolationCultureFormat Specifiers
Illustration of C# string format placeholders and format specifiers in a code editor.

c# string format requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The string.Format method in C# uses composite formatting to insert values into a template string. The template contains indexed placeholders such as {0} and {1}, which are replaced at runtime with the corresponding arguments. This article explains the syntax, format specifiers, alignment, culture behavior, and the tradeoffs between string.Format and string interpolation.

Composite Formatting Basics

The core syntax of string.Format is a format string that contains one or more format items. Each format item is enclosed in braces and has the form {index[,alignment][:formatString]}. The index is zero-based and refers to the position of the argument in the parameter list.

string message = string.Format("User {0} logged in at {1}", "alice", DateTime.Now);

The first placeholder {0} receives the first argument, and {1} receives the second. The method returns a new string with the placeholders replaced by the string representation of each argument. If an argument is null, it becomes an empty string.

Format items can appear in any order and can be repeated. For example, "{1} - {0}" is valid. The number of arguments must be at least one greater than the highest index used; otherwise, the runtime throws a FormatException.

Format Specifiers for Common Types

A format specifier controls how a value is converted to text. It follows the index and an optional colon. The most common use is with numeric and date/time types.

string formatted = string.Format("Price: {0:C2}", 1234.5);

The C2 specifier formats the number as currency with two decimal places. The exact output depends on the current culture. For example, in en-US it produces $1,234.50, while in de-DE it produces 1.234,50 €.

Standard numeric format specifiers include:

SpecifierMeaningExample (en-US)
CCurrency$1,234.50
NNumber with group separators1,234.50
FFixed-point1234.50
EScientific notation1.234500E+003
PPercent123,450.00%
XHexadecimal (integer types only)4D2

Date and time types also support format specifiers. The D specifier produces a long date, d a short date, T a long time, and t a short time. Custom format strings, such as "yyyy-MM-dd", give more control but require a separate syntax.

Alignment and Padding

Format items can include an alignment component, which is a positive or negative integer. Positive values right-align the text within the specified width; negative values left-align. This is useful for creating columnar output in console applications or log files.

string table = string.Format("{0,-10} {1,8}", "Name", "Score"); table += string.Format("{0,-10} {1,8}", "Alice", 95); table += string.Format("{0,-10} {1,8}", "Bob", 87);

The first column is left-aligned with a width of 10, and the second is right-aligned with a width of 8. If the value is longer than the width, the alignment is ignored and the value is rendered in full. Alignment does not truncate text.

Alignment works with any format specifier. For example, {0,10:N2} right-aligns a number with two decimal places within a 10-character field. This combination is common when building reports that need consistent column widths.

Culture and Globalization

string.Format has overloads that accept an IFormatProvider. The provider determines how culture-sensitive formats behave. The default overload uses CultureInfo.CurrentCulture, which can produce different results on different machines.

CultureInfo culture = new CultureInfo("fr-FR"); string formatted = string.Format(culture, "{0:N2}", 1234.5);

In this example, the number is formatted with the French decimal separator, producing 1 234,50. Passing an explicit provider is essential in scenarios where output must be consistent regardless of the machine's regional settings, such as when generating reports or API responses.

When the format string itself contains literal braces, you must escape them by doubling them: {{ and }}. This is a common source of errors. For example, string.Format("{{0}} = {0}", 5) produces {0} = 5.

String Interpolation vs string.Format

C# 6 introduced string interpolation, which is syntactic sugar over composite formatting. Interpolated strings start with $ and embed expressions directly in the string.

string name = "alice"; int score = 95; string result = $"{name}: {score}";

Interpolation is generally more readable because the expression appears inline rather than in a separate argument list. It also supports the same format specifiers and alignment: $"{score,8:N2}" is equivalent to string.Format("{0,8:N2}", score).

At runtime, an interpolated string is compiled to a call to string.Format (or a similar method) when the format string is not a constant. When the format string is a constant, the compiler may use DefaultInterpolatedStringHandler to build the result more efficiently, avoiding the overhead of parsing the format string. This makes interpolated strings preferable in most modern C# code.

However, string.Format remains useful when the format string itself is dynamic. For example, if the format template comes from a configuration file or a resource, you cannot use interpolation because the expression positions are not known at compile time. In that case, string.Format is the appropriate tool.

Common Mistakes and Edge Cases

A FormatException is thrown when the format string is invalid or when the number of arguments does not match the placeholders. The most common mistakes are:

  • Using an index that is larger than the highest argument index.
  • Forgetting to escape braces in a format string.
  • Using a format specifier that is not valid for the argument type.
// Throws FormatException: index 2 does not exist string bad = string.Format("{0} {2}", 1, 2); // Throws FormatException: unescaped brace string bad2 = string.Format("{0}", 5); // The format string is "{0}" but the brace is interpreted as a placeholder

Another subtle issue is that string.Format uses the current culture by default. If you are building strings that are stored or transmitted, such as in a file name or an API payload, relying on the current culture can lead to inconsistent output across environments. Always pass an explicit culture, typically CultureInfo.InvariantCulture, when the output must be stable.

Performance Considerations

string.Format and interpolated strings both produce a new string. The cost includes parsing the format string and converting each argument to text. If a format string is reused frequently, string.Format will parse it every time unless you cache the CompositeFormat instance. In .NET 8 and later, CompositeFormat allows you to parse a format string once and reuse it across many calls.

CompositeFormat cf = CompositeFormat.Parse("{0} - {1}"); string result = string.Format(cf, arg1, arg2);

This reduces repeated parsing overhead. For most applications, the difference is negligible, but in high-throughput logging or tight loops, it can matter. Interpolated strings with constant format strings avoid parsing entirely because the compiler generates code that directly appends the values.

When building large strings with many parts, consider using StringBuilder instead of repeated string.Format calls. Each string.Format creates a new string, and concatenating many such strings causes multiple allocations. StringBuilder accumulates the result with fewer allocations.

Choosing Between string.Format and Interpolation

The decision between string.Format and string interpolation depends on whether the format template is known at compile time. Use interpolation when the template is fixed and you have direct access to the variables. It is more readable and often more efficient. Use string.Format when the template is dynamic, such as when it is loaded from a resource file, a database, or a user-provided configuration. In those cases, interpolation cannot be applied because the compiler cannot resolve the expressions.

For culture-sensitive output, both approaches allow you to specify a culture. Interpolated strings accept a culture through the FormattableString type, but the syntax is more verbose. For most scenarios, passing a culture to string.Format is straightforward.

Finally, consider the maintainability of your code. Interpolation keeps the expression next to its placeholder, which reduces the risk of mismatched indexes. If you are working on a codebase that already uses string.Format extensively, consistency may be more important than switching to interpolation. The two approaches can coexist, but a consistent style within a project reduces cognitive load.