Skip to content

LoggerMessage source generator: preserve ref readonly; forbid params, scoped, and ref struct parameters#124589

Merged
svick merged 14 commits intomainfrom
copilot/fix-source-generation-error
Feb 25, 2026
Merged

LoggerMessage source generator: preserve ref readonly; forbid params, scoped, and ref struct parameters#124589
svick merged 14 commits intomainfrom
copilot/fix-source-generation-error

Conversation

Copy link
Contributor

Copilot AI commented Feb 19, 2026

The LoggerMessage source generator dropped the ref readonly modifier from generated implementations (causing CS0758), and silently accepted params, scoped, and ref struct parameters that either produce confusing semantics or fail to compile when stored in the generated >6-parameter struct.

Description

  • Parser

    • Preserve ref readonly: use RefKind.RefReadOnlyParameter under #if ROSLYN4_8_OR_GREATER, fall back to (RefKind)4
    • Emit SYSLIB1028 (InvalidLoggingMethodParameterParams) for params or scoped parameters (scoped detection guarded by #if ROSLYN4_4_OR_GREATER)
    • Emit SYSLIB1029 (InvalidLoggingMethodParameterRefStruct) for any ref struct parameter (paramTypeSymbol.IsRefLikeType), including ref ReadOnlySpan<T>, scoped ReadOnlySpan<T>, etc.
    • Both new checks run before typeName allocation to avoid unnecessary string work on failure paths
  • Emitter: emit in/ref/ref readonly qualifier correctly in generated parameter list

  • Resources: Strings.resx + all 13 xlf files updated with SYSLIB1028 and SYSLIB1029 message strings

  • Docs: docs/project/list-of-diagnostics.md updated with SYSLIB1028 and SYSLIB1029 entries

  • Tests

    • RefReadOnlyParameterOK, RefReadOnlyParameterWithMoreThan6ParamsOK — verify modifier is preserved
    • InvalidParamsParameter, InvalidParamsCollectionParameter — SYSLIB1028 for params T[] and params IEnumerable<T>
    • InvalidScopedRefParameter, InvalidScopedRefReadOnlyParameter — SYSLIB1028 for scoped ref T variants
    • InvalidRefLikeTypeParameter, InvalidScopedValueParameter — SYSLIB1029 for ref struct parameters
    • Emitter baseline test TestBaseline_TestWithRefReadOnlyParam_Success
// ref readonly is now preserved in the generated implementation
[LoggerMessage(Message = "{p}", Level = LogLevel.Info)]
static partial void Log(ILogger l, ref readonly int p);
// → ref readonly global::System.Int32 p  ✓

// params and scoped are now compile-time errors (SYSLIB1028)
static partial void Log(ILogger l, params object?[] args);
static partial void Log(ILogger l, scoped ref int p);

// ref struct parameters are now a compile-time error (SYSLIB1029)
static partial void Log(ILogger l, ref MyRefStruct s);
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 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
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
@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.

@tarekgh
Copy link
Member

tarekgh commented Feb 20, 2026

@svick

You are missing important point here. Either we claim we support params or not. As soon as we claim we support it, then you have to address all scenarios supported by params. You cannot say we are partially supported to help users passing individual values of the array instead of passing array. The next ask will come is why we don't fully support it allowing variable number of message arguments? I hope you are seeing where I am heading with that. It is simple and clearer we say we don't support params with the LoggerMessage because it is not adding much value and set the right expectation to the users. Otherwise, with current proposal it is very confusing from my point of view.

@tarekgh
Copy link
Member

tarekgh commented Feb 20, 2026

@copilot does the support of ref readonly and scoped will work with logger messages accept more than 7 parameters? If so, please add a test for that to validate this scenario.

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

tarekgh commented Feb 23, 2026

@copilot please address the test failure (when targeting NET481).

C:\h\w\9CF3089C\w\9958089D\e>xunit.console.exe Microsoft.Extensions.Logging.Generators.Roslyn4.8.Tests.dll -xml testResults.xml -nologo -nocolor -notrait category=IgnoreForCI -notrait category=OuterLoop -notrait category=failing  
  Discovering: Microsoft.Extensions.Logging.Generators.Roslyn4.8.Tests (app domain = on [no shadow copy], method display = ClassAndMethod, method display options = None)
  Discovered:  Microsoft.Extensions.Logging.Generators.Roslyn4.8.Tests (found 112 of 113 test cases)
  Starting:    Microsoft.Extensions.Logging.Generators.Roslyn4.8.Tests (parallel test collections = on [2 threads], stop on fail = off)
    Microsoft.Extensions.Logging.Generators.Tests.LoggerMessageGeneratorParserTests.InvalidScopedValueParameter [FAIL]
      Assert.Single() Failure: The collection was empty
      Stack Trace:
        /_/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs(988,0): at Microsoft.Extensions.Logging.Generators.Tests.LoggerMessageGeneratorParserTests.<InvalidScopedValueParameter>d__57.MoveNext()
        --- End of stack trace from previous location where exception was thrown ---
           at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
           at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
        --- End of stack trace from previous location where exception was thrown ---
           at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
           at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
        --- End of stack trace from previous location where exception was thrown ---
           at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
           at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
    Microsoft.Extensions.Logging.Generators.Tests.LoggerMessageGeneratorParserTests.InvalidRefLikeTypeParameter [FAIL]
      Assert.Single() Failure: The collection was empty
      Stack Trace:
        /_/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs(1004,0): at Microsoft.Extensions.Logging.Generators.Tests.LoggerMessageGeneratorParserTests.<InvalidRefLikeTypeParameter>d__58.MoveNext()
        --- End of stack trace from previous location where exception was thrown ---
           at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
           at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
        --- End of stack trace from previous location where exception was thrown ---
           at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
           at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
        --- End of stack trace from previous location where exception was thrown ---
           at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
           at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
  Finished:    Microsoft.Extensions.Logging.Generators.Roslyn4.8.Tests

Copy link
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot please address the test failure (when targeting NET481).

...

Fixed in 7ef8ebf. The root cause: scoped ReadOnlySpan<int> (a ScopedValue parameter) is only semantically valid when Roslyn can confirm the type is a ref struct. On net481, if ReadOnlySpan<int> isn't recognized as a ref struct from the available netstandard/System.Runtime references, Roslyn marks it as an error type — hitting the IErrorTypeSymbol early-exit in the parser before SYSLIB1029 can fire.

Both InvalidScopedValueParameter and InvalidRefLikeTypeParameter now define their own ref struct MyRefStruct { } directly in the test source, guaranteeing IsRefLikeType == true regardless of available BCL assemblies.

@svick
Copy link
Member

svick commented Feb 24, 2026

Now that I think about it, the scoped modifier is only useful for ref struct types, which are not supported, so there is no reason to support it.

@copilot Remove the support for scoped added in this PR and instead produce the SYSLIB1028 diagnostic. Update that diagnostic and any mention of it appropriately.

cc: @tarekgh

Copy link
Contributor Author

Copilot AI commented Feb 24, 2026

Now that I think about it, the scoped modifier is only useful for ref struct types, which are not supported, so there is no reason to support it.

@copilot Remove the support for scoped added ...

Done in 05f16d8. Removed scoped support entirely — IsScoped field, emitter emission, and Roslyn4.0.cs propagation are all gone. scoped parameters now emit SYSLIB1028 (same as params), and the diagnostic message/title has been updated to "unsupported params or scoped parameter modifier". All "ScopedRef*OK" parser tests are converted to error tests, the TestBaseline_TestWithScopedRefParam_Success emitter test and its baseline file are removed, and UseScopedRefParameter is removed from ParameterTestExtensions.cs.

Copilot AI changed the title LoggerMessage source generator: preserve ref readonly and scoped modifiers; forbid params; diagnose ref struct parameters LoggerMessage source generator: preserve ref readonly; forbid params, scoped, and ref struct parameters Feb 24, 2026
Copilot AI requested a review from svick February 24, 2026 10:23
Copy link
Member

@tarekgh tarekgh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, Thanks @svick!

CC @evgenyfedorov2

@svick svick merged commit 9b312a6 into main Feb 25, 2026
94 of 96 checks passed
@svick svick deleted the copilot/fix-source-generation-error branch February 25, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants