Skip to content

Commit 20a1d18

Browse files
authored
Added the syntax highlighting for template messages in generic Log* and BeginScope overloads for Jetbrains tools (#27)
1 parent 42deaec commit 20a1d18

16 files changed

+485
-1413
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
### Title: ADR-08 Message Template Syntax Highlighting
2+
3+
### Status: Accepted
4+
5+
### Context:
6+
7+
When writing log messages using structured templates (like `"User {UserId} logged in"`), it's good to have some syntax highlighting to see if it's a raw string literal or a template parameter. IDE support could help a lot here by offering syntax highlighting out of the box.
8+
9+
There are two main options we looked at:
10+
11+
1. **`[SyntaxAttribute]` from Microsoft** — this is supported in Visual Studio and JetBrains Rider.
12+
2. **`[StructuredMessageTemplate]` from `JetBrains.Annotations`** — works only in JetBrains tools, like Rider and ReSharper.
13+
14+
`SyntaxAttribute` seemed promising at first, but we hit a few issues:
15+
16+
* It’s only available starting from **.NET 7**, and even though our target app is .NET 8+ (because we're using interceptors), source generators themselves still need to be compiled against `netstandard2.0` for Visual Studio compatibility. So, we’d have to wrap every usage in `#if NET8_0_OR_GREATER`, which would make the code messy.
17+
* The only `SyntaxAttribute` that somewhat matches our case is `SyntaxAttribute(SyntaxKind.CompositeFormat)`, which was designed for `string.Format`-style messages. That doesn't map well to structured logging templates, which can use named placeholders like `{UserId}` — not just `{0}`, `{1}`, etc.
18+
19+
Then we looked at `StructuredMessageTemplate` from `JetBrains.Annotations`. It's not built into the .NET SDK, but:
20+
21+
* It’s a tiny library with **no extra dependencies**.
22+
* It targets **.NET Standard 1.0**, so it works fine with our source generator build setup.
23+
24+
The downside is that it’s **only useful in Rider/ReSharper** — Visual Studio (Code) won’t recognize it. But that’s fine: it’s better than nothing, and we shouldn't introduce any breakage.
25+
26+
### Decision:
27+
28+
We’re going to add `[StructuredMessageTemplate]` from the `JetBrains.Annotations` package to mark message template parameters in the generated logging code.
29+
30+
Specifically:
31+
32+
* Add the `[StructuredMessageTemplate]` attribute to all generated `message` parameters in `Log*` and `BeginScope` generic overloads.
33+
* Pull in the latest version of `JetBrains.Annotations` as a compile-time-only dependency.
34+
35+
### Consequences:
36+
37+
* **Short-term**:
38+
* Improved developer experience in Rider/ReSharper.
39+
* Helps catch common mistakes in message templates at design time.
40+
41+
* **Long-term**:
42+
* No negative impact for Visual Studio users, but they won’t benefit from it either.
43+
* Keeps the generic overloads clean and tidy.
44+
45+
* **Risks**:
46+
* Adds a soft dependency (though very minimal and safe).
47+
48+
* **Maintenance**:
49+
* Easy to keep up — JetBrains.Annotations is stable and tiny.
50+
* If Microsoft adds a better-suited `SyntaxAttribute` in future versions, we can revisit this.
51+
52+
### Alternatives Considered:
53+
54+
* **Use `SyntaxAttribute(SyntaxKind.CompositeFormat)`**:
55+
Clean and native, but doesn’t fit well in our use case at this moment.
56+
* **Do nothing**:
57+
Simple, but leaves developers without any IDE help for message templates, which is a missed opportunity.

src/AutoLoggerMessageGenerator.BuildOutput/AutoLoggerMessageGenerator.BuildOutput.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8+
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
89
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" PrivateAssets="all"/>
910
</ItemGroup>
1011
</Project>

src/AutoLoggerMessageGenerator.BuildOutput/GenericLoggerExtensions.g.cs

Lines changed: 198 additions & 197 deletions
Large diffs are not rendered by default.

src/AutoLoggerMessageGenerator.BuildOutput/GenericLoggerScopeExtensions.g.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,42 @@
22
#nullable enable
33

44
using System;
5+
using JetBrains.Annotations;
56

67
namespace Microsoft.Extensions.Logging
78
{
8-
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.0.10.0")]
9+
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("AutoLoggerMessageGenerator", "1.0.12.0")]
910
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]
1011
[System.Diagnostics.DebuggerStepThrough]
1112
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
1213
public static class GenericLoggerScopeExtensions
1314
{
14-
public static IDisposable? BeginScope<T0>(this ILogger @logger, string @message, T0 @arg0)
15+
public static IDisposable? BeginScope<T0>(this ILogger @logger, [StructuredMessageTemplate] string @message, T0 @arg0)
1516
{
1617
return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0 });
1718
}
1819

19-
public static IDisposable? BeginScope<T0, T1>(this ILogger @logger, string @message, T0 @arg0, T1 @arg1)
20+
public static IDisposable? BeginScope<T0, T1>(this ILogger @logger, [StructuredMessageTemplate] string @message, T0 @arg0, T1 @arg1)
2021
{
2122
return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1 });
2223
}
2324

24-
public static IDisposable? BeginScope<T0, T1, T2>(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2)
25+
public static IDisposable? BeginScope<T0, T1, T2>(this ILogger @logger, [StructuredMessageTemplate] string @message, T0 @arg0, T1 @arg1, T2 @arg2)
2526
{
2627
return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2 });
2728
}
2829

29-
public static IDisposable? BeginScope<T0, T1, T2, T3>(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3)
30+
public static IDisposable? BeginScope<T0, T1, T2, T3>(this ILogger @logger, [StructuredMessageTemplate] string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3)
3031
{
3132
return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3 });
3233
}
3334

34-
public static IDisposable? BeginScope<T0, T1, T2, T3, T4>(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4)
35+
public static IDisposable? BeginScope<T0, T1, T2, T3, T4>(this ILogger @logger, [StructuredMessageTemplate] string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4)
3536
{
3637
return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3, @arg4 });
3738
}
3839

39-
public static IDisposable? BeginScope<T0, T1, T2, T3, T4, T5>(this ILogger @logger, string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4, T5 @arg5)
40+
public static IDisposable? BeginScope<T0, T1, T2, T3, T4, T5>(this ILogger @logger, [StructuredMessageTemplate] string @message, T0 @arg0, T1 @arg1, T2 @arg2, T3 @arg3, T4 @arg4, T5 @arg5)
4041
{
4142
return Microsoft.Extensions.Logging.LoggerExtensions.BeginScope(@logger, @message, new object?[] { @arg0, @arg1, @arg2, @arg3, @arg4, @arg5 });
4243
}

src/AutoLoggerMessageGenerator/Constants.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ internal static class Constants
5151
$"\"{typeof(Generators.AutoLoggerMessageGenerator).Assembly.GetName().Version}\")]";
5252
public const string EditorNotBrowsableAttribute = "[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)]";
5353

54-
public static string GeneratedFileHeader => """
54+
public const string GeneratedFileHeader = """
5555
// <auto-generated/>
5656
#nullable enable
5757
5858
using System;
59-
6059
""";
60+
public const string JetBrainsAnnotationsImport = "using JetBrains.Annotations;";
61+
public const string MessageTemplateDecorator = "[StructuredMessageTemplate]";
6162
}

src/AutoLoggerMessageGenerator/Emitters/GenericLoggerExtensionsEmitter.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public static string Emit()
1818
using var sb = new IndentedTextWriter(new StringWriter());
1919

2020
sb.WriteLine(GeneratedFileHeader);
21+
sb.WriteLine(JetBrainsAnnotationsImport);
22+
sb.WriteLine();
2123

2224
sb.WriteLine($"namespace {DefaultLoggingNamespace}");
2325
sb.WriteLine('{');
@@ -33,9 +35,9 @@ public static string Emit()
3335

3436
string[] logLevels = ["Trace", "Debug", "Information", "Warning", "Error", "Critical"];
3537

36-
var messageParameter = ("string", MessageArgumentName: MessageParameterName);
37-
var exceptionParameter = ("Exception?", ExceptionArgumentName: ExceptionParameterName);
38-
var eventIdParameter = ("EventId", EventIdArgumentName: EventIdParameterName);
38+
var messageParameter = ($"{MessageTemplateDecorator} string", MessageParameterName);
39+
var exceptionParameter = ("Exception?", ExceptionParameterName);
40+
var eventIdParameter = ("EventId", EventIdParameterName);
3941

4042
(string Type, string Name)[][] fixedParametersOverloads =
4143
[

src/AutoLoggerMessageGenerator/Emitters/GenericLoggerScopeExtensionsEmitter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public static string Emit()
1818
using var sb = new IndentedTextWriter(new StringWriter());
1919

2020
sb.WriteLine(GeneratedFileHeader);
21+
sb.WriteLine(JetBrainsAnnotationsImport);
22+
sb.WriteLine();
2123

2224
sb.WriteLine($"namespace {DefaultLoggingNamespace}");
2325
sb.WriteLine('{');
@@ -51,7 +53,7 @@ public static string Emit()
5153
? string.Empty
5254
: $", new object?[] {{ {objectParameters} }}";
5355

54-
sb.WriteLine($"public static IDisposable? BeginScope{genericTypesDefinition}(this ILogger {LoggerParameterName}, string {MessageParameterName}{genericParametersDefinition})");
56+
sb.WriteLine($"public static IDisposable? BeginScope{genericTypesDefinition}(this ILogger {LoggerParameterName}, {MessageTemplateDecorator} string {MessageParameterName}{genericParametersDefinition})");
5557
sb.WriteLine('{');
5658
sb.Indent++;
5759

src/AutoLoggerMessageGenerator/Emitters/InterceptorAttributeEmitter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public static string Emit()
99
using var sb = new IndentedTextWriter(new StringWriter());
1010

1111
sb.WriteLine(Constants.GeneratedFileHeader);
12+
sb.WriteLine();
1213

1314
sb.WriteLine($"namespace {Constants.InterceptorNamespace}");
1415
sb.WriteLine('{');

src/AutoLoggerMessageGenerator/Emitters/LoggerInterceptorsEmitter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public static string Emit(IEnumerable<LogMessageCall> logCalls)
1010
using var sb = new IndentedTextWriter(new StringWriter());
1111

1212
sb.WriteLine(Constants.GeneratedFileHeader);
13+
sb.WriteLine();
1314

1415
sb.WriteLine($"namespace {Constants.GeneratorNamespace}");
1516
sb.WriteLine('{');

src/AutoLoggerMessageGenerator/Emitters/LoggerScopeInterceptorsEmitter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public static string Emit(IEnumerable<LoggerScopeCall> loggerScopes)
1010
using var sb = new IndentedTextWriter(new StringWriter());
1111

1212
sb.WriteLine(Constants.GeneratedFileHeader);
13+
sb.WriteLine();
1314

1415
sb.WriteLine($"namespace {Constants.GeneratorNamespace}");
1516
sb.WriteLine('{');

0 commit comments

Comments
 (0)