You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让编译器提醒使用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:部署并使用分析器

  1. 编译分析器项目,右键选择「打包」生成NuGet包
  2. 在业务项目中引用该NuGet包
  3. 当直接调用Log.Information(...)时,编译器会触发LOG001警告;Log.Here().Information(...)则不会触发警告

关键说明

  • 分析器仅针对Serilog.ILogger的日志方法,不会误报其他类型的方法调用
  • 调用链检查会忽略中间的其他方法,只要链中存在Here()就不会触发警告
  • 可修改分析器代码中的DiagnosticSeverity,将警告改为错误以强制要求修复

内容的提问来源于stack exchange,提问作者Tri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 13:48:22