Understanding .NET Analyzer Rule CA2254

When working with structured logging in modern .NET (via ILogger), you might encounter the static analysis warning CA2254: The logging message template should not vary between calls to 'LoggerExtensions.Log...'.

This rule exists because structured logging relies on constant message templates with named placeholders (e.g., "Person {FirstName} {LastName} encountered an issue"). Passing pre-interpolated strings or dynamic template variables breaks structured telemetry indexing in aggregators like Seq, Datadog, or Elasticsearch, and introduces performance penalties.

However, developers often face a dilemma when they need to reuse the same human-readable message for an external system (such as an Event Bus, Exception message, or API response) without duplicating string literals.


Solution 1: Decouple Payloads with Structured DTOs (Best Practice)

In distributed architectures, sharing a rendered string across systems is an anti-pattern. Instead of sending an already-rendered string to your event bus, publish a structured event object and let the receiving service format it as needed.

// Define a structured event payload
public record PersonIssuePayload(string FirstName, string LastName, string IssueDetails);

// Usage
var firstName = "Lorenz";
var lastName = "Otto";

// 1. Structured log preserving placeholders
logger.LogWarning("Person {FirstName} {LastName} encountered an issue", firstName, lastName);

// 2. Publish structured event
eventBus.Publish(new PersonIssuePayload(firstName, lastName, "encountered an issue"));

Solution 2: Use a Constant Template with .NET 6+ [LoggerMessage] Source Generators

If you are on .NET 6 or later, Microsoft recommends using the compile-time [LoggerMessage] source generator. This guarantees high performance, eliminates allocations, and complies strictly with CA2254.

public static partial class LogDefinitions
{
    public const string PersonIssueTemplate = "Person {FirstName} {LastName} encountered an issue";

    [LoggerMessage(EventId = 101, Level = LogLevel.Warning, Message = PersonIssueTemplate)]
    public static partial void LogPersonIssue(this ILogger logger, string firstName, string lastName);
}

To generate the plain string for external use without duplicating code, you can use standard positional formatting or string interpolation alongside the structured logger:

var firstName = "Lorenz";
var lastName = "Otto";

// Log efficiently with CA2254 compliance
logger.LogPersonIssue(firstName, lastName);

// Format the message for external distribution
var message = $"Person {firstName} {lastName} encountered an issue";
eventBus.Publish(new Payload(message));

Solution 3: Create a Shared Formatted Message Helper

If you must maintain a single source of truth for both logging and string production, you can encapsulate the template and parameter extraction in a custom utility or record:

public record LoggableMessage(string Template, params object?[] Arguments)
{
    public override string ToString()
    {
        // Simple replacement for basic tokenized strings
        var result = Template;
        for (int i = 0; i < Arguments.Length; i++)
        {
            var match = System.Text.RegularExpressions.Regex.Match(result, @"\{[a-zA-Z0-9_]+\}");
            if (match.Success)
            {
                result = result.Remove(match.Index, match.Length).Insert(match.Index, Arguments[i]?.ToString() ?? string.Empty);
            }
        }
        return result;
    }
}

However, note that passing a dynamic template like logger.LogWarning(msg.Template, msg.Arguments) will still trigger CA2254 because the compiler cannot guarantee the template remains constant across runtime invocations.


Summary

  • Avoid string interpolation in ILogger to preserve semantic querying in log aggregators.
  • Do not disable CA2254 unless strictly necessary; it protects your application from performance degradation and memory churn.
  • Prefer decoupled structured events for publishing data across services rather than sharing formatted display strings.