Why is this an issue?

Logging arguments should not require evaluation in order to avoid unnecessary performance overhead. When passing concatenated strings or string interpolations directly into a logging method, the evaluation of these expressions occurs every time the logging method is called, regardless of the log level. This can lead to inefficient code execution and increased resource consumption.

Instead, it is recommended to use the overload of the logger that accepts a log format and its arguments as separate parameters. By separating the log format from the arguments, the evaluation of expressions can be deferred until it is necessary, based on the log level. This approach improves performance by reducing unnecessary evaluations and ensures that logging statements are only evaluated when needed.

Furthermore, using a constant log format enhances observability and facilitates searchability in log aggregation and monitoring software.

The rule covers the following logging frameworks:

How to fix it

Use an overload that takes the log format and the parameters as separate arguments. The log format should be a constant string.

Code examples

Noncompliant code example

logger.DebugFormat($"The value of the parameter is: {parameter}.");

Compliant solution

logger.DebugFormat("The value of the parameter is: {Parameter}.", parameter);

Resources

Documentation

Benchmarks

Method Runtime Mean Standard Deviation Allocated

CastleCoreLoggingTemplateNotConstant

.NET 9.0

230.306 us

2.7116 us

479200 B

CastleCoreLoggingTemplateConstant

.NET 9.0

46.827 us

1.4743 us

560000 B

CastleCoreLoggingTemplateNotConstant

.NET Framework 4.8.1

1,060.413 us

32.3559 us

1115276 B

CastleCoreLoggingTemplateConstant

.NET Framework 4.8.1

93.697 us

1.8201 us

561650 B

MSLoggingTemplateNotConstant

.NET 9.0

333.246 us

12.9214 us

479200 B

MSLoggingTemplateConstant

.NET 9.0

441.118 us

68.7999 us

560000 B

MSLoggingTemplateNotConstant

.NET Framework 4.8.1

1,542.076 us

99.3423 us

1115276 B

MSLoggingTemplateConstant

.NET Framework 4.8.1

698.071 us

18.6319 us

561653 B

NLogLoggingTemplateNotConstant

.NET 9.0

178.789 us

9.2528 us

479200 B

NLogLoggingTemplateConstant

.NET 9.0

6.009 us

1.3303 us

-

NLogLoggingTemplateNotConstant

.NET Framework 4.8.1

1,086.260 us

44.1670 us

1115276 B

NLogLoggingTemplateConstant

.NET Framework 4.8.1

25.132 us

0.5666 us

-

SerilogLoggingTemplateNotConstant

.NET 9.0

234.460 us

7.4831 us

479200 B

SerilogLoggingTemplateConstant

.NET 9.0

49.854 us

1.8232 us

-

SerilogLoggingTemplateNotConstant

.NET Framework 4.8.1

1,103.939 us

47.0203 us

1115276 B

SerilogLoggingTemplateConstant

.NET Framework 4.8.1

35.752 us

0.6022 us

-

Log4NetLoggingTemplateNotConstant

.NET 9.0

255.754 us

5.6046 us

479200 B

Log4NetLoggingTemplateConstant

.NET 9.0

46.425 us

1.7087 us

240000 B

Log4NetLoggingTemplateNotConstant

.NET Framework 4.8.1

1,109.874 us

23.4388 us

1115276 B

Log4NetLoggingTemplateConstant

.NET Framework 4.8.1

92.305 us

2.4161 us

240707 B

Glossary

The results were generated by running the following snippet with BenchmarkDotNet:

using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

[Params(10_000)]
public int Iterations;

private ILogger ms_logger;
private Castle.Core.Logging.ILogger cc_logger;
private log4net.ILog l4n_logger;
private Serilog.ILogger se_logger;
private NLog.ILogger nl_logger;

[GlobalSetup]
public void GlobalSetup()
{
    ms_logger = new LoggerFactory().CreateLogger<LoggingTemplates>();
    cc_logger = new Castle.Core.Logging.NullLogFactory().Create("Castle.Core.Logging");
    l4n_logger = log4net.LogManager.GetLogger(typeof(LoggingTemplates));
    se_logger = Serilog.Log.Logger;
    nl_logger = NLog.LogManager.GetLogger("NLog");
}

[BenchmarkCategory("Microsoft.Extensions.Logging")]
[Benchmark]
public void MSLoggingTemplateNotConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        ms_logger.LogInformation($"Param: {i}");
    }
}

[BenchmarkCategory("Microsoft.Extensions.Logging")]
[Benchmark]
public void MSLoggingTemplateConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        ms_logger.LogInformation("Param: {Parameter}", i);
    }
}

[BenchmarkCategory("Castle.Core.Logging")]
[Benchmark]
public void CastleCoreLoggingTemplateNotConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        cc_logger.Info($"Param: {i}");
    }
}

[BenchmarkCategory("Castle.Core.Logging")]
[Benchmark]
public void CastleCoreLoggingTemplateConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        cc_logger.InfoFormat("Param: {Parameter}", i);
    }
}

[BenchmarkCategory("log4net")]
[Benchmark]
public void Log4NetLoggingTemplateNotConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        l4n_logger.Info($"Param: {i}");
    }
}

[BenchmarkCategory("log4net")]
[Benchmark]
public void Log4NetLoggingTemplateConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        l4n_logger.InfoFormat("Param: {Parameter}", i);
    }
}

[BenchmarkCategory("Serilog")]
[Benchmark]
public void SerilogLoggingTemplateNotConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        se_logger.Information($"Param: {i}");
    }
}

[BenchmarkCategory("Serilog")]
[Benchmark]
public void SerilogLoggingTemplateConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        se_logger.Information("Param: {Parameter}", i);
    }
}

[BenchmarkCategory("NLog")]
[Benchmark]
public void NLogLoggingTemplateNotConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        nl_logger.Info($"Param: {i}");
    }
}

[BenchmarkCategory("NLog")]
[Benchmark]
public void NLogLoggingTemplateConstant()
{
    for (int i = 0; i < Iterations; i++)
    {
        nl_logger.Info("Param: {Parameter}", i);
    }
}

Hardware Configuration:

BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.5247/22H2/2022Update)
12th Gen Intel Core i7-12800H, 1 CPU, 20 logical and 14 physical cores
  [Host]               : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
  .NET 9.0             : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
  .NET Framework 4.8.1 : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256