Skip to content

Commit e1c62a5

Browse files
Copilotsvicktarekgh
authored
Logging source generator: support generic methods (lift SYSLIB1011) (#124638)
Generic `[LoggerMessage]` methods were unconditionally rejected with `SYSLIB1011`. Methods with type parameters are fully supportable via the struct-based code generation path. The only remaining restriction is `allows ref struct` (C# 13), since the generated struct stores params in fields and cannot hold ref-struct type arguments. ## Description ### Parser (`LoggerMessageGenerator.Parser.cs`) - Removed the `method.Arity > 0` → `SYSLIB1011` check - Added `LoggerMethodTypeParameter` / `LoggerMethodTypeParameterSpec` record types to carry type parameter names + constraints through the pipeline - `GetTypeParameterConstraints` serializes all constraint kinds: `class`/`class?`, `struct`, `unmanaged`, `notnull`, base/interface types (incl. nullable), `new()` - Preserves all `FullyQualifiedFormat.MiscellaneousOptions` (`EscapeKeywordIdentifiers | UseSpecialTypes`) when adding `IncludeNullableReferenceTypeModifier` — fixes a pre-existing bug on the same call for parameter types - Skips `IErrorTypeSymbol` constraint types (unresolvable in generated code) - `allows ref struct` detected via `Func<ITypeParameterSymbol, bool>` delegate compiled from `ITypeParameterSymbol.AllowsRefLikeType` via `Delegate.CreateDelegate` (compiles against all supported Roslyn versions; no per-call boxing) — emits `SYSLIB1011` - Renamed `DiagnosticDescriptors.LoggingMethodIsGeneric` → `LoggingMethodHasAllowsRefStructConstraint`; updated message + all XLF files (only changed entry marked `needs-translation`) ### Emitter (`LoggerMessageGenerator.Emitter.cs`) - `UseLoggerMessageDefine` excludes generic methods — the `Define` path caches a static delegate and cannot capture method type parameters - `GenStruct` emits the state struct as generic (`__M1Struct<T>`) with matching `where` constraints - `GenLogMethod` emits `<T, U>` and `where` clauses on the partial method signature - `GenHolder`/`Format` references parameterized with concrete type arguments - Replaced `GetTypeParameterList`/`GetTypeConstraints` (string-returning, allocating) with `GenTypeParameterList`/`GenTypeConstraints` writing directly to `_builder` - Removed `using System.Linq`; `IndexOf('{')` replaces `Contains('{')` (no allocating LINQ path on netstandard2.0) ### Incremental cache (`LoggerMessageGenerator.Roslyn4.0.cs`) - `FromSpec` round-trips `TypeParameters` through the incremental cache ### Docs - `docs/project/list-of-diagnostics.md` updated: SYSLIB1011 now describes the `allows ref struct` restriction - External learn.microsoft.com docs tracked in [dotnet/docs#51840](dotnet/docs#51840) ## Example ```csharp // Previously: SYSLIB1011. Now: works, no boxing of TCode. [LoggerMessage(0, LogLevel.Trace, "Received {Code} ({Length} bytes)")] public static partial void PacketReceived<TCode>(ILogger logger, TCode code, int length) where TCode : struct, Enum; ``` All constraint forms supported: `class`, `class?`, `struct`, `unmanaged`, `notnull`, base type, interface, nullable interface, `new()`. `allows ref struct` emits SYSLIB1011. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>Logging source generator should support generic methods (`SYSLIB1011` should be lifted)</issue_title> > <issue_description>From [this](https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator#log-method-constraints) section: > > > Logging methods cannot be generic. > > This seems to be an unnecessary restriction that also works against the whole notion of high-performance logging. > > I would like to be able to write a logging method like so: > > ```csharp > [LoggerMessage(0, LogLevel.Trace, "C -> S {EndPoint}: {Channel}:{Code} ({Length} bytes)")] > public static partial void PacketReceived<TCode>(ILogger logger, IPEndPoint endPoint, ConnectionChannel channel, TCode code, int length) > where TCode : struct, Enum; > ``` > > Here, the type argument for `TCode` depends on the value of `Channel`. Of course, I could make the logging method non-generic and accept an `Enum`-typed value, but then I get unconditional boxing... certainly not what I want when logging in a fairly hot path. Alternatively, I could duplicate the method for as many `TCode`s as I have (which is what I'll do for now), but this is not great either, for all the expected maintenance reasons. > > I think this restriction should just be lifted. Maybe there's a good reason for it, but if there is, it's not obvious to me.</issue_description> > > <agent_instructions>Implement and test this. Make sure that all of these are supported: multiple type parameters, multiple constraints, various kinds of constraints.</agent_instructions> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > <comment_new><author>@</author><body> > 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. > <details> > <summary>Issue Details</summary> > <hr /> > > From [this](https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator#log-method-constraints) section: > > > Logging methods cannot be generic. > > This seems to be an unnecessary restriction that also works against the whole notion of high-performance logging. > > I would like to be able to write a logging method like so: > > ```csharp > [LoggerMessage(0, LogLevel.Trace, "C -> S {EndPoint}: {Channel}:{Code} ({Length} bytes)")] > public static partial void PacketReceived<TCode>(ILogger logger, IPEndPoint endPoint, ConnectionChannel channel, TCode code, int length) > where TCode : struct, Enum; > ``` > > Here, the type argument for `TCode` depends on the value of `Channel`. Of course, I could make the logging method non-generic and accept an `Enum`-typed value, but then I get unconditional boxing... certainly not what I want when logging in a fairly hot path. Alternatively, I could duplicate the method for as many `TCode`s as I have (which is what I'll do for now), but this is not great either, for all the expected maintenance reasons. > > I think this restriction should just be lifted. Maybe there's a good reason for it, but if there is, it's not obvious to me. > > <table> > <tr> > <th align="left">Author:</th> > <td>alexrp</td> > </tr> > <tr> > <th align="left">Assignees:</th> > <td>-</td> > </tr> > <tr> > <th align="left">Labels:</th> > <td> > > `untriaged`, `area-Extensions-Logging` > > </td> > </tr> > <tr> > <th align="left">Milestone:</th> > <td>-</td> > </tr> > </table> > </details></body></comment_new> > <comment_new><author>@cincuranet</author><body> > Triage: Niche scenario, but would be nice to have it one day.</body></comment_new> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #90589 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 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](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: svick <287848+svick@users.noreply.github.com> Co-authored-by: Petr Onderka <petronderka@microsoft.com> Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
1 parent b5fef80 commit e1c62a5

24 files changed

Lines changed: 531 additions & 77 deletions

docs/project/list-of-diagnostics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL
137137
| __`SYSLIB1008`__ | One of the arguments to a logging method must implement the Microsoft.Extensions.Logging.ILogger interface |
138138
| __`SYSLIB1009`__ | Logging methods must be static |
139139
| __`SYSLIB1010`__ | Logging methods must be partial |
140-
| __`SYSLIB1011`__ | Logging methods cannot be generic |
140+
| __`SYSLIB1011`__ | Logging methods cannot use the `allows ref struct` constraint |
141141
| __`SYSLIB1012`__ | Redundant qualifier in logging message |
142142
| __`SYSLIB1013`__ | Don't include exception parameters as templates in the logging message |
143143
| __`SYSLIB1014`__ | Logging template has no corresponding method argument |

src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/DiagnosticDescriptors.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ public static class DiagnosticDescriptors
8181
DiagnosticSeverity.Error,
8282
isEnabledByDefault: true);
8383

84-
public static DiagnosticDescriptor LoggingMethodIsGeneric { get; } = DiagnosticDescriptorHelper.Create(
84+
public static DiagnosticDescriptor LoggingMethodHasAllowsRefStructConstraint { get; } = DiagnosticDescriptorHelper.Create(
8585
id: "SYSLIB1011",
86-
title: new LocalizableResourceString(nameof(SR.LoggingMethodIsGenericMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)),
87-
messageFormat: new LocalizableResourceString(nameof(SR.LoggingMethodIsGenericMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)),
86+
title: new LocalizableResourceString(nameof(SR.LoggingMethodHasAllowsRefStructConstraintMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)),
87+
messageFormat: new LocalizableResourceString(nameof(SR.LoggingMethodHasAllowsRefStructConstraintMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)),
8888
category: "LoggingGenerator",
8989
DiagnosticSeverity.Error,
9090
isEnabledByDefault: true);

src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Linq;
76
using System.Text;
87
using System.Threading;
98
using Microsoft.CodeAnalysis;
@@ -60,6 +59,7 @@ public string Emit(IReadOnlyList<LoggerClass> logClasses, CancellationToken canc
6059
private static bool UseLoggerMessageDefine(LoggerMethod lm)
6160
{
6261
bool result =
62+
(lm.TypeParameters.Count == 0) && // generic methods can't use LoggerMessage.Define's static callback
6363
(lm.TemplateParameters.Count <= MaxLoggerMessageDefineArguments) && // more args than LoggerMessage.Define can handle
6464
(lm.Level != null) && // dynamic log level, which LoggerMessage.Define can't handle
6565
(lm.TemplateList.Count == lm.TemplateParameters.Count); // mismatch in template to args, which LoggerMessage.Define can't handle
@@ -146,11 +146,15 @@ namespace {lc.Namespace}
146146

147147
private void GenStruct(LoggerMethod lm, string nestedIndentation)
148148
{
149-
_builder.AppendLine($@"
149+
_builder.Append($@"
150150
{nestedIndentation}/// {GeneratedTypeSummary}
151151
{nestedIndentation}[{s_generatedCodeAttribute}]
152152
{nestedIndentation}[{EditorBrowsableAttribute}]
153-
{nestedIndentation}private readonly struct __{lm.UniqueName}Struct : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>
153+
{nestedIndentation}private readonly struct __{lm.UniqueName}Struct");
154+
GenTypeParameterList(lm);
155+
_builder.Append($" : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>");
156+
GenTypeConstraints(lm, nestedIndentation + " ");
157+
_builder.AppendLine($@"
154158
{nestedIndentation}{{");
155159
GenFields(lm, nestedIndentation);
156160

@@ -175,7 +179,7 @@ private void GenStruct(LoggerMethod lm, string nestedIndentation)
175179
GenVariableAssignments(lm, nestedIndentation);
176180

177181
string formatMethodBegin =
178-
!lm.Message.Contains('{') ? "" :
182+
lm.Message.IndexOf('{') < 0 ? "" :
179183
_hasStringCreate ? "string.Create(global::System.Globalization.CultureInfo.InvariantCulture, " :
180184
"global::System.FormattableString.Invariant(";
181185
string formatMethodEnd = formatMethodBegin.Length > 0 ? ")" : "";
@@ -185,7 +189,9 @@ private void GenStruct(LoggerMethod lm, string nestedIndentation)
185189
{nestedIndentation}}}
186190
");
187191
_builder.Append($@"
188-
{nestedIndentation}public static readonly global::System.Func<__{lm.UniqueName}Struct, global::System.Exception?, string> Format = (state, ex) => state.ToString();
192+
{nestedIndentation}public static readonly global::System.Func<__{lm.UniqueName}Struct");
193+
GenTypeParameterList(lm);
194+
_builder.Append($@", global::System.Exception?, string> Format = (state, ex) => state.ToString();
189195
190196
{nestedIndentation}public int Count => {lm.TemplateParameters.Count + 1};
191197
@@ -369,9 +375,9 @@ private void GenArguments(LoggerMethod lm)
369375

370376
private void GenHolder(LoggerMethod lm)
371377
{
372-
string typeName = $"__{lm.UniqueName}Struct";
373-
374-
_builder.Append($"new {typeName}(");
378+
_builder.Append($"new __{lm.UniqueName}Struct");
379+
GenTypeParameterList(lm);
380+
_builder.Append('(');
375381
foreach (LoggerParameter p in lm.TemplateParameters)
376382
{
377383
if (p != lm.TemplateParameters[0])
@@ -385,6 +391,44 @@ private void GenHolder(LoggerMethod lm)
385391
_builder.Append(')');
386392
}
387393

394+
private void GenTypeParameterList(LoggerMethod lm)
395+
{
396+
if (lm.TypeParameters.Count == 0)
397+
{
398+
return;
399+
}
400+
401+
_builder.Append('<');
402+
bool firstItem = true;
403+
foreach (LoggerMethodTypeParameter tp in lm.TypeParameters)
404+
{
405+
if (firstItem)
406+
{
407+
firstItem = false;
408+
}
409+
else
410+
{
411+
_builder.Append(", ");
412+
}
413+
414+
_builder.Append(tp.Name);
415+
}
416+
417+
_builder.Append('>');
418+
}
419+
420+
private void GenTypeConstraints(LoggerMethod lm, string nestedIndentation)
421+
{
422+
foreach (LoggerMethodTypeParameter tp in lm.TypeParameters)
423+
{
424+
if (tp.Constraints is not null)
425+
{
426+
_builder.Append(@$"
427+
{nestedIndentation}where {tp.Name} : {tp.Constraints}");
428+
}
429+
}
430+
}
431+
388432
private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
389433
{
390434
string level = GetLogLevel(lm);
@@ -414,11 +458,15 @@ private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
414458

415459
_builder.Append($@"
416460
{nestedIndentation}[{s_generatedCodeAttribute}]
417-
{nestedIndentation}{lm.Modifiers} void {lm.Name}({extension}");
461+
{nestedIndentation}{lm.Modifiers} void {lm.Name}");
462+
GenTypeParameterList(lm);
463+
_builder.Append($"({extension}");
418464

419465
GenParameters(lm);
420466

421-
_builder.Append($@")
467+
_builder.Append(')');
468+
GenTypeConstraints(lm, nestedIndentation);
469+
_builder.Append($@"
422470
{nestedIndentation}{{");
423471

424472
string enabledCheckIndentation = lm.SkipEnabledCheck ? "" : " ";
@@ -448,7 +496,9 @@ private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
448496
GenHolder(lm);
449497
_builder.Append($@",
450498
{nestedIndentation}{enabledCheckIndentation}{exceptionArg},
451-
{nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Struct.Format);");
499+
{nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Struct");
500+
GenTypeParameterList(lm);
501+
_builder.Append(".Format);");
452502
}
453503

454504
if (!lm.SkipEnabledCheck)

src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ internal sealed class Parser
2222
{
2323
internal const string LoggerMessageAttribute = "Microsoft.Extensions.Logging.LoggerMessageAttribute";
2424

25+
// ITypeParameterSymbol.AllowsRefLikeType was added in Roslyn 4.9 (C# 13). Access via a compiled
26+
// delegate so the same source file compiles against all supported Roslyn versions, while
27+
// avoiding the per-call overhead of PropertyInfo.GetValue boxing.
28+
private static readonly Func<ITypeParameterSymbol, bool>? s_getAllowsRefLikeType =
29+
(Func<ITypeParameterSymbol, bool>?)
30+
typeof(ITypeParameterSymbol).GetProperty("AllowsRefLikeType")?.GetGetMethod()!.CreateDelegate(typeof(Func<ITypeParameterSymbol, bool>));
31+
2532
private readonly CancellationToken _cancellationToken;
2633
private readonly INamedTypeSymbol _loggerMessageAttribute;
2734
private readonly INamedTypeSymbol _loggerSymbol;
@@ -239,8 +246,29 @@ public IReadOnlyList<LoggerClass> GetLogClasses(IEnumerable<ClassDeclarationSynt
239246
SkipEnabledCheck = skipEnabledCheck
240247
};
241248

249+
foreach (ITypeParameterSymbol tp in logMethodSymbol.TypeParameters)
250+
{
251+
lm.TypeParameters.Add(new LoggerMethodTypeParameter
252+
{
253+
Name = tp.Name,
254+
Constraints = GetTypeParameterConstraints(tp)
255+
});
256+
}
257+
242258
bool keepMethod = true; // whether or not we want to keep the method definition or if it's got errors making it so we should discard it instead
243259

260+
// Forbid 'allows ref struct': the code generator stores template parameters as
261+
// fields in a generated struct, so ref-struct type arguments cannot be supported.
262+
foreach (ITypeParameterSymbol tp in logMethodSymbol.TypeParameters)
263+
{
264+
if (s_getAllowsRefLikeType?.Invoke(tp) == true)
265+
{
266+
Diag(DiagnosticDescriptors.LoggingMethodHasAllowsRefStructConstraint, method.Identifier.GetLocation());
267+
keepMethod = false;
268+
break;
269+
}
270+
}
271+
244272
bool success = ExtractTemplates(message, lm.TemplateMap, lm.TemplateList);
245273
if (!success)
246274
{
@@ -263,13 +291,6 @@ public IReadOnlyList<LoggerClass> GetLogClasses(IEnumerable<ClassDeclarationSynt
263291
keepMethod = false;
264292
}
265293

266-
if (method.Arity > 0)
267-
{
268-
// we don't currently support generic methods
269-
Diag(DiagnosticDescriptors.LoggingMethodIsGeneric, method.Identifier.GetLocation());
270-
keepMethod = false;
271-
}
272-
273294
bool isStatic = false;
274295
bool isPartial = false;
275296
foreach (SyntaxToken mod in method.Modifiers)
@@ -400,6 +421,7 @@ public IReadOnlyList<LoggerClass> GetLogClasses(IEnumerable<ClassDeclarationSynt
400421

401422
string typeName = paramTypeSymbol.ToDisplayString(
402423
SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
424+
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions |
403425
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier));
404426

405427
var lp = new LoggerParameter
@@ -745,6 +767,48 @@ private static string GenerateClassName(TypeDeclarationSyntax typeDeclaration)
745767
return (loggerField, false);
746768
}
747769

770+
private static string? GetTypeParameterConstraints(ITypeParameterSymbol typeParameter)
771+
{
772+
var constraints = new List<string>();
773+
774+
if (typeParameter.HasReferenceTypeConstraint)
775+
{
776+
string classConstraint = typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
777+
? "class?"
778+
: "class";
779+
constraints.Add(classConstraint);
780+
}
781+
else if (typeParameter.HasValueTypeConstraint)
782+
{
783+
// HasUnmanagedTypeConstraint also implies HasValueTypeConstraint
784+
constraints.Add(typeParameter.HasUnmanagedTypeConstraint ? "unmanaged" : "struct");
785+
}
786+
else if (typeParameter.HasNotNullConstraint)
787+
{
788+
constraints.Add("notnull");
789+
}
790+
791+
foreach (ITypeSymbol constraintType in typeParameter.ConstraintTypes)
792+
{
793+
if (constraintType is IErrorTypeSymbol)
794+
{
795+
continue;
796+
}
797+
798+
constraints.Add(constraintType.ToDisplayString(
799+
SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
800+
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions |
801+
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)));
802+
}
803+
804+
if (typeParameter.HasConstructorConstraint)
805+
{
806+
constraints.Add("new()");
807+
}
808+
809+
return constraints.Count > 0 ? string.Join(", ", constraints) : null;
810+
}
811+
748812
private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs)
749813
{
750814
// Report immediately if callback is provided (preserves pragma suppression with original locations)
@@ -949,6 +1013,7 @@ internal sealed class LoggerMethod
9491013
public readonly List<LoggerParameter> TemplateParameters = new();
9501014
public readonly Dictionary<string, string> TemplateMap = new(StringComparer.OrdinalIgnoreCase);
9511015
public readonly List<string> TemplateList = new();
1016+
public readonly List<LoggerMethodTypeParameter> TypeParameters = new();
9521017
public string Name = string.Empty;
9531018
public string UniqueName = string.Empty;
9541019
public string Message = string.Empty;
@@ -966,6 +1031,7 @@ internal sealed class LoggerMethod
9661031
TemplateParameters = TemplateParameters.Select(p => p.ToSpec()).ToImmutableEquatableArray(),
9671032
TemplateMap = TemplateMap.Select(kvp => new KeyValuePairEquatable<string, string>(kvp.Key, kvp.Value)).ToImmutableEquatableArray(),
9681033
TemplateList = TemplateList.ToImmutableEquatableArray(),
1034+
TypeParameters = TypeParameters.Select(tp => tp.ToSpec()).ToImmutableEquatableArray(),
9691035
Name = Name,
9701036
UniqueName = UniqueName,
9711037
Message = Message,
@@ -988,6 +1054,7 @@ internal sealed record LoggerMethodSpec : IEquatable<LoggerMethodSpec>
9881054
public required ImmutableEquatableArray<LoggerParameterSpec> TemplateParameters { get; init; }
9891055
public required ImmutableEquatableArray<KeyValuePairEquatable<string, string>> TemplateMap { get; init; }
9901056
public required ImmutableEquatableArray<string> TemplateList { get; init; }
1057+
public required ImmutableEquatableArray<LoggerMethodTypeParameterSpec> TypeParameters { get; init; }
9911058
public required string Name { get; init; }
9921059
public required string UniqueName { get; init; }
9931060
public required string Message { get; init; }
@@ -1007,6 +1074,7 @@ public bool Equals(LoggerMethodSpec? other)
10071074
TemplateParameters.Equals(other.TemplateParameters) &&
10081075
TemplateMap.Equals(other.TemplateMap) &&
10091076
TemplateList.Equals(other.TemplateList) &&
1077+
TypeParameters.Equals(other.TypeParameters) &&
10101078
Name == other.Name &&
10111079
UniqueName == other.UniqueName &&
10121080
Message == other.Message &&
@@ -1025,6 +1093,7 @@ public override int GetHashCode()
10251093
hash = HashHelpers.Combine(hash, TemplateParameters.GetHashCode());
10261094
hash = HashHelpers.Combine(hash, TemplateMap.GetHashCode());
10271095
hash = HashHelpers.Combine(hash, TemplateList.GetHashCode());
1096+
hash = HashHelpers.Combine(hash, TypeParameters.GetHashCode());
10281097
hash = HashHelpers.Combine(hash, Name.GetHashCode());
10291098
hash = HashHelpers.Combine(hash, UniqueName.GetHashCode());
10301099
hash = HashHelpers.Combine(hash, Message.GetHashCode());
@@ -1133,6 +1202,44 @@ public override int GetHashCode()
11331202
}
11341203
}
11351204

1205+
/// <summary>
1206+
/// A type parameter of a generic logging method.
1207+
/// </summary>
1208+
internal sealed class LoggerMethodTypeParameter
1209+
{
1210+
public string Name = string.Empty;
1211+
public string? Constraints;
1212+
1213+
public LoggerMethodTypeParameterSpec ToSpec() => new LoggerMethodTypeParameterSpec
1214+
{
1215+
Name = Name,
1216+
Constraints = Constraints
1217+
};
1218+
}
1219+
1220+
/// <summary>
1221+
/// Immutable specification of a type parameter for incremental caching.
1222+
/// </summary>
1223+
internal sealed record LoggerMethodTypeParameterSpec : IEquatable<LoggerMethodTypeParameterSpec>
1224+
{
1225+
public required string Name { get; init; }
1226+
public required string? Constraints { get; init; }
1227+
1228+
public bool Equals(LoggerMethodTypeParameterSpec? other)
1229+
{
1230+
if (other is null) return false;
1231+
if (ReferenceEquals(this, other)) return true;
1232+
return Name == other.Name && Constraints == other.Constraints;
1233+
}
1234+
1235+
public override int GetHashCode()
1236+
{
1237+
int hash = Name.GetHashCode();
1238+
hash = HashHelpers.Combine(hash, Constraints?.GetHashCode() ?? 0);
1239+
return hash;
1240+
}
1241+
}
1242+
11361243
/// <summary>
11371244
/// Returns a non-randomized hash code for the given string.
11381245
/// We always return a positive value.

src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Roslyn4.0.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,15 @@ private static LoggerClass FromSpec(LoggerClassSpec spec)
259259
lm.TemplateList.Add(template);
260260
}
261261

262+
foreach (var typeParamSpec in methodSpec.TypeParameters)
263+
{
264+
lm.TypeParameters.Add(new LoggerMethodTypeParameter
265+
{
266+
Name = typeParamSpec.Name,
267+
Constraints = typeParamSpec.Constraints
268+
});
269+
}
270+
262271
lc.Methods.Add(lm);
263272
}
264273

0 commit comments

Comments
 (0)