Let me start off by saying if you’ve not tried the source generation logging mechanism yet, which first came out in .NET 6, then I would highly recommend it. It allows for improved logging performance without having to write it yourself; win win!

For further reading - https://learn.microsoft.com/en-us/dotnet/core/extensions/high-performance-logging

If we look at a simple class definition which has some logging setup with the LoggerMessage attribute, below, we can see that it contains the standard constructor injection for the service to be provided with a logger at run time.

public partial class MyService
{
    private readonly ILogger<MyService> _logger;

    public MyService(ILogger<MyService> logger)
    {
        _logger = logger;
    }

    [LoggerMessage(LogLevel.Debug, "Logging this ...")]
    private partial void LoggingThis();
}

This is a pretty standard setup when using the LoggerMessage attribute and one of the many different ways you can use it. The partial method definition and the LoggerMessage attriute usage allows for the implementation of the method to be generated at build time.

partial class MyService
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.10.11423")]
    private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __LoggingThisCallback =
        global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(138567002, nameof(LoggingThis)), "Logging this ...", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); 

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.10.11423")]
    private partial void LoggingThis()
    {
        if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Debug))
        {
            __LoggingThisCallback(_logger, null);
        }
    }
}

As we can see this has all the benefits you would expect from a logging method; checking the log level is enabled and specifying a Define delegate for the performance benefits.

So what is the issue?

We if you look closer at the generate code you can see that it is using the _logger class level field value to call into. Nothing wrong with this. However if you are working on modern .NET and using C#12 then your IDE might suggest you move to using primary constructor injection.

public partial class MyService(ILogger<MyService> logger)
{
    [LoggerMessage(LogLevel.Debug, "Logging this ...")]
    private partial void LoggingThis();
}

The above example is the original example converted to using a primary constructor. The problem with this is the ILogger<MyService> parameter is now not accessible to the logging source code generation! With this you will be presented with a build error with the code SYSLIB1019.

What is SYSLIB1019? Looking at the documentation - https://learn.microsoft.com/en-gb/dotnet/fundamentals/syslib-diagnostics/syslib1019 - we can see that it is an error which occurs when there isn’t one, and only one, field of type ILogger and also the method signature doesn’t explicitly include a parameter of type ILogger.

public partial class MyService(ILogger<MyService> logger)
{
    public void Run()
    {
        LoggingThis(logger);
    }

    [LoggerMessage(LogLevel.Debug, "Logging this ...")]
    private partial void LoggingThis(ILogger<MyService> logger);
}

If we follow the suggestion of including the parameter of the ILogger type then the compiler is happy again. It does however, in my opinion, make it look quite clunky when calling the logging method as you have to pass in the ILogger parameter in. The generated code is updated to use the parameter logger so is happy from that perspective.

The next issue you are then presented with is a SYSLIB1009 compiler warning which says all logging messages should be static - https://learn.microsoft.com/en-gb/dotnet/fundamentals/syslib-diagnostics/syslib1009

public partial class MyService(ILogger<MyService> logger)
{
    public void Run()
    {
        LoggingThis(logger);
    }

    [LoggerMessage(LogLevel.Debug, "Logging this ...")]
    private partial void LoggingThis(ILogger<MyService> logger);
}

This keeps the compiler happy but doesn’t resolve the need to pass the ILogger into the logging method. You might as well make the logging message an extension method and remove it from the calling class completely. How would that look?

public static partial class LoggingExtensions
{
    [LoggerMessage(LogLevel.Debug, "Logging this ...")]
    public static partial void LoggingThis(this ILogger<MyService> logger);
}

public class MyService(ILogger<MyService> logger)
{
    public void Run()
    {
        logger.LoggingThis();
    }
}

This now adds additional noise to the intellisense on ILogger which as your application grows can get a bit much. It can be limited by namespace restriction etc. Whether this is right or wrong is up to you to decide and maybe a topic for another day. It does solve the referencing the primary constructor parameter however still doesn’t feel right.

So how can we resolve this?

One option is to not use Primary Constructors. They have a good use case for record types and data transfer objects etc. however when you’re instantiating services through dependency injection they still don’t quite seem right.

However if your heart is set on using Primary Constructors for your service and keep your LoggerMessage based logging contained in the service which calls it then the only option I could come up with is assign the constructor parameter to a readonly field.

public partial class MyService(ILogger<MyService> logger)
{
    private readonly ILogger<MyService> _logger = logger;

    public void Run()
    {
        LoggingThis();
    }

    [LoggerMessage(LogLevel.Debug, "Logging this ...")]
    private partial void LoggingThis();
}

This implementation is the closest you can get based on the original implementation and Primary Constructors.

As for me I’m going to be sticking with explicit constructor injection for now but I reserve the right to change my mind!

What are you thoughts? On X/Twitter @WestDiscGolf as always if you want to get in touch.