Building Web API Visual Studio support tools with Roslyn

Β· 1447 words Β· 7 minutes to read

In my humble opinion, Microsoft Roslyn is one of the most exciting things on the .NET stack. One of the many (MANY) things you can do easily with Roslyn, is write your own development-time code analysis tools.

We have talked about Roslyn scripting capabilities on this blog before (twice actually). Let’s look at code analysis today and see how we could built tools that could help Web API developers build nice clean HTTP services.

More after the jump.

Background πŸ”—

I was recently called by Tugberk “a designated community crazy posts guy,” which I hope was a compliment πŸ™‚ I guess here comes one.

So I had this idea that perhaps Roslyn could make it easier for the devs to embrace HTTP standards. What Roslyn allows us to write, are refactoring tools, which would scan the code as it’s being written and hint about possible changes and fixes (just like Resharper does). You can think of the Roslyn API as reflection, only available prior to compilation, in real time during code writing.

Obviously this is applicable to any code base, not just Web API - we’ll just use Web API context as an example which we’ll create here.

Our Web API Roslyn refactoring tool πŸ”—

In this example, we will create a code issue provider, which will find all methods that are:

  • Inside an ApiController
  • are responding to POST requests
  • are void

In a typical RESTful API, POST on a resource would result in creation of a resource, and most often it would mean the client should get a 201 response (it’s not a rule, but as I said, that’s by far most common). Web API automatically issues 204 for void methods.

So we will underline the return type with a little warning, infomring the developer that he should consider changing the status code. Additionally, we will provide a refactoring action - just like Resharper does - when on click, the method will be re-written according to our suggestion.

Getting started with Roslyn πŸ”—

I already provided a brief introduction to Roslyn the last time we used it here, so I will not duplicate that, in short you need to install Roslyn CTP. You can get it here.

Once you got everything in place, create a new project, using the template Code Issue.

CodeIssue πŸ”—

Initially we need to implement an ICodeIssueProvider. This will find the errors as the developer writes code, and allow us to underline it.

[ExportCodeIssueProvider("WebApi.CI", LanguageNames.CSharp)]  
class CodeIssueProvider : ICodeIssueProvider  
{  
public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken)  
{  
var typedNode = (ClassDeclarationSyntax) node;  
if (!typedNode.BaseList.Types.Any(x => x.GetFirstToken().ValueText == "ApiController")) yield break;

var methods = typedNode.Members.OfType<MethodDeclarationSyntax>().  
Where(x => (x.Identifier.ValueText.StartsWith("Post") || x.AttributeLists.Any(a => a.Attributes.Any(s => s.Name.ToString() == "HttpPost"))) &&  
x.ReturnType.GetType().IsAssignableFrom(typeof(PredefinedTypeSyntax)) && ((PredefinedTypeSyntax)x.ReturnType).Keyword.Kind == SyntaxKind.VoidKeyword);

foreach (var method in methods)  
{  
yield return new CodeIssue(CodeIssueKind.Warning, method.ReturnType.Span,  
"Consider returning a 201 Status Code");  
}  
}

public IEnumerable<Type> SyntaxNodeTypes  
{  
get  
{  
yield return typeof(ClassDeclarationSyntax);  
}  
}

public IEnumerable<CodeIssue> GetIssues(IDocument document, CommonSyntaxToken token, CancellationToken cancellationToken)  
{  
throw new NotImplementedException();  
}

public IEnumerable<int> SyntaxTokenKinds  
{  
get  
{  
return null;  
}  
}  
}  
    1. SyntaxNodeTypes property, allows us to export only specific syntax types from the document we analyze - in our case, we export classes - ClassDeclarationSyntax.
    1. We check if the class inherits from ApiController - if not, we cease further processing.
    1. We export all the methods (MethodDeclarationSyntax) which meet our predefined conditions. For example we not only select Post methods based on the name, but also based on HttpPostAttribute. Void is represented in Roslyn as SyntaxKind.VoidKeyword so we can easily test against such return type.
    1. Finally, we issue a warning for the developer, indicating that it should span over the method return type - which means it will underline the void keyword.

This already works - we can run the application - when you debug Roslyn solution it will actually start a 2nd VS instance (after all your solution is going to be an extension to VS), where you can open any project and verify whether your CodeIssue works correctly.

If we run it against a WebAPI project we get a nice result, as expected:

But of course, that’s just a warning now. We still need a second piece of the puzzle, Code Action, which will provide an option for the developer to automatically rewrite his code according to the warning’s suggestion.

CodeAction πŸ”—

This time we implement ICodeAction. This class will be capable of rewriting the entire document the developer is currently working on.

By default we need to just implement a single method, GetEdit. Interestingly it doesn’t take any useful parameters, so we pass whatever we need through a custom constructor.

class CodeAction : ICodeAction  
{  
private readonly IDocument _document;  
private readonly MethodDeclarationSyntax _method;

public CodeAction(IDocument document, MethodDeclarationSyntax method)  
{  
_document = document;  
_method = method;  
}

public CodeActionEdit GetEdit(CancellationToken cancellationToken = new CancellationToken())  
{  
var newReturnType = Syntax.ParseTypeName("HttpResponseMessage").WithTrailingTrivia(Syntax.Space);  
var exp = Syntax.ParseExpression("new HttpResponseMessage(HttpStatusCode.Created)").WithLeadingTrivia(Syntax.Space);

var returnStatement = Syntax.ReturnStatement(Syntax.Token(SyntaxKind.ReturnKeyword), exp ,Syntax.Token(SyntaxKind.SemicolonToken));  
var oldBody = _method.Body;

var statements = oldBody.Statements.Where(x => x.GetType() != typeof (ReturnStatementSyntax)).ToList();  
var syntaxListStatements = new SyntaxList<StatementSyntax>();  
syntaxListStatements = statements.Aggregate(syntaxListStatements, (current, syntaxListStatement) => current.Add(syntaxListStatement));  
syntaxListStatements = syntaxListStatements.Add(returnStatement);

var newBody = Syntax.Block(Syntax.Token(SyntaxKind.OpenBraceToken), syntaxListStatements, Syntax.Token(SyntaxKind.CloseBraceToken));

var newmethod = Syntax.MethodDeclaration(\_method.AttributeLists, \_method.Modifiers, newReturnType,  
\_method.ExplicitInterfaceSpecifier, \_method.Identifier,  
\_method.TypeParameterList, \_method.ParameterList,  
_method.ConstraintClauses, newBody);

var oldRoot = _document.GetSyntaxRoot(cancellationToken);  
var newRoot = oldRoot.ReplaceNode(_method,newmethod);

var newDoc = _document.UpdateSyntaxRoot(newRoot.Format(FormattingOptions.GetDefaultOptions()).GetFormattedRoot());  
return new CodeActionEdit(newDoc);

}

public string Description  
{  
get { return "Replace with HttpResponseMessage"; }  
}  
}  

This looks a bit convoluted, but in fact is very simple - as soon as you get used to different Roslyn artefacts.

    1. We pass the IDocument (what the developer is working on) and a MethodDeclarationSyntax (corresponds to the methods we found in the CodeIssue). Notice we don’t pass an array of methods, since each CodeAction executes seperately!
    1. We create a new return type and a new expression statement for the method (without return keyword though). Notice that since we are rewriting raw uncompiled code, we have to take care of everything - like spaces, semicolons, curly braces and so on. Roslyn has syntax that allows you to operate on all of that.
    1. We compose a new ReturnStatement - out of SyntaxKind.ReturnKeyword, our expression and - yes - semi colon (SyntaxKind.SemicolonToken).
    1. We then copy the existing method’s body - except its return statement, if it already has one (yes, it might even though it’s a void method - remember this code does not need to compile to be possible to be processed by Roslyn). We insert our return statement in there. On a side note, notice how Roslyn collections are immutable, and I need to reassign them to the original variable after every change. That’s obviously for performance reasons.
    1. We create a new method based on the old method’s data and the changes we have made so far. This new method will have a correct return type (HttpResponseMessage).
    1. We get the existing syntax root from the developer’s workspace - IDocument, and replace old method with new method. Finally we just do some basic formatting and we are done!

In order for this to work, we still have to modify the CodeIssue to call the CodeAction after it finds the matches, so we change the yield to:

yield return new CodeIssue(CodeIssueKind.Warning, method.ReturnType.Span,  
"Consider returning a 201 Status Code", new CodeAction(document, method));  

We can now run this solution and it works as expected. Notice a little lightbulb icon providing us with more info. When we hover on there, we get a nice little preview of what will happen if we click. Notice the rest of the method’s body is preserved.

Voila:

Installation πŸ”—

By default, this CodeIssue project template compiles to VSIX - Visual Studio extension. Indeed, if you compile it, you will find the *.vsix installer in the Debug/Release folder.

You can install it as any other Visual Studio Extension.

Important: In order for this to work (remember Roslyn is a CTP) you need to run Visual Studio with /rootsuffix Roslyn. So:

"C:Program Files (x86)Microsoft Visual Studio 11.0Common7IDEdevenv.exe" /rootsuffix Roslyn  

If you open VS, you will see the extension in the installed list:

And it will be inspecting your code in real time (all your open documents will be marked with [Roslyn]).

Summary & source πŸ”—

I hope this was an interesting exercise. Using the exact same pattern, you can build rich development time design tools, for example to enforce certain rules in your team or to simply provide shortcuts and easy refactoring mechanisms.

I think it would be nice to package various HTTP standards-driven semantics (like the one I showed here) into a big VSIX, and distribute it to provide rich Web API tooling. For example we could easily sniff out the dreaded ambigous action found error at design time πŸ™‚

Source code, as usually is on GitHub.

About


Hi! I'm Filip W., a cloud architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP