Roslyn add new method to an existing class
Asked Answered
I

1

11

I'm investigating the use of the Roslyn compiler within a Visual Studio Extension (VSIX) that uses the VisualStudioWorkspace to update existing code. Having spent the last few days reading up on this, there seem to be several ways to achieve this....I'm just not sure which is the best approach for me.

Okay, so let's assume that the User has their solution open in Visual Studio 2015. They click on my Extension and (via a form) they tell me that they want to add the following method definition to an interface:

GetSomeDataResponse GetSomeData(GetSomeDataRequest request);

They also tell me the name of the interface, it's ITheInterface.

The interface already has some code in it:

namespace TheProjectName.Interfaces
{
    using System;
    public interface ITheInterface
    {
        /// <summary>
        ///    A lonely method.
        /// </summary>
        LonelyMethodResponse LonelyMethod(LonelyMethodRequest request);
    }
}

Okay, so I can load the Interface Document using the following:

Document myInterface = this.Workspace.CurrentSolution?.Projects?
    .FirstOrDefault(p 
        => p.Name.Equals("TheProjectName"))
    ?.Documents?
        .FirstOrDefault(d 
            => d.Name.Equals("ITheInterface.cs"));

So, what is the best way to now add my new method to this existing interface, ideally writing in the XML comment (triple-slash comment) too? Bear in mind that the request and response types (GetSomeDataRequest and GetSomeDataResponse) may not actually exist yet. I'm very new to this, so if you can provide code examples then that would be terrific.

UPDATE

I decided that (probably) the best approach would be simply to inject in some text, rather than try to programmatically build up the method declaration.

I tried the following, but ended up with an exception that I don't comprehend:

SourceText sourceText = await myInterface.GetTextAsync();
string text = sourceText.ToString();
var sb = new StringBuilder();

// I want to all the text up to and including the last
// method, but without the closing "}" for the interface and the namespace
sb.Append(text.Substring(0, text.LastIndexOf("}", text.LastIndexOf("}") - 1)));

// Now add my method and close the interface and namespace.
sb.AppendLine("GetSomeDataResponse GetSomeData(GetSomeDataRequest request);");
sb.AppendLine("}");
sb.AppendLine("}");

Inspecting this, it's all good (my real code adds formatting and XML comments, but removed that for clarity).

So, knowing that these are immutable, I tried to save it as follows:

var updatedSourceText = SourceText.From(sb.ToString());
var newInterfaceDocument = myInterface.WithText(updatedSourceText);
var newProject = newInterfaceDocument.Project;
var newSolution = newProject.Solution;
this.Workspace.TryApplyChanges(newSolution);

But this created the following exception:

bufferAdapter is not a VsTextDocData 

at Microsoft.VisualStudio.Editor.Implementation.VsEditorAdaptersFactoryService.GetAdapter(IVsTextBuffer bufferAdapter) at Microsoft.VisualStudio.Editor.Implementation.VsEditorAdaptersFactoryService.GetDocumentBuffer(IVsTextBuffer bufferAdapter) at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.InvisibleEditor..ctor(IServiceProvider serviceProvider, String filePath, Boolean needsSave, Boolean needsUndoDisabled) at Microsoft.VisualStudio.LanguageServices.RoslynVisualStudioWorkspace.OpenInvisibleEditor(IVisualStudioHostDocument hostDocument) at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.DocumentProvider.StandardTextDocument.UpdateText(SourceText newText) at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.VisualStudioWorkspaceImpl.ApplyDocumentTextChanged(DocumentId documentId, SourceText newText) at Microsoft.CodeAnalysis.Workspace.ApplyProjectChanges(ProjectChanges projectChanges) at Microsoft.CodeAnalysis.Workspace.TryApplyChanges(Solution newSolution) at Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.VisualStudioWorkspaceImpl.TryApplyChanges(Solution newSolution)

Interlaken answered 8/6, 2016 at 18:56 Comment(1)
You probably need to change the existing SourceText (which has additional source file information attached to it), by calling SourceText.WithChanges(new TextChange(...)), see this answer for an example.Antitank
P
12

If I were you I would take advantage of all Roslyn benefits, i.e. I would work with the SyntaxTree of the Document rather than processing the files text (you are able to do the latter without using Roslyn at all).

For instance:

...
SyntaxNode root = await document.GetSyntaxRootAsync().ConfigureAwait(false);
var interfaceDeclaration = root.DescendantNodes(node => node.IsKind(SyntaxKind.InterfaceDeclaration)).FirstOrDefault() as InterfaceDeclarationSyntax;
if (interfaceDeclaration == null) return;

var methodToInsert= GetMethodDeclarationSyntax(returnTypeName: "GetSomeDataResponse ", 
          methodName: "GetSomeData", 
          parameterTypes: new[] { "GetSomeDataRequest" }, 
          paramterNames: new[] { "request" });
var newInterfaceDeclaration = interfaceDeclaration.AddMembers(methodToInsert);

var newRoot = root.ReplaceNode(interfaceDeclaration, newInterfaceDeclaration);

// this will format all nodes that have Formatter.Annotation
newRoot = Formatter.Format(newRoot, Formatter.Annotation, workspace);
workspace.TryApplyChanges(document.WithSyntaxRoot(newRoot).Project.Solution);
...

public MethodDeclarationSyntax GetMethodDeclarationSyntax(string returnTypeName, string methodName, string[] parameterTypes, string[] paramterNames)
{
    var parameterList = SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(GetParametersList(parameterTypes, paramterNames)));
    return SyntaxFactory.MethodDeclaration(attributeLists: SyntaxFactory.List<AttributeListSyntax>(), 
                  modifiers: SyntaxFactory.TokenList(), 
                  returnType: SyntaxFactory.ParseTypeName(returnTypeName), 
                  explicitInterfaceSpecifier: null, 
                  identifier: SyntaxFactory.Identifier(methodName), 
                  typeParameterList: null, 
                  parameterList: parameterList, 
                  constraintClauses: SyntaxFactory.List<TypeParameterConstraintClauseSyntax>(), 
                  body: null, 
                  semicolonToken: SyntaxFactory.Token(SyntaxKind.SemicolonToken))
          // Annotate that this node should be formatted
          .WithAdditionalAnnotations(Formatter.Annotation);
}

private IEnumerable<ParameterSyntax> GetParametersList(string[] parameterTypes, string[] paramterNames)
{
    for (int i = 0; i < parameterTypes.Length; i++)
    {
        yield return SyntaxFactory.Parameter(attributeLists: SyntaxFactory.List<AttributeListSyntax>(),
                                                 modifiers: SyntaxFactory.TokenList(),
                                                 type: SyntaxFactory.ParseTypeName(parameterTypes[i]),
                                                 identifier: SyntaxFactory.Identifier(paramterNames[i]),
                                                 @default: null);
    }
}

Note that this is pretty raw code, Roslyn API is extremely powerful when it comes to analyzing/processing the syntax tree, getting symbol information/references and so on. I would recommend you to look at this page and this page for reference.

Pushcart answered 10/6, 2016 at 8:15 Comment(3)
Wow. You are of course right. The idea of injecting a code file containing the concrete implementation containing many lines of template code in this manner might take some time to construct. I guess I could load a string template and parse that to a syntax tree and then inject that "sub" tree into an existing tree. Just go to work out how to do that.....Interlaken
This really helped me. However, a semi-colon isn't added for some reason, still trying to figure that out.Provoke
Found it, had to add declaration = declaration.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)); for some reason. I guess the builder pattern has fewer bugs? ;)Provoke

© 2022 - 2024 — McMap. All rights reserved.