dotnet/extensions

`Microsoft.Extensions.Http.Resilience` breaks `LoggerMessage` code generation.

hugoqribeiro opened this issue ยท 20 comments

Description

We have a .NET 8 project where we add a reference to a package that includes a dependency on Microsoft.Extensions.Http.Resilience. That project also uses [LoggerMessage] to take advantage of the source generator.

That project also includes a reference to a package (.NET Standard 2.0) that is using il-repack, which results in all the types under Microsoft.Extensions.Logging (including Microsoft.Extensions.Logging.LoggerMessageAttribute) being included in the assembly as internal types.

In this scenario, the source generator does not work.

[LoggerMessage(Level = LogLevel.Information, Message = "Something")]
private static partial void LogSomething(ILogger logger);

The error is:

CS8795	Partial method 'Program.LogSomething(ILogger)' must have an implementation part because it has accessibility modifiers.

Investigating this, I believe that the problem is related with how Microsoft.Gen.Logging.LoggingGenerator.LoggingGenerator is looking up for Microsoft.Extensions.Logging.LoggerMessageAttribute.

I think that Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator.Parser did that lookup considering the visibility of the types:

INamedTypeSymbol bestTypeByMetadataName = _compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerMessageAttribute");

I'm not 100% sure, but it looks like Microsoft.Gen.Logging.LoggingGenerator.LoggingGenerator isn't considering visibility. Which results in the code not being generated.

Reproduction Steps

Here's the csproj definition:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Cegid.Hydrogen.Hosting" Version="8.0.5" />
        <PackageReference Include="Cegid.Lithium.Settings.Client.Rest.SingleAssembly" Version="6.0.0" />
    </ItemGroup>

</Project>

Cegid.Hydrogen.Hosting is the package that depends on Microsoft.Extensions.Http.Resilience.
Cegid.Lithium.Settings.Client.Rest.SingleAssembly is the package that uses il-repack and has LoggerMessageAttribute as an internal type.

capture

Expected behavior

The source generator should work as expected, because there is only one public type accessible named Microsoft.Extensions.Logging.LoggerMessageAttribute.

Actual behavior

The source generator does not kick-in.

Regression?

No.

Known Workarounds

Adding this in the csproj, after the package reference, makes the old source generator work:

<PropertyGroup>
   <DisableMicrosoftExtensionsLoggingSourceGenerator>false</DisableMicrosoftExtensionsLoggingSourceGenerator>
</PropertyGroup>

Obviously, this does not work if we remove the reference to the single assembly, as BOTH source generators kick-in and we end up with 2 implementations of the logging method.

Configuration

We're using .NET 8.0.4 and Microsoft.Extensions.Http.Resilience 8.4.0.
Nothing else seams relevant for this issue.

Other information

No response

Can you provide a minimal reproduction of this issue using il-repack? It would help with investigations.

You mean a simplified package after being processed with il-repack?

I believe that I can reproduce this without il-repack, if I add an internal type named Microsoft.Extensions.Logging.LoggerMessageAttribute in the original project...

Will that help?

With il-repack, it can be configured to not internalize specific types. Do you know if this is possible to use in your case?

 - /internalize[:<excludefile>]  sets all types but the ones from the first assembly 'internal'. <excludefile> contains one regex per
                                 line to compare against FullName of types NOT to internalize.

With il-repack, it can be configured to not internalize specific types. Do you know if this is possible to use in your case?

 - /internalize[:<excludefile>]  sets all types but the ones from the first assembly 'internal'. <excludefile> contains one regex per
                                 line to compare against FullName of types NOT to internalize.

I'm unsure that having 2 public types with the same name will solve the issue.
What I've tried was renaming Microsoft.Extensions.Logging to Cegid.Microsoft.Extensions.Logging, but that breaks the package at runtime (because il-repack is unable to do the replaces everywhere, for some reason that I didn't understand (yet).

Let me try reproducing the issue without il-repack...

Can you provide a minimal reproduction of this issue using il-repack? It would help with investigations.

OK. This is reproducible by referencing a simple client library.

Here's a solution that reproduces the problem:

Repo: https://github.com/hugoqribeiro/issues
Solution: https://github.com/hugoqribeiro/issues/tree/main/src/2024.05.Microsoft.Extensions.Http.Resilience

Thanks for the repro

I'm unsure that having 2 public types with the same name will solve the issue.

There's also this /union but not sure how that will look either without actually trying it.

To add to this: I'm having a similar issue with the visibility of an inherited ILogger property.

We're using the Azure Mobile Apps SDK from Microsoft. This SDK has a base class for a controller which you can inherit from, containing a public property of type ILogger:

namespace Microsoft.AspNetCore.Datasync
{
    ...
    public class TableController<TEntity> : ControllerBase where TEntity : class, ITableData
    {
        ...
        public ILogger Logger { get; set; } = null;
        ...
    }
}

I inherited from that class as intended by the Azure Mobile Apps SDK and in my class have a more specific ILogger<T> property as follows:

public partial class InspectionSyncController : TableController<FarmInspectionSyncModel>
{
    private readonly ILogger<InspectionSyncController> logger;

    public InspectionSyncController(
        ...
        ILogger<InspectionSyncController> logger)
        : base(...)
    {
        ...
        this.logger = logger;
    }
    ...
    [LoggerMessage(
    EventId = 0,
    Level = LogLevel.Warning,
    Message = "Not authorized to perform operation `{Operation}` on entity `{EntityId}`")]
    private partial void LogNotAuthorizedToPerformOperationOnEntity(TableOperation operation, string? entityId);
    ...
}

This works without Microsoft.Extensions.Http.Resilience.

Actual behavior

After adding Microsoft.Extensions.Http.Resilience, I get this warning:

Found multiple fields or properties of type "Microsoft.Extensions.Logging.ILogger" in type "InspectionSyncController" (https://aka.ms/dotnet-extensions-warnings/LOGGEN016).

Expected behavior

It still works after adding Microsoft.Extensions.Http.Resilience

Hi @dariusclay? No news on this issue?

It keeps coming back as more and more of our projects that the dependency that references Microsoft.Extensions.Http.Resilience.

@hansmbakker your case is a little different than the one in the original post. When including the Microsoft.Extensions.Http.Resilience package, it replaces the .NET logging generator with the .NET extensions logging generator. This has slightly different behavior as seen with diagnostic warning. You can get around this by providing an ILogger parameter to the method and making it static:

public partial class InspectionSyncController : TableController<FarmInspectionSyncModel>
{
    private readonly ILogger<InspectionSyncController> logger;

    public InspectionSyncController(
        ...
        ILogger<InspectionSyncController> logger)
        : base(...)
    {
        ...
        this.logger = logger;
    }
    ...
    [LoggerMessage(
    EventId = 0,
    Level = LogLevel.Warning,
    Message = "Not authorized to perform operation `{Operation}` on entity `{EntityId}`")]
    private static partial void LogNotAuthorizedToPerformOperationOnEntity(ILogger logger, TableOperation operation, string? entityId);
    ...
}

@geeknoid should we consider this a bug and bring parity to the way the .NET generator works? To only use the most derived logger member?

@hugoqribeiro we can alter the logic to search for a public attribute. Although, I am curious if using the il-repack options internalize + union works for you?

@dariusclay thank you for the explanation and the workaround!
Even though there may be a good reason for it, I was surprised to read

it replaces the .NET logging generator with the .NET extensions logging generator. This has slightly different behavior as seen with diagnostic warning.

A lot of people will be getting the Microsoft.Extensions.Http.Resilience package as part of adding Aspire.NET orchestration to their solution, and are not notified of this behavior change.

Can you please move our comments to a new issue? I can only recreate my own comments but not move yours (I did not intend to hijack @hugoqribeiro's issue, I thought they our issues were symptoms of the same cause).

To the original discussion -- I've narrowed down the issue and created a unit test that captures the problem during symbol loading. Will publish a PR with the changes soon, and we can discuss on the review. @geeknoid

@hugoqribeiro note that this will occur if any of the following types are internalized by il-repack

"Microsoft.Extensions.Logging.LoggerMessageAttribute"
"Microsoft.Extensions.Logging.LogPropertiesAttribute"
"Microsoft.Extensions.Logging.TagProviderAttribute"
"Microsoft.Extensions.Logging.TagNameAttribute"
"Microsoft.Extensions.Logging.LogPropertyIgnoreAttribute"
"Microsoft.Extensions.Logging.ITagCollector"
"Microsoft.Extensions.Logging.ILogger"
"Microsoft.Extensions.Logging.LogLevel"
"Microsoft.Extensions.Compliance.Classification.DataClassificationAttribute"
"Microsoft.Extensions.Compliance.Classification.NoDataClassificationAttribute"

@dariusclay, thanks. I'm waiting for the guy that implemented the il-repack thing to come back from vacations. :)

Hi @dariusclay, we tested union and it doesn't solve the issue. There is still an internal ILogger that causes the issue.

I see you merged a fix into main. Does that mean that it will be released in 8.x anytime soon?

Thanks.

@hugoqribeiro Yes, this will be part of the next monthly snapshot.

@hugoqribeiro can you test the fix using our nightlies? I did a test run using my local repro but it will be good to know if it works in the wild.

Update your nuget package sources to include our public nightly feed:
https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json

Add a reference to this package:
<PackageReference Include="Microsoft.Extensions.Telemetry.Abstractions" Version="8.8.0-preview.24413.3" />