Skip to Content
Course content

101: Code Analysis with Roslyn Analyzers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've probably spent plenty of time staring at those little lightbulbs and squiggly lines in Visual Studio. Most of those are provided by the built-in .NET analyzers, but as your projects grow and your team's coding standards get more specific, you'll find that the default rules aren't enough. That's where Roslyn comes in.

Roslyn is the .NET compiler platform, and the coolest part about it is that it exposes the compiler's internal "brain" as an API. We can write our own analyzers to enforce a specific rule across a whole codebase. Today, I want to show you how to build a custom analyzer that flags the use of DateTime.Now. In my experience, using local time in a backend service is a recipe for disaster once you deploy to a server in a different timezone. We should be using DateTime.UtcNow instead.

Scaffolding the Analyzer

To get started, you'll need the "Analyzer" project template. I'm going to create a class that inherits from DiagnosticAnalyzer. The core of any analyzer is the Initialize method, where we tell Roslyn exactly which parts of the code's syntax tree we want to watch. Since we are looking for a property access, we'll register an action to trigger whenever the compiler encounters a MemberAccessExpression.


[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UtcDateTimeAnalyzer : DiagnosticAnalyzer
{
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        
        // We want to look at every single member access (e.g., object.Property)
        context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.SimpleMemberAccessExpression);
    }
}

The "Naive" implementation mistake

Now, I'll write the AnalyzeNode method. My first instinct is to just check if the property being accessed is named "Now". It seems simple enough, right?


private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
    var memberAccess = (MemberAccessExpressionSyntax)context.Node;
    
    if (memberAccess.Name.Identifier.Text == "Now")
    {
        var diagnostic = Diagnostic.Create(Rule, memberAccess.Name.GetLocation());
        context.ReportDiagnostic(diagnostic);
    }
}

I ran this against a test project, and it was a disaster. My analyzer started flagging Order.Now, UserSession.Now, and every other property named "Now" in my entire domain model. I forgot that the syntax tree only tells me the name of the member, not its type. This is a common trap when you first start with Roslyn: confusing Syntax (the text) with Semantics (the meaning).

Adding Semantic Analysis

To fix this, I need to use the SemanticModel. This allows me to ask Roslyn, "I know this is called 'Now', but who actually owns this property?" I'll use the GetSymbolInfo method to ensure the property belongs to System.DateTime.


private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
    var memberAccess = (MemberAccessExpressionSyntax)context.Node;
    
    // Get the symbol information for the member being accessed
    var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess);
    var symbol = symbolInfo.Symbol;

    // Now we check if it's actually the DateTime.Now property
    if (symbol is IPropertySymbol propertySymbol && 
        propertySymbol.ContainingType.SpecialType == SpecialType.System_DateTime && 
        propertySymbol.Name == "Now")
    {
        var diagnostic = Diagnostic.Create(Rule, memberAccess.Name.GetLocation());
        context.ReportDiagnostic(diagnostic);
    }
}

That's much better. Now the analyzer ignores my domain models and only screams at me when I'm about to introduce a timezone bug.

Defining the Diagnostic Rule

Finally, the analyzer needs a DiagnosticDescriptor. This is essentially the "metadata" for the error—the ID, the severity, and the message the developer sees. I've set this as a Warning rather than an Error because, while DateTime.Now is usually a mistake, there are rare cases (like UI formatting) where it's actually intended.


private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
    id: "UTC001",
    title: "Use UtcNow instead of Now",
    message: "Use DateTime.UtcNow to avoid timezone-related bugs in the backend.",
    category: "Naming",
    defaultSeverity: DiagnosticSeverity.Warning,
    isEnabledByDefault: true);

When you build this project and reference it as an analyzer in your main project, you'll see that warning pop up in real-time as you type. It's like having a senior dev looking over your shoulder, but without the judgmental sighs.




📋 Practical Task

Build a "No Empty Catch Block" Analyzer

Empty catch blocks are a silent killer in production—they swallow exceptions and leave you guessing why a feature stopped working. Your task is to create a custom Roslyn Analyzer that detects empty catch blocks.

Requirements:

  • Register a syntax node action that targets CatchClauseSyntax.
  • Check if the Block inside the catch clause contains any statements.
  • If the block is empty, report a diagnostic with the ID CATCH001 and the message: "Empty catch blocks are not allowed. Either handle the exception or rethrow it."
  • Set the severity to DiagnosticSeverity.Error to ensure the build fails if an empty catch is left in the code.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.