Skip to content

LoggerMessage source generator: preserve params, ref readonly, and scoped parameter modifiers#124589

Draft
Copilot wants to merge 5 commits intomainfrom
copilot/fix-source-generation-error
Draft

LoggerMessage source generator: preserve params, ref readonly, and scoped parameter modifiers#124589
Copilot wants to merge 5 commits intomainfrom
copilot/fix-source-generation-error

Conversation

Copy link
Contributor

Copilot AI commented Feb 19, 2026

The LoggerMessage source generator dropped params, ref readonly, and scoped modifiers from generated partial method implementations, causing CS0758 (mismatched declaration/implementation). C# 13 params collections (params IEnumerable<T>, etc.) were also untested.

Changes

Generator

  • Parser: detect params via IParameterSymbol.IsParams; detect ref readonly via (RefKind)4 (cross-version safe — RefKind.RefReadOnlyParameter only exists in Roslyn 4.8+); detect scoped via paramSymbol.ScopedKind != ScopedKind.None under #if ROSLYN4_4_OR_GREATER
  • LoggerParameter / LoggerParameterSpec: add IsParams and IsScoped fields; #pragma warning disable CS0649 on IsScoped for Roslyn 3.11 builds where the field is intentionally unassigned
  • Emitter (GenParameters): emit scoped, qualifier (in/ref/ref readonly), and params prefixes in correct order
  • Roslyn4.0.cs: propagate IsParams and IsScoped through the incremental spec→model conversion

Tests

  • ParameterTestExtensions.cs: add UseParamsParameter, UseParamsCollectionParameter, UseRefReadOnlyParameter, UseScopedRefParameter (latter three under #if ROSLYN4_8_OR_GREATER)
  • Parser tests: ParamsParameterOK, ParamsCollectionParameterOK, RefReadOnlyParameterOK, ScopedRefParameterOK, ScopedRefReadOnlyParameterOK
  • Emitter baseline tests: TestWithParamsArray and TestWithParamsCollection (C# 13 params IEnumerable<T>)
// All of these now compile correctly:
[LoggerMessage(Message = "{args}", Level = LogLevel.Info)]
static partial void Log1(ILogger logger, params object?[] args);
// → params global::System.Object?[] args  ✓

[LoggerMessage(Message = "{args}", Level = LogLevel.Info)]
static partial void Log2(ILogger logger, params IEnumerable<string> args);
// → params global::System.Collections.Generic.IEnumerable<global::System.String> args  ✓

[LoggerMessage(Message = "{p}", Level = LogLevel.Info)]
static partial void Log3(ILogger logger, ref readonly int p);
// → ref readonly global::System.Int32 p  ✓

[LoggerMessage(Message = "{p}", Level = LogLevel.Info)]
static partial void Log4(ILogger logger, scoped ref int p);
// → scoped ref global::System.Int32 p  ✓
Original prompt

This section details on the original issue you should resolve

<issue_title>Source generation error in LoggerMessageAttribute in Microsoft.Extensions.Logging.Abstractions</issue_title>
<issue_description>### Description

Using 'params' keyword in method parameters "params object?[] args" in partial class under [LoggerMessage] attribute cause Compiler Error CS0758 - in generated code 'params' keyword is absent

Reproduction Steps

using Microsoft.Extensions.Logging;

internal static partial class LogMessages
{
    [LoggerMessage(Message = "Message: {message}, {args}", Level = LogLevel.Information)]
    internal static partial void MSG(this ILogger logger, string? message, params object?[] args);
}

Expected behavior

Compile

Actual behavior

in source-generated file LoggerMessage.g.cs, 'params' keyword is absent in method parameters "global::System.Object?[] args"

partial class LogMessages
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "7.0.7.1805")]
    private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.String?, global::System.Object?[], global::System.Exception?> __MSGCallback =
        global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::System.String?, global::System.Object?[]>(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(-1, nameof(MSG)), "Message: {message}, {args}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); 

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "7.0.7.1805")]
    internal static partial void MSG(this global::Microsoft.Extensions.Logging.ILogger logger, global::System.String? message, global::System.Object?[] args)
    {
        if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
        {
            __MSGCallback(logger, message, args, null);
        }
    }
}

Regression?

No response

Known Workarounds

No response

Configuration

.NET 7.0
Windows 10 x64
Visual Studio Community 2022

Other information

No response</issue_description>

<agent_instructions>Add support for params (including params collections) in logging methods. Add tests verifying it works.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ Tagging subscribers to this area: @dotnet/area-extensions-logging See info in [area-owners.md](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) if you want to be subscribed.
Issue Details

Description

Using 'params' keyword in method parameters "params object?[] args" in partial class under [LoggerMessage] attribute cause Compiler Error CS0758 - in generated code 'params' keyword is absent

Reproduction Steps

using Microsoft.Extensions.Logging;

internal static partial class LogMessages
{
    [LoggerMessage(Message = "Message: {message}, {args}", Level = LogLevel.Information)]
    internal static partial void MSG(this ILogger logger, string? message, params object?[] args);
}

Expected behavior

Compile

Actual behavior

in source-generated file LoggerMessage.g.cs, 'params' keyword is absent in method parameters "global::System.Object?[] args"

partial class LogMessages
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "7.0.7.1805")]
    private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.String?, global::System.Object?[], global::System.Exception?> __MSGCallback =
        global::Microsoft.Extensions.Logging.LoggerMessage.Define<global::System.String?, global::System.Object?[]>(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(-1, nameof(MSG)), "Message: {message}, {args}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); 

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "7.0.7.1805")]
    internal static partial void MSG(this global::Microsoft.Extensions.Logging.ILogger logger, global::System.String? message, global::System.Object?[] args)
    {
        if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
        {
            __MSGCallback(logger, message, args, null);
        }
    }
}

Regression?

No response

Known Workarounds

No response

Configuration

.NET 7.0
Windows 10 x64
Visual Studio Community 2022

Other information

No response


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Author: abrca
Assignees: -

Copilot AI and others added 2 commits February 19, 2026 12:28
…enerator

Co-authored-by: svick <287848+svick@users.noreply.github.com>
…generator

Co-authored-by: svick <287848+svick@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix source generation error in LoggerMessageAttribute LoggerMessage source generator: support params, ref readonly, and scoped parameters Feb 19, 2026
Copilot AI requested a review from svick February 19, 2026 12:45
Co-authored-by: svick <287848+svick@users.noreply.github.com>
Copilot AI changed the title LoggerMessage source generator: support params, ref readonly, and scoped parameters LoggerMessage source generator: preserve params, ref readonly, and scoped parameter modifiers Feb 19, 2026
Co-authored-by: svick <287848+svick@users.noreply.github.com>
@tarekgh tarekgh added this to the 11.0.0 milestone Feb 19, 2026
@tarekgh
Copy link
Member

tarekgh commented Feb 19, 2026

@svick did you read the comment #91678 (comment)?

I agree with @ericstj params will be a problematic to support. The semantic of params is treated as multiple variable parameters. Imagine something like:

[LoggerMessage(Message = "{arg1} ... {arg2}", Level = LogLevel.Info)]
static partial void Log1(ILogger logger, params object?[] args);

The logger message expects to have two parameters. at runtime we should get exactly number of parameters match the message, otherwise should fail. It is better to have the source generator produce a diagnostic for this case and fail it instead of trying to work it out and produce runtime failure.

Whatever we do here, we want to ensure the same be applied to https://github.com/dotnet/extensions/tree/main/src/Generators/Microsoft.Gen.Logging.

@tarekgh tarekgh added the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Feb 19, 2026
@svick
Copy link
Member

svick commented Feb 19, 2026

@tarekgh

The semantic of params is treated as multiple variable parameters.

I don't understand why that behavior would be desirable here.

If you want to have two values, you can add two parameters to the message and two regular parameters to the logging method, there is no need to use params.

If you want to have variable number of values, then with this PR, you can add one parameter to the message, a params parameter to the method and it will format the actual values (because it's internally treated like a regular array).

For example, with the code from the original issue:

internal static partial class LogMessages
{
    [LoggerMessage(Message = "Message: {message}, {args}", Level = LogLevel.Information)]
    internal static partial void MSG(this ILogger logger, string? message, params object?[] args);
}

If you call:

LogMessages.MSG(logger, "message", 1, 2, 3);

Then with this PR and default console logger, you will get:

info: LogMessages[624365154]
      Message: message, 1, 2, 3

What am I missing?

@tarekgh
Copy link
Member

tarekgh commented Feb 19, 2026

The whole point of params keyword is in C# allows a method to accept a variable number of arguments. This is useful when the number of arguments is not known beforehand. This is different than just passing arrays.

For example, if I write something like the following, would you accept it as valid or invalid?

    [LoggerMessage(Message = "Message: {message}, Index:{args0}  Values:{args1}", Level = LogLevel.Information)]
    internal static partial void MSG(this ILogger logger, string? message, params object?[] args);

Then calling:

LogMessages.MSG(logger, "message", 1, 2, 3);

Logically (following C# concepts), this should be valid. This means if we support it, at run time you have validate and ensure args has at least one argument and format it in the message and then format the rest as one array. That will be very confusing. Additionally, we'll lose matching the parameter names in the message to the passed parameters to the method.

Look at https://learn.microsoft.com/en-us/dotnet/api/system.console.writeline?view=net-10.0#system-console-writeline(system-string-system-object()) as a good example when params is useful. I am not seeing any value it is adding to the logger SG other than not having the caller to manually passing array like LogMessages.MSG(logger, "message", [1, 2, 3]);

In another word, in logging source gen, the number of message arguments is always known beforehand which defeat the params purpose.

@svick
Copy link
Member

svick commented Feb 20, 2026

@tarekgh

Logically (following C# concepts), this should be valid.

I see the message template to be on the same level as the body of the logging method. There, args is just an array.

So your example would be invalid, and I think it doesn't violate C# concepts.

Or, to be more technical, params means that the number of arguments is variable, but the number of parameters is always the same. And the message template is referencing parameters, not arguments. (This differentiates it from Console.WriteLine or the ILogger.Log methods, whose format strings do reference arguments.)

The C# spec even says:

Except for allowing a variable number of arguments in an invocation, a parameter array is precisely equivalent to a value parameter of the same type.

So I think making it precisely equivalent to a regular array parameter here is fine.


I am not seeing any value it is adding to the logger SG other than not having the caller to manually passing array like LogMessages.MSG(logger, "message", [1, 2, 3]);

That's fair. Personally, I think the value of params over using a collection expression is low, but not zero. So, if we can resolve the conceptual issues above, I don't see a reason not to allow it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Extensions-Logging NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Source generation error in LoggerMessageAttribute in Microsoft.Extensions.Logging.Abstractions

3 participants

Comments