Learn Roslyn Now: Part 12 Document Editing with the DocumentEditor

One drawback of Roslyn’s immutability is that it can sometimes make it tricky to apply multiple changes to a Document or SyntaxTree. Immutability means that every time we apply changes to a syntax tree, we’re given an entirely new syntax tree. By default we can’t compare nodes across trees, so what do we do when we want to make multiple changes to a syntax tree?

Roslyn gives us four options:

The DocumentEditor allows us to make multiple changes to a document and get the resulting document after the changes have been applied. Under the covers, the DocumentEditor is a thin layer over the SyntaxEditor.

We’ll use the DocumentEditor to change:


char key = Console.ReadKey();
if(key == 'A')
{
Console.WriteLine("You pressed A");
}
else
{
Console.WriteLine("You didn't press A");
}

view raw

Original.cs

hosted with ❤ by GitHub

to:


char key = Console.ReadKey();
if(key == 'A')
{
LogConditionWasTrue();
Console.WriteLine("You pressed A");
}
else
{
Console.WriteLine("You didn't press A");
LogConditionWasFalse();
}

view raw

Edited.cs

hosted with ❤ by GitHub

We’ll use the DocumentEditor to simultaneously insert an invocation before the first Console.WriteLine() and to insert another after the second.

Unfortunately there’s a ton of boiler plate when creating a Document from scratch. Typically you’ll get a Document from a Workspace so it shouldn’t be this bad:


var mscorlib = MetadataReference.CreateFromAssembly(typeof(object).Assembly);
var workspace = new AdhocWorkspace();
var projectId = ProjectId.CreateNewId();
var versionStamp = VersionStamp.Create();
var projectInfo = ProjectInfo.Create(projectId, versionStamp, "NewProject", "projName", LanguageNames.CSharp);
var newProject = workspace.AddProject(projectInfo);
var sourceText = SourceText.From(@"
class C
{
void M()
{
char key = Console.ReadKey();
if (key == 'A')
{
Console.WriteLine(""You pressed A"");
}
else
{
Console.WriteLine(""You didn't press A"");
}
}
}");
var document = workspace.AddDocument(newProject.Id, "NewFile.cs", sourceText);
var syntaxRoot = await document.GetSyntaxRootAsync();
var ifStatement = syntaxRoot.DescendantNodes().OfType<IfStatementSyntax>().Single();
var conditionWasTrueInvocation =
SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("LogConditionWasTrue"))
.WithArgumentList(
SyntaxFactory.ArgumentList()
.WithOpenParenToken(
SyntaxFactory.Token(
SyntaxKind.OpenParenToken))
.WithCloseParenToken(
SyntaxFactory.Token(
SyntaxKind.CloseParenToken))))
.WithSemicolonToken(
SyntaxFactory.Token(
SyntaxKind.SemicolonToken));
var conditionWasFalseInvocation =
SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("LogConditionWasFalse"))
.WithArgumentList(
SyntaxFactory.ArgumentList()
.WithOpenParenToken(
SyntaxFactory.Token(
SyntaxKind.OpenParenToken))
.WithCloseParenToken(
SyntaxFactory.Token(
SyntaxKind.CloseParenToken))))
.WithSemicolonToken(
SyntaxFactory.Token(
SyntaxKind.SemicolonToken));
//Finally… create the document editor
var documentEditor = await DocumentEditor.CreateAsync(document);
//Insert LogConditionWasTrue() before the Console.WriteLine()
documentEditor.InsertBefore(ifStatement.Statement.ChildNodes().Single(), conditionWasTrueInvocation);
//Insert LogConditionWasFalse() after the Console.WriteLine()
documentEditor.InsertAfter(ifStatement.Else.Statement.ChildNodes().Single(), conditionWasFalseInvocation);
var newDocument = documentEditor.GetChangedDocument();

All the familiar SyntaxNode methods are here. We can Insert, Replace and Remove nodes as we see fit, all based off of nodes in our original syntax tree. Many people find this approach more intuitive than building an entire CSharpSyntaxRewriter.

It can be somewhat difficult to debug things when they go wrong. When writing this post I was mistakenly trying to insert nodes after ifStatement.Else instead of ifStatement.Else.Statement. I was receiving an InvalidOperationException but the message wasn’t very useful and it took me quite some time to figure out what I was doing wrong. The documentation on InsertNodeAfter says:

This node must be of a compatible type to be placed in the same list containing the existing node.

How can we know which types of nodes are compatible with one another? I don’t think there’s a good answer here. We essentially have to learn which nodes are compatible ourselves. As usual the Syntax Visualizer and Roslyn Quoter are the best tools for figuring out what kinds of nodes you should be creating.

It’s worth noting that the DocumentEditor exposes the SemanticModel of your original document. You may need this when editing the original document and making decisions about what you’d like to change.

It’s also worth noting that the underlying SyntaxEditor exposes a SyntaxGenerator that you can use to build syntax nodes without relying on the more verbose SyntaxFactory.

5 thoughts on “Learn Roslyn Now: Part 12 Document Editing with the DocumentEditor

  1. Those SyntaxFactory expressions can be simplified by quite a bit. Roslyn Quoter used to show them more complex than necessary, but it has been improved now.

  2. Is there any way to add a Using statement via the DocumentEditor?

    I created a UsingDirectiveSyntax, then I fetched the CompilationUnitSyntax from the document and inserted the Using statement using InsertBefore relative to the first member in the CompilationUnitSyntax.

    That leads to an error (unable to cast object from UsingDirectiveSyntax to MemberDeclarationSyntax) when I finally call the GetChangedDocument() method.

    1. I don’t know the DocumentEditor well enough to say. I know you can create them with the SyntaxFactory with something like:

      SyntaxFactory.CompilationUnit()
      .WithUsings(
      SyntaxFactory.SingletonList(
      SyntaxFactory.UsingDirective(
      SyntaxFactory.IdentifierName(“System”))))

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s