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

ApproachBest ForTemplate Source
String.FormatExternal templates, localizationRuntime strings
$"" interpolationInline formatting in codeCompile-time strings
StringBuilderLoops, large string assemblyEither
Composite formattingConsole, logging, diagnosticsInline
Custom IFormatProviderDomain-specific formatting rulesEither
String.ReplaceNamed placeholders, simple templatesRuntime strings

Melhores Práticas

  1. Use string interpolation for most inline formatting in your code. It is the most readable option.
  2. Use String.Format when templates are stored externally (resource files, databases, configuration) or when supporting localization.
  3. Use StringBuilder when building strings in loops or when concatenating more than a few segments.
  4. Specify culture when formatting numbers, currencies, or dates for end users: String.Format(CultureInfo.InvariantCulture, ...) or value.ToString("C2", new CultureInfo("en-US")).
  5. Avoid excessive concatenation with + in loops, as each concatenation allocates a new string object.

Resumo

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.