ASP.NET Core middleware with Roslyn Analyzers - Part 1
ASP.NET Core middleware is a clever way to configure features like authentication, routing, and error logging in ASP.NET Core. Adding the different pieces of middleware and in the right order can be quite a nightmare, though. In this post, I'll show you how Roslyn Analyzers can help.
Middleware works like pearls on a string. The order you add your middleware determines which middleware is called first and which is called last. This can cause serious problems, like error logging middleware that is never notified of the errors happening. The problem is already nicely described in Andrew Lock's Inserting middleware between UseRouting() and UseEndpoints() as a library author - Part 1 and Part 2, so make sure to read through those if you have felt the pain in your projects already.
I wanted to create a couple of Roslyn Analyzers to help elmah.io users insert the call to UseElmahIo
and in the right place. Analyzers are small pieces of C# code running inside Visual Studio that analyzes the project source code and report any problems to the Visual Studio Error List window.
To create new Roslyn Analyzers, the simplest approach is to base it on the template already available in Visual Studio. Create a new project and select the Analyzer with Code Fix (.NET Standard) template (install the .NET Compiler Platform SDK from the Visual Studio Installer if not already installed). This generates a new project with a MakeConst analyzer and code fix. You can delete the two C# files since we will create new ones for this demo.
Let's start by creating an analyzer that verifies that the UseElmahIo
method is even added to the Configure
method in the Startup.cs
file. I'm creating a new C# class named CallUseElmahIoAnalyzer.cs
. I've pretty much copied the basic structure from the MakeConst
analyzer previously there:
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CallUseElmahIoAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "EIO1001";
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
DiagnosticId,
"Configure must call UseElmahIo",
"Configure must call UseElmahIo",
"Elmah.Io.CSharp.AspNetCoreRules",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
public override void Initialize(AnalysisContext context)
{
}
}
There are a few things to notice. Analyzers declare and extend the DiagnosticAnalyzer
class. This will make Visual Studio pick up your analyzer automatically once installed as a VSIX (Visual Studio extension). Metadata for the analyzer is declared as a DiagnosticDescriptor
and includes an id, the message to show to the user, as well as a couple of other things. The Initialize
method is where the magic happens.
Analyzers are implemented using a visitor pattern. You register handlers for code pieces you are interested in and Roslyn automatically parses the source code under analysis and provides your code with metadata. The code may look a bit weird and over-engineered at first, but turns out pretty strong once you get acquainted with the model.
For this first analyzer, we want to see if the Configure
method calls the UseElmahIo
method. For this simple example, we assume that the call should be made inside a method named Configure
in a class named Startup
:
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;
});
}
The action provided for the RegisterCodeBlocStartAction
method is called by Roslyn every time a new code block begins. A code block can be a class, a field, a method, etc. By inspecting metadata, the example returns if the current callback doesn't fulfill the requirements already mentioned.
If the code hasn't returned after the last if
statement, we know that we are currently inside the Configure
method. Roslyn allows for nested syntax actions, why we can register a new callback:
public override void Initialize(AnalysisContext context)
{
context.RegisterCodeBlockStartAction<SyntaxKind>(cb =>
{
// the code from the previous example
bool useElmahIoInvocationFound = false;
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 (methodName == "UseElmahIo") useElmahIoInvocationFound = true;
}, SyntaxKind.InvocationExpression);
});
}
The new code registers a syntax node action to get a notification of each invocation expression inside the Configure
method (by specifying SyntaxKind.InvocationExpression
as a parameter to the RegisterSyntaxNodeAction
method). On each syntax node, we check if it's a method call and if the method call is UseElmahIo
. In case we find a call to UseElmahIo
we set the useElmahIoInvocationFound
boolean to true.
The last thing missing is to register an end action. When using the RegisterCodeBlockStartAction
method, you need to provide a callback for Roslyn to call when on the end of the semantic analysis for the method analyzed (in this case Configure
). Confused? Let's look at the code:
public override void Initialize(AnalysisContext context)
{
context.RegisterCodeBlockStartAction<SyntaxKind>(cb =>
{
// the code from the previous example
cb.RegisterCodeBlockEndAction(ctx =>
{
if (!useElmahIoInvocationFound)
{
var diag = Diagnostic.Create(Rule, method.Locations[0]);
ctx.ReportDiagnostic(diag);
}
});
});
}
In case the useElmahIoInvocationFound
boolean is still set to false
we use the ReportDiagnostic
method to show a warning inside Visual Studio. The metadata used for the warning is taken from the Rule
field that we already discussed as well as the location of the Configure
method.
Let's look at the entire Initialize
method:
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;
bool useElmahIoInvocationFound = false;
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 (methodName == "UseElmahIo") useElmahIoInvocationFound = true;
}, SyntaxKind.InvocationExpression);
cb.RegisterCodeBlockEndAction(ctx =>
{
if (!useElmahIoInvocationFound)
{
var diag = Diagnostic.Create(Rule, method.Locations[0]);
ctx.ReportDiagnostic(diag);
}
});
});
}
To test the new 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. If no UseElmahIo
call is made, you should see the warning directly in Visual Studio:
To distribute your new analyzer, either upload the generated VSIX file to the Visual Studio Marketplace or publish the project containing the analyzers as a NuGet package.
That's it! In the next post, I will continue the example by creating an analyzer that verifies the correct order of middleware.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website