如何让编译器提醒使用ILogger的自定义扩展方法Here()
解决方案:用自定义Roslyn分析器实现编译器警告
要实现这种基于调用模式的编译器警告,必须借助Roslyn静态代码分析器——它能在编译阶段检测代码结构,匹配指定模式后触发自定义警告。
步骤1:创建Roslyn分析器项目
在Visual Studio中创建「Analyzer with Code Fix (.NET Standard)」项目,该模板会自动生成分析器和代码修复的基础框架。
步骤2:编写分析器逻辑
替换自动生成的Analyzer1.cs代码,实现针对Serilog ILogger直接调用的检测:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; namespace LoggerUsageAnalyzer { [DiagnosticAnalyzer(LanguageNames.CSharp)] public class LoggerDirectCallAnalyzer : DiagnosticAnalyzer { // 自定义警告ID,可按需修改 public const string DiagnosticId = "LOG001"; private static readonly LocalizableString Title = "直接调用ILogger日志方法"; private static readonly LocalizableString MessageFormat = "请使用Here()扩展方法后再调用日志方法,例如:{0}.Here().{1}(...)"; private static readonly LocalizableString Description = "直接调用ILogger的日志方法会缺失上下文信息,建议先调用Here()扩展方法。"; private const string Category = "Usage"; private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // 监听所有方法调用节点 context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression); } private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) { var invocationExpr = (InvocationExpressionSyntax)context.Node; // 1. 检查是否是ILogger的实例方法调用(如Information、Debug等) var memberAccess = invocationExpr.Expression as MemberAccessExpressionSyntax; if (memberAccess == null) return; var methodSymbol = context.SemanticModel.GetSymbolInfo(memberAccess.Name).Symbol as IMethodSymbol; if (methodSymbol == null) return; // 验证方法所属类型为Serilog.ILogger var containingType = methodSymbol.ContainingType; if (containingType?.ToString() != "Serilog.ILogger") return; // 2. 检查调用链是否包含Here()扩展方法 var currentExpression = memberAccess.Expression; bool hasHereCall = false; // 向上遍历调用链,确认是否存在Here()调用 while (currentExpression is InvocationExpressionSyntax chainInvocation) { var chainMemberAccess = chainInvocation.Expression as MemberAccessExpressionSyntax; if (chainMemberAccess?.Name.Identifier.Text == "Here") { hasHereCall = true; break; } currentExpression = chainMemberAccess?.Expression; } // 未调用Here()则触发警告 if (!hasHereCall) { var diagnostic = Diagnostic.Create(Rule, invocationExpr.GetLocation(), memberAccess.Expression.ToString(), methodSymbol.Name); context.ReportDiagnostic(diagnostic); } } } }
步骤3:(可选)添加一键修复功能
修改自动生成的CodeFixProvider1.cs,让开发者可以一键添加.Here()调用:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace LoggerUsageAnalyzer { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(LoggerDirectCallCodeFixProvider)), Shared] public class LoggerDirectCallCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(LoggerDirectCallAnalyzer.DiagnosticId); public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; // 定位触发警告的调用节点 var invocationExpr = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf() .OfType<InvocationExpressionSyntax>().First(); // 注册修复动作 context.RegisterCodeFix( CodeAction.Create( title: "添加Here()调用", createChangedDocument: c => AddHereCall(context.Document, invocationExpr, c), equivalenceKey: nameof(LoggerDirectCallCodeFixProvider)), diagnostic); } private async Task<Document> AddHereCall(Document document, InvocationExpressionSyntax invocationExpr, CancellationToken cancellationToken) { var memberAccess = (MemberAccessExpressionSyntax)invocationExpr.Expression; // 在原实例与日志方法之间插入.Here()调用 var newMemberAccess = memberAccess.WithExpression( SyntaxFactory.InvocationExpression( SyntaxFactory.MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, memberAccess.Expression, SyntaxFactory.IdentifierName("Here")))); var newInvocationExpr = invocationExpr.WithExpression(newMemberAccess); var root = await document.GetSyntaxRootAsync(cancellationToken); var newRoot = root.ReplaceNode(invocationExpr, newInvocationExpr); return document.WithSyntaxRoot(newRoot); } } }
步骤4:部署并使用分析器
- 编译分析器项目,右键选择「打包」生成NuGet包
- 在业务项目中引用该NuGet包
- 当直接调用
Log.Information(...)时,编译器会触发LOG001警告;Log.Here().Information(...)则不会触发警告
关键说明
- 分析器仅针对Serilog.ILogger的日志方法,不会误报其他类型的方法调用
- 调用链检查会忽略中间的其他方法,只要链中存在
Here()就不会触发警告 - 可修改分析器代码中的
DiagnosticSeverity,将警告改为错误以强制要求修复
内容的提问来源于stack exchange,提问作者Tri
相关产品推荐
相关产品推荐

