When building applications that display messages, generate reports, or produce log output, you need a clean way to insert variable values into predefined text templates. Concatenating strings manually is error-prone and hard to maintain. C# .NET provides several powerful approaches for string formatting with placeholders.
Method 1: String.Format (Classic Approach)
String.Format uses indexed placeholders enclosed in curly braces. Each placeholder number corresponds to a positional argument:
string template = "Hello, {0}! You have {1} new messages.";
string result = String.Format(template, "Alice", 5);
// Output: "Hello, Alice! You have 5 new messages."
Reusing Placeholders
You can reference the same index multiple times:
string template = "{0} is great. I love {0}!";
string result = String.Format(template, "C#");
// Output: "C# is great. I love C#!"
Alignment and Padding
Add a comma and width after the index to align output. A positive number right-aligns; a negative number left-aligns:
string template = "|{0,-15}|{1,10}|";
string result = String.Format(template, "Item", "Price");
// Output: "|Item | Price|"
Format Specifiers
Add a colon and format string after the index for built-in formatting:
// Currency
String.Format("{0:C}", 49.99); // "$49.99" (culture-dependent)
// Decimal with precision
String.Format("{0:N2}", 1234567.891); // "1,234,567.89"
// Date
String.Format("{0:yyyy-MM-dd}", DateTime.Now); // "2026-02-11"
// Percentage
String.Format("{0:P1}", 0.856); // "85.6%"
// Hex
String.Format("{0:X}", 255); // "FF"
Storing Templates Externally
A key advantage of String.Format is that the template string can be stored outside your code — in a database, resource file, or configuration — and the placeholders are resolved at runtime:
// Template loaded from a resource file
string errorTemplate = Resources.ErrorMessages.FileNotFound;
// Value: "The file '{0}' was not found in directory '{1}'."
string message = String.Format(errorTemplate, fileName, directory);
This makes it straightforward to support localization and to update messages without recompiling.
Method 2: String Interpolation (Modern Approach)
Introduced in C# 6, string interpolation lets you embed expressions directly in string literals using the $ prefix:
string name = "Alice";
int count = 5;
string result = $"Hello, {name}! You have {count} new messages.";
Expressions Inside Placeholders
You can use any valid C# expression inside the braces:
string result = $"Total: {price * quantity:C2}";
string result2 = $"Name: {user.FirstName.ToUpper()}";
string result3 = $"Status: {(isActive ? "Active" : "Inactive")}";
Note that the ternary expression requires parentheses inside the interpolation braces.
Format Specifiers
Format specifiers work the same way as in String.Format, placed after a colon:
decimal price = 49.99m;
DateTime now = DateTime.Now;
string result = $"Price: {price:C2}, Date: {now:MMMM dd, yyyy}";
// Output: "Price: $49.99, Date: February 11, 2026"
Alignment
Use a comma for alignment, just like String.Format:
string result = $"|{"Item",-15}|{"Price",10}|";
// Output: "|Item | Price|"
Raw String Literals (C# 11+)
C# 11 introduced raw string literals with """ that combine well with interpolation for multi-line templates:
string name = "Alice";
string html = $$"""
<div class="greeting">
<h1>Hello, {{name}}</h1>
</div>
""";
The number of $ signs determines how many braces are needed for interpolation (here $$ requires {{ and }}), which avoids conflicts with literal braces in HTML or JSON.
Method 3: Composite Formatting
Several .NET methods accept composite format strings directly, so you do not need to call String.Format separately:
// Console.WriteLine
Console.WriteLine("User {0} logged in at {1:HH:mm:ss}", username, DateTime.Now);
// StringBuilder.AppendFormat
var sb = new StringBuilder();
sb.AppendFormat("Order #{0}: {1} x {2:C2}\n", orderId, itemName, price);
// TextWriter.Write / StreamWriter.Write
writer.Write("Error in {0}: {1}", moduleName, errorMessage);
// Debug and Trace
Debug.WriteLine("Value of x: {0}", x);
These methods use the same {index:format} syntax as String.Format.
Method 4: StringBuilder for Loops and Large Strings
When building a string with many repeated placeholder substitutions, use StringBuilder to avoid creating intermediate string objects:
var sb = new StringBuilder();
sb.AppendLine("Order Summary");
sb.AppendLine("=============");
foreach (var item in orderItems)
{
sb.AppendFormat(" {0,-30} {1,3} x {2,10:C2}\n",
item.Name, item.Quantity, item.Price);
}
sb.AppendFormat("\nTotal: {0:C2}", orderItems.Sum(i => i.Quantity * i.Price));
string report = sb.ToString();
StringBuilder is significantly more efficient than concatenation in loops because it avoids allocating a new string on every iteration.
Method 5: Custom Format Providers
You can create a custom IFormatProvider and ICustomFormatter to define your own formatting logic:
public class UpperCaseFormatter : IFormatProvider, ICustomFormatter
{
public object GetFormat(Type formatType)
{
return formatType == typeof(ICustomFormatter) ? this : null;
}
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (format == "UC" && arg is string s)
{
return s.ToUpperInvariant();
}
// Fall back to default formatting
return arg is IFormattable formattable
? formattable.ToString(format, CultureInfo.CurrentCulture)
: arg?.ToString() ?? string.Empty;
}
}
// Usage
var formatter = new UpperCaseFormatter();
string result = String.Format(formatter, "Hello, {0:UC}!", "alice");
// Output: "Hello, ALICE!"
Custom format providers are useful when you have domain-specific formatting rules that you want to apply consistently across your application.
Method 6: Named Placeholders with Replace
For simple cases where you want descriptive placeholder names rather than numeric indices, you can use String.Replace:
string template = "Dear {CustomerName}, your order #{OrderId} has shipped.";
string result = template
.Replace("{CustomerName}", customer.Name)
.Replace("{OrderId}", order.Id.ToString());
This approach is easy to read but creates a new string for each replacement. For templates with many placeholders, consider using StringBuilder.Replace or a dedicated template library.
Comparison of Approaches
| Approach | Best For | Template Source |
|---|---|---|
String.Format | External templates, localization | Runtime strings |
$"" interpolation | Inline formatting in code | Compile-time strings |
StringBuilder | Loops, large string assembly | Either |
| Composite formatting | Console, logging, diagnostics | Inline |
Custom IFormatProvider | Domain-specific formatting rules | Either |
String.Replace | Named placeholders, simple templates | Runtime strings |
Best Practices
- Use string interpolation for most inline formatting in your code. It is the most readable option.
- Use
String.Formatwhen templates are stored externally (resource files, databases, configuration) or when supporting localization. - Use
StringBuilderwhen building strings in loops or when concatenating more than a few segments. - Specify culture when formatting numbers, currencies, or dates for end users:
String.Format(CultureInfo.InvariantCulture, ...)orvalue.ToString("C2", new CultureInfo("en-US")). - Avoid excessive concatenation with
+in loops, as each concatenation allocates a new string object.
Summary
C# .NET offers flexible string templating through String.Format for runtime templates, $"" interpolation for readable inline formatting, StringBuilder for performance-sensitive scenarios, and custom format providers for specialized requirements. Choose the approach that best fits whether your template is known at compile time or loaded at runtime, and whether you need control over culture-specific formatting.