ASP.NET Core middleware with Roslyn Analyzers - Part 2
Tags: dotnet, dotnetcore, csharp, aspnetcore
Published: 2021-09-22 06:53:27

---
title: ASP.NET Core middleware with Roslyn Analyzers - Part 2
published: true
description: Learn how to create a Roslyn analyzer that helps your users insert middleware in the Configure method of the Startup.cs file and in the right order.
tags: dotnet, dotnetcore, csharp, aspnetcore
canonical_url: https://blog.elmah.io/asp-net-core-middleware-with-roslyn-analyzers-part-2/
---

This is the second post in the series about Roslyn analyzers with ASP.NET Core middleware. If you haven't read [part 1](https://dev.to/thomasardal/asp-net-core-middleware-with-roslyn-analyzers-part-1-4g0l) already, go ahead a do that for a general introduction. In this post, I'll continue the example from the first post and start looking at the order middleware are added.

Setting up middleware often isn't enough for everything to work properly. The order in which different middleware components are added is important for almost all features in ASP.NET Core. As mentioned in the previous post, middleware works like pearls on a string. The order you add your middleware determines which middleware is called first and which is called last. If you add exception handling middleware too soon or too late, important errors in the pipeline, are never picked up and handled.

Let's create a new analyzer that validates the right placement of a middleware component. Again, I'm using the elmah.io middleware, but that's because I already wrote the code for that. You can implement this for any middleware.

The code will go into a new analyzer, why I create a new file named `UseElmahIoOrderAnalyzer.cs`. The overall structure looks similar to the analyzer from the previous post:

```csharp
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UseElmahIoOrderAnalyzer : DiagnosticAnalyzer
{
    public const string DiagnosticId = "EIO1002";

    private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
        DiagnosticId,
        "UseElmahIo must be called in the right order",
        "UseElmahIo must be called after {1}",
        "Elmah.Io.CSharp.AspNetCoreRules",
        DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public override void Initialize(AnalysisContext context)
        {
        }
    }
```

I changed two things: the analyzer id (`DiagnosticId`) and the text messages inside the `DiagnosticDescriptor`. Notice how the second message is no longer simply a hard-coded string. Analyzers support formatting too, which we will dig into in a bit.

The analyzer should only run on the method named `Configure` in classes named `Startup`. Let's repeat how the first lines from the `Initialize` method can decide if the analyzer should run or not:

```csharp
context.RegisterCodeBlockStartAction<SyntaxKind>(cb =>
{
    if (cb.OwningSymbol.Kind != SymbolKind.Method) return;
    var method = (IMethodSymbol)cb.OwningSymbol;
    // We only care about methods named ConfigureServices
    if (method.Name != "Configure") return;
    if (method.ContainingType.Name != "Startup") return;

    // ...
});
```

With that out of the way, let's start implementing the rule. For the `UseElmahIo` method, I want to make sure that it is called after any calls to `UseDeveloperExceptionPage` or `UseExceptionHandler`. To implement this in the most simple way I can imagine, my analyzer will iterate through the `Configure` method and remember all calls starting with `Use` and in the end, verify that the order is correct:

```csharp
Location useElmahIoLocation = null;
List<string> calls = new List<string>();
cb.RegisterSyntaxNodeAction(ctx =>
{
    var node = ctx.Node as InvocationExpressionSyntax;
    if (node == null) return;
    var expression = node.Expression as MemberAccessExpressionSyntax;
    if (expression == null) return;
    var methodName = expression.Name?.Identifier.ValueText;
    if (string.IsNullOrWhiteSpace(methodName) || !methodName.StartsWith("Use")) return;
    calls.Add(methodName);
    if (methodName.Equals("UseElmahIo")) useElmahIoLocation = expression.GetLocation();
}, SyntaxKind.InvocationExpression);
```

I register a syntax node action once we are inside the `Configure` method. If you lost track of where this code should be placed you can relax. The full code is available later in this post 😃 The code stores any method invocations starting with `Use`. It also saves the location of the `UseElmahIo` method, which we will use when reporting the warning.

Once we are out of scope of the `Configure` method, we can inspect the invoked methods:

```csharp
cb.RegisterCodeBlockEndAction(ctx =>
{
    // EIO1001 will catch this
    if (!calls.Contains("UseElmahIo")) return;

    var index = calls.IndexOf("UseElmahIo");
    var useDeveloperExceptionPageIndex = calls.IndexOf("UseDeveloperExceptionPage");
    var useExceptionHandlerIndex = calls.IndexOf("UseExceptionHandler");
    
    if (useDeveloperExceptionPageIndex != -1 && index < useDeveloperExceptionPageIndex)
        Report(ctx, useElmahIoLocation, "UseDeveloperExceptionPage");
    else if (useExceptionHandlerIndex != -1 && index < useExceptionHandlerIndex)
        Report(ctx, useElmahIoLocation, "UseExceptionHandler");
});
```

The `Report` method should look familiar to what you've already seen, so let's jump in:

```csharp
private void Report(CodeBlockAnalysisContext context, Location location, string use)
{
    var diag = Diagnostic.Create(Rule, location, use);
    context.ReportDiagnostic(diag);
}
```

The method reports the warning to Visual Studio. Depending on which method name was sent by the analyzer, the `Diagnostic.Create` method replaces `{0}` in the message with the method name provided by the analyzer (`UseDeveloperExceptionPage` or `UseExceptionHandler`).

Let's look at the entire `Initialize` method:

```csharp
public override void Initialize(AnalysisContext context)
{
    context.RegisterCodeBlockStartAction<SyntaxKind>(cb =>
    {
        // We only care about method bodies.
        if (cb.OwningSymbol.Kind != SymbolKind.Method) return;
        var method = (IMethodSymbol)cb.OwningSymbol;
        // We only care about methods named ConfigureServices
        if (method.Name != "Configure") return;
        if (method.ContainingType.Name != "Startup") return;
        Location useElmahIoLocation = null;

        List<string> calls = new List<string>();

        cb.RegisterSyntaxNodeAction(ctx =>
        {
            var node = ctx.Node as InvocationExpressionSyntax;
            if (node == null) return;
            var expression = node.Expression as MemberAccessExpressionSyntax;
            if (expression == null) return;
            var methodName = expression.Name?.Identifier.ValueText;
            if (string.IsNullOrWhiteSpace(methodName) || !methodName.StartsWith("Use")) return;
            calls.Add(methodName);
            if (methodName.Equals("UseElmahIo")) useElmahIoLocation = expression.GetLocation();
        }, SyntaxKind.InvocationExpression);

        cb.RegisterCodeBlockEndAction(ctx =>
        {
            // EIO1001 will catch this
            if (!calls.Contains("UseElmahIo")) return;

            var index = calls.IndexOf("UseElmahIo");

            var useDeveloperExceptionPageIndex = calls.IndexOf("UseDeveloperExceptionPage");
            var useExceptionHandlerIndex = calls.IndexOf("UseExceptionHandler");
            var useAuthorizationIndex = calls.IndexOf("UseAuthorization");

            if (useDeveloperExceptionPageIndex != -1 && index < useDeveloperExceptionPageIndex)
                Report(ctx, useElmahIoLocation, "UseDeveloperExceptionPage");
            else if (useExceptionHandlerIndex != -1 && index < useExceptionHandlerIndex)
                Report(ctx, useElmahIoLocation, "UseExceptionHandler");
        });
    });
}
```

To test the analyzer, make sure to set the VSIX project as the startup project and hit F5. This will start an experimental instance of Visual Studio. Create a new ASP.NET Core project or open an existing one and navigate to the `Startup` class. When adding the `UseElmahIo` call before `UseDeveloperExceptionPage` the warning is shown in Visual Studio:

![](https://blog.elmah.io/content/images/2020/02/useelmahio-after-exceptionpage.png)

Multiple analyzers can be distributed in the same VSIX and/or NuGet package. Visual Studio automatically picks up all analyzers in installed packages.

That's it! The next step is for you to start experimenting with Roslyn Analyzers for your middleware. The full source code for the elmah.io Roslyn Analyzers for ASP.NET Core can be found here: https://github.com/elmahio/Elmah.Io.AspNetCore.Analyzers

## Would your users appreciate fewer errors?

elmah.io is the easy error logging and uptime monitoring service for .NET. Take back control of your errors with support for all .NET web and logging frameworks.

➡️ [Error Monitoring for .NET Web Applications](https://elmah.io/?utm_source=devto&utm_medium=social&utm_campaign=devtoposts) ⬅️

This article first appeared on the elmah.io blog at https://blog.elmah.io/asp-net-core-middleware-with-roslyn-analyzers-part-2/