How to Reuse Structured Logging Messages Without Violating CA2254 in .NET
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
ILoggerto 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.