Search code examples
c#visual-studiomicrosoft-extensions-logging

How does Visual Studio know about Microsoft Logging placeholders?


In a .NET C# application with Microsoft logging, one will have logging statements like the following, with a message that may include brace-delimited placeholders followed by an ordered sequence of values to fill the placeholders with:

  logger.LogInformation("{numRequests} orders for {item} were received in the past hour", 47, "doughnuts");

If the number of parameters after the message parameter differs from the number of placeholders, Visual Studio, flags the discrepancy. How does Visual Studio know?

  • Is Visual Studio hard-coded to recognize Microsoft Logging statements and apply special treatment to them? In other words, not something I could replicate when defining a method of my own where I'd like this to occur.
  • Or is there a generally available way of declaring a string parameter as a template and associating it with a parameter array? Could I use this in a method declaration of my own?

Yes, I know about formattable strings, but I'm curious about what's behind the way it works in the logging methods, without formattable strings.


Solution

  • LoggingAnalyzer.cs

    using System.Collections.Immutable;
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.Diagnostics;
    using Microsoft.CodeAnalysis.CSharp;
    using Microsoft.CodeAnalysis.CSharp.Syntax;
    using System.Linq;
    using System.Text.RegularExpressions;
     
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class LoggingAnalyzer : DiagnosticAnalyzer
    {
        private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
            "LOG001",
            "Logging placeholder mismatch",
            "The number of placeholders in the message does not match the number of arguments",
            "Logging",
            DiagnosticSeverity.Error,
            isEnabledByDefault: true);
     
        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 invocation = (InvocationExpressionSyntax)context.Node;
            var memberAccess = invocation.Expression as MemberAccessExpressionSyntax;
     
            if (memberAccess == null || !memberAccess.Name.ToString().StartsWith("Log"))
                return;
     
            var arguments = invocation.ArgumentList.Arguments;
            if (arguments.Count < 2)
                return;
     
            var messageArgument = arguments[0];
            var messageExpression = messageArgument.Expression as LiteralExpressionSyntax;
     
            if (messageExpression == null || messageExpression.Kind() != SyntaxKind.StringLiteralExpression)
                return;
     
            var message = messageExpression.Token.ValueText;
            int placeholderCount = Regex.Matches(message, @"\{[^}]+\}").Count;
            int argumentCount = arguments.Count - 1;
     
            if (placeholderCount != argumentCount)
            {
                var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation());
                context.ReportDiagnostic(diagnostic);
            }
        }
    }
    

    Program.cs

    using System;
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.CSharp;
    using Microsoft.CodeAnalysis.Diagnostics;
    using System.Linq;
    using System.Reflection;
    using System.Text.RegularExpressions;
    using System.Collections.Immutable;
     
    class Program
    {
        static void Main(string[] args)
        {
            string code = @"
                using Microsoft.Extensions.Logging;
     
                public class TestClass
                {
                    private ILogger<TestClass> _logger;
     
                    public TestClass(ILogger<TestClass> logger)
                    {
                        _logger = logger;
                    }
     
                    public void TestMethod()
                    {
                        _logger.LogInformation("" orders for {item} were received in the past hour"", 47, ""doughnuts"");
                    }
                }";
     
            var tree = CSharpSyntaxTree.ParseText(code);
            var compilation = CSharpCompilation.Create("TestAssembly", new[] { tree });
     
            var analyzer = new LoggingAnalyzer();
            var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(analyzer);
     
            var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
            var diagnostics = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
     
            foreach (var diagnostic in diagnostics)
            {
                Console.WriteLine(diagnostic);
            }
        }
    }