Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Roslyn Code Analysis in Easy Samples (Part 1)

0.00/5 (No votes)
11 Jan 2015 3  
Describe Roslyn code analysis functionality providing easy samples

Introduction

In Implementing Adapter Pattern and Imitating Multiple Inheritance in C# using Roslyn based VS Extension Wrapper Generator article I describe building a VS 2015 preview Roslyn based extension for generating class member wrappers.

In that article and two subsequent articles, however, I do not talk much about how the VS extension was built, instead I talk mostly about the way it was used.

Here I would like to go over more about what I learned while building that visual studio extension, in particular sharing my experience with Roslyn. Hopefully this will encourage other people to write there own Roslyn based extensions and will eventually result in greater C# language capabilities.

In the beginning I wanted to simply describe my NP.WrapperGenerator.vsix code, but then I decided that it will be of more use if I present the Roslyn functionality in a set of simple samples - each sample emphasizing some Roslyn feature, so that the users can read it s a Roslyn Code Analysis tutorial.

While Roslyn code analysis features are superb, Roslyn code generation is too verbose and too difficult to debug, from my point of view. Because of that, when working on NP.WrapperGenerator.vsix extension, I tapped into simpler and more reliable CodeDOM code generation features. CodeDOM, however, has been around for a while, there are some other articles describing it in detail and here I concentrate only on Roslyn code analysis part.

Building of the VS 2015 extension itself is describe in detail in the article metioned above. On the other hand, one can play with Roslyn much faster using simple Console projects - otherwise each time you start an extension project you have to wait until the whole VS studio starts. Because of that all of my samples are built as simple Console projects.

In order to run the samples, you have to have VS 2015 preview installed.

In this article, we concentrate on using Roslyn compilation to obtain information about simple namespaces, properties, events, methods and attributes.

In part 2 of article, I plan to talk about more complex case - in particular

  1. Attributes whose constructors have variable number of arguments.
  2. Methods with variable number of arguments (params array).
  3. Generic classes and methods.

 

Sky is the Limit with Roslyn

Before VS 2015, the main problem for the developers writing VS extensions affecting and extending the C# or VB languages was the lack of reliable way of obtaining information about the code to be extended.

Some large companies, of course, could build their own frameworks for maintaining full solution information - example of this would be Resharper product, but individual developers were at a loss. For example I had this idea of building adapters and Multiple Inheritance using wrapper generation many years ago, but only Roslyn built into VS 2015 allowed me to implement it.

VS 2015 has its C# and VB compilers powered by Roslyn. VS 2015 is also the first version of Visual Studio where the Roslyn can be used for building the VS extensions. The VS 2015 maintains Roslyn Workspace for the solutions to which the VS extensions are applied allowing the developers to find out anything and everything they need about the projects and files being extended.

Now that Microsoft basically opened up its compiler blackbox, hopefully various developers will be able to tap into it and create their own extensions of the C# and VB languages some of which will be adopted by the development community and Microsoft itself. The main purpose of this article is to show the Roslyn code analysis capabilities and to get the developers interested.

Installing Roslyn Dlls

In VS 2015 Preview you need to use the package manager to install your Roslyn dlls as described at Roslyn.

You can open NuGet package manager console and type Install-Package Microsoft.CodeAnalysis -Pre

Also you'll have to install MEF 2 by running Install-Package Microsoft.Composition.

General Description of Roslyn Code Analysis API

Roslyn code analysis API allows to get full information about the compiled code without actually loading it. I would say that it is similar to large degree to System.Reflection library accept that it does not require the code under investication to be loaded into your .NET solution. This makes it very useful for writing compiler and VS extensions.

Samples

The samples contain two solutions - SampleToAnalyze and SimpleRoslynAnalysis.

SampleToAnalyze contains code that is being analized. SimpleRoslynAnalysis presents examples of code analysis using Roslyn.

SampleToAnalyze contains class SimpleClassToAnalyze that's being analyzed by SimpleRoslynAnalysis project. Here is the class'es code

namespace SampleToAnalyze.SubNamespace
{
    [SimpleAttr(5, "Hello World")]
    public class SimpleClassToAnalyze
    {
        public int MySimpleProperty { get; set; }

        public event Action<object> MySimpleEvent;

        public int MySimpleMethod(string str, out bool flag, int i = 5)
        {
            flag = true;

            return 5;
        }
    }
}

As you can see, it contains a property MySimpleProperty, an event MySimpleEvent and a method MySimpleMethod. There is also a class attribute - SimpleAttr. This attribute is defined in the same project:

// set this to be a class-only attribute
[AttributeUsage(AttributeTargets.Class)]
public class SimpleAttrAttribute : Attribute
{
    public int IntProp { get; protected set; }

    public string StringProp { get; protected set; }

    public SimpleAttrAttribute(int intProp, string stringProp)
    {
        IntProp = intProp;
        StringProp = stringProp;
    }
}  

SimpleRoslynAnalysis is a console project. Its main Program class contains all the samples. It utilizes extension methods from the static Extension class.

Here is the code that we employ to obtain the Roslyn compilation object i.e. object that contains information about all the types:

const string pathToSolution = @"..\..\..\SampleToAnalyze\SampleToAnalyze.sln";
const string projectName = "SampleToAnalyze";

// start Roslyn workspace
MSBuildWorkspace workspace = MSBuildWorkspace.Create();

// open solution we want to analyze
Solution solutionToAnalyze =
    workspace.OpenSolutionAsync(pathToSolution).Result;

// get the project we want to analyze out
// of the solution
Project sampleProjectToAnalyze =
    solutionToAnalyze.Projects
                        .Where((proj) => proj.Name == projectName)
                        .FirstOrDefault();

// get the project's compilation
// compilation contains all the types of the 
// project and the projects referenced by 
// our project. 
Compilation sampleToAnalyzeCompilation =
    sampleProjectToAnalyze.GetCompilationAsync().Result;  

We pullout the information about our SampleClassToAnalyze from the compilation by using GetTypeByMetadataName method, passing to it the name full name of the class (including the namespaces):

string classFullName = "SampleToAnalyze.SubNamespace.SimpleClassToAnalyze";

// getting type out of the compilation
INamedTypeSymbol simpleClassToAnalyze =
    sampleToAnalyzeCompilation.GetTypeByMetadataName(classFullName);  

INamedTypeSymbol contains all the compiler information about the type (analogous to System.Type in the usual Reflection based code investigation).

The first line of code shows how to get the full C# namespace for the INamedTypeSymbol (or for any ISymbol, for that matter):

string fullNamespacePath = simpleClassToAnalyze.GetFullNamespace(); 

The variable fullNamespacePath will contain "SampleToAnalyze.SubNamespace". Take a look at Extensions.GetFullNamespace(...) extension method:

public static string GetFullNamespace(this ISymbol symbol)
{
    if ((symbol.ContainingNamespace == null) ||
         (string.IsNullOrEmpty(symbol.ContainingNamespace.Name)))
    {
        return null;
    }

    // get the rest of the full namespace string
    string restOfResult = symbol.ContainingNamespace.GetFullNamespace();

    string result = symbol.ContainingNamespace.Name;

    if (restOfResult != null)
        // if restOfResult is not null, append it after a period
        result = restOfResult + '.' + result;

    return result;
}  

As you can see, this is a recursive method that recursively ContainingNamespace property to create the resulting namespace string.

In order to get class members we use GetMembers(...) method.

Here is how we get the property:

IPropertySymbol propertySymbol = 
    simpleClassToAnalyze.GetMembers("MySimpleProperty").FirstOrDefault() 
    as IPropertySymbol;  

From IPropertySymbol we can get property type: propertySymbol.Type and property name: propertySymbol.Name

IEventSymbol contains information about and event:

IEventSymbol eventSymbol = 
    simpleClassToAnalyze.GetMembers("MySimpleEvent").FirstOrDefault() 
    as IEventSymbol;  

Note that MySimpleEvent is defined as Action<object> - a type with generic arguments.

If we print eventSymbol.Type.Name, we'll see only "Action" printed. In order to reconstruct the whole generic type, we employ GetFullTypeString(...) recursive extension method:

public static string GetFullTypeString(this INamedTypeSymbol type)
{
    string result = type.Name;

    if (type.TypeArguments.Count() > 0)
    {
        result += "<";

        bool isFirstIteration = true;
        foreach(INamedTypeSymbol typeArg in type.TypeArguments)
        {
            if (isFirstIteration)
            {
                isFirstIteration = false;
            }
            else
            {
                result += ", ";
            }

            result += typeArg.GetFullTypeString();
        }

        result += ">";
    }

    return result;
}  

The generic type arguments are located within TypeArguments property of the INamedTypeSymbol object.

The method GetFullTypeString(...) assembles the resulting string by calling itself recursively for each of the type arguments and placing them within <...> brackets separated by commas.

In order to get information about the method MySimpleMethod, we employ a similar approach:

IMethodSymbol methodSymbol = 
    simpleClassToAnalyze.GetMembers("MySimpleMethod").FirstOrDefault() 
    as IMethodSymbol;  

GetMethodSignuture(...) extension method shows how to reconstruct the MySimpleMethod method's signature from an IMethodSymbol object:

public static string GetMethodSignature(this IMethodSymbol methodSymbol)
{
    string result = methodSymbol.DeclaredAccessibility.ConvertAccessabilityToString();

    if (methodSymbol.IsAsync)
        result += " async";

    if (methodSymbol.IsAbstract)
        result += " abstract";

    if (methodSymbol.IsVirtual)
    {
        result += " virtual";
    }

    if (methodSymbol.IsStatic)
    {
        result += " static";
    }

    if (methodSymbol.IsOverride)
    {
        result += " override";
    }

    if (methodSymbol.ReturnsVoid)
    {
        result += " void";
    }
    else
    {
        result += " " + (methodSymbol.ReturnType as INamedTypeSymbol).GetFullTypeString();
    }

    result += " " + methodSymbol.Name + "(";

    bool isFirstParameter = true;
    foreach(IParameterSymbol parameter in methodSymbol.Parameters)
    {
        if (isFirstParameter)
        {
            isFirstParameter = false;
        }
        else
        {
            result += ", ";
        }

        if (parameter.RefKind == RefKind.Out)
        {
            result += "out ";
        }
        else if (parameter.RefKind == RefKind.Ref)
        {
            result += "ref ";
        }

        string parameterTypeString = 
            (parameter.Type as INamedTypeSymbol).GetFullTypeString();

        result += parameterTypeString;

        result += " " + parameter.Name;

        if (parameter.HasExplicitDefaultValue)
        {
            result += " = " + parameter.ExplicitDefaultValue.ToString();
        }
    }

    result += ")";

    return result;
}  

The encapsulation level of the function (or any other class member) is determined by DeclaredAccessibility property of Microsoft.CodeAnalysis.Accessibility enumeration. I created a utility method ConvertAccessabilityToString(...) to produce string out of this enumeration:

public static string ConvertAccessabilityToString(this Accessibility accessability)
{
    switch (accessability)
    {
        case Accessibility.Internal:
            return "internal";
        case Accessibility.Private:
            return "private";
        case Accessibility.Protected:
            return "protected";
        case Accessibility.Public:
            return "public";
        case Accessibility.ProtectedAndInternal:
            return "protected internal";
        default:
            return "private";
    }
}  

As you can see from GetMethodSignature(...) method's implementation, IMethodSymbol has various flags - IsAsync IsAbstract, IsVirtual, IsStatic, IsOverride that specify whether the function is async, abstract, virtual, static, or overrides a function declared in a super-class.

ReturnsVoid boolean property is true when the function is void. If this property is false, the method's return type is specified by methodSymbol.ReturnType (can and should be cast to INamedTypeSymbol).

The method's arguments are described by methodSymbol.Parameters array that consists of IParameterSymbol objects.

Each IParameterSymbol contains the parameter's type (as parameterSymbol.Type) and parameter name (as parameterSymbol.Name). They also contain RefKind property of RefKind enumeration that specifies if the parameter is out or ref or none of those.

HasExplicityDefaultValue of IParameterSymbol specifies whether the parameter has a default value - (in our case last parameter i has default value 5). The value itself is contained in IParameterSymbol.ExplicitDefaultValue property as C# object.

Finally we'll demonstrate pulling Attribute information out of class definition - even though it is not presented here, but pulling Attribute info out of method definition can be done in exactly the same fashion.

As was shown above, we use SimpleAttrAttribute in our SampleToAnalize project. Here is the attribute's code:

// set this to be a class-only attribute
[AttributeUsage(AttributeTargets.Class)]
public class SimpleAttrAttribute : Attribute
{
    public int IntProp { get; protected set; }

    public string StringProp { get; protected set; }

    public SimpleAttrAttribute(int intProp, string stringProp)
    {
        IntProp = intProp;
        StringProp = stringProp;
    }
}  

Note, that the setters of the attribute's properties are protected, so that the only way to set them is via the constructor. This was done on purpose in order to simplify parsing the attribute's information using Roslyn - now all we need to do - is to figure out which constructor argument corresponds to which attribute property.

We use GetAttributes() method in order to pull the class'es attribute information:

AttributeData attrData = 
    simpleClassToAnalyze.GetAttributes().FirstOrDefault();  

As you can see, the attribute information comes in the shape of Microsoft.CodeAnalysis.AttributeData object.

I created GetAttributeConstructorValueByParameterName(...) extension method to pull the Attribute's property values out of the constructor - here is how we use it to get IntProp and StringProp values in Program.Main:

object intProperty = attrData.GetAttributeConstructorValueByParameterName("intProp");
Console.WriteLine();
Console.WriteLine("Attribute's IntProp = " + intProperty);

object stringProperty = attrData.GetAttributeConstructorValueByParameterName("stringProp");

Console.WriteLine();
Console.WriteLine("Attribute's StringProp = " + stringProperty); 

Now, let us take a look at GetAttributeConstructorValueByParameterName(...) method's implementation:

public static object 
    GetAttributeConstructorValueByParameterName
    (
        this AttributeData attributeData, 
        string argName
    )
{

    // Get the parameter
    IParameterSymbol parameterSymbol = attributeData.AttributeConstructor
        .Parameters
        .Where((constructorParam) => constructorParam.Name == argName).FirstOrDefault();

    // get the index of the parameter
    int parameterIdx = attributeData.AttributeConstructor.Parameters.IndexOf(parameterSymbol);

    // get the construct argument corresponding to this parameter
    TypedConstant constructorArg = attributeData.ConstructorArguments[parameterIdx];

    // return the value passed to the attribute
    return constructorArg.Value;
}  

The code comments basically explain what happens there. We check the index of the constructor parameter by using attributeData.AttributeConstructor.Parameters collection. Then using that index we get the corresponding constructor argument.

Here is the Program.Main method's implementation

static void Main(string[] args)
{
    const string pathToSolution = @"..\..\..\SampleToAnalyze\SampleToAnalyze.sln";
    const string projectName = "SampleToAnalyze";

    // start Roslyn workspace
    MSBuildWorkspace workspace = MSBuildWorkspace.Create();

    // open solution we want to analyze
    Solution solutionToAnalyze =
        workspace.OpenSolutionAsync(pathToSolution).Result;

    // get the project we want to analyze out
    // of the solution
    Project sampleProjectToAnalyze =
        solutionToAnalyze.Projects
                            .Where((proj) => proj.Name == projectName)
                            .FirstOrDefault();

    // get the project's compilation
    // compilation contains all the types of the 
    // project and the projects referenced by 
    // our project. 
    Compilation sampleToAnalyzeCompilation =
        sampleProjectToAnalyze.GetCompilationAsync().Result;

    string classFullName = "SampleToAnalyze.SubNamespace.SimpleClassToAnalyze";

    // getting type out of the compilation
    INamedTypeSymbol simpleClassToAnalyze =
        sampleToAnalyzeCompilation.GetTypeByMetadataName(classFullName);

    string fullNamespacePath = simpleClassToAnalyze.GetFullNamespace();

    Console.WriteLine("Full Namespace:");
    Console.WriteLine(fullNamespacePath);
    Console.WriteLine();

    IPropertySymbol propertySymbol = 
        simpleClassToAnalyze.GetMembers("MySimpleProperty").FirstOrDefault() 
        as IPropertySymbol;

    INamedTypeSymbol propertyType = propertySymbol.Type as INamedTypeSymbol;

    IEventSymbol eventSymbol = 
        simpleClassToAnalyze.GetMembers("MySimpleEvent").FirstOrDefault() 
        as IEventSymbol;
    Console.WriteLine("Event Name:");
    Console.WriteLine(eventSymbol.Name);

    INamedTypeSymbol eventType = eventSymbol.Type as INamedTypeSymbol;


    Console.WriteLine();
    Console.WriteLine("Event Type:");
    Console.WriteLine(eventType.GetFullTypeString());


    IMethodSymbol methodSymbol = 
        simpleClassToAnalyze.GetMembers("MySimpleMethod").FirstOrDefault() 
        as IMethodSymbol;

    string methodDeclarationString = methodSymbol.GetMethodSignature();


    Console.WriteLine();
    Console.WriteLine("Method Signature:");
    Console.WriteLine(methodDeclarationString);

    // dealing with attributes
    AttributeData attrData = 
        simpleClassToAnalyze.GetAttributes().FirstOrDefault();

    object intProperty = attrData.GetAttributeConstructorValueByParameterName("intProp");
    Console.WriteLine();
    Console.WriteLine("Attribute's IntProp = " + intProperty);

    object stringProperty = attrData.GetAttributeConstructorValueByParameterName("stringProp");

    Console.WriteLine();
    Console.WriteLine("Attribute's StringProp = " + stringProperty);
}  

When run, it shows the following on the console

Full Namespace:
SampleToAnalyze.SubNamespace

Event Name:
MySimpleEvent

Event Type:
Action<Object>

Method Signature:
public Int32 MySimpleMethod(String str, out Boolean flag, Int32 i = 5)

Attribute's IntProp = 5

Attribute's StringProp = Hello World  

Conclusion

We've shown some basic Roslyn functionality allowing analysis of .NET code without loading the corresponding dll. In part 2, we plan to consider more complex cases of generic classes and functions, methods with variable number of arguments and other interesting capabilities we've used for building NP.WrappgerGenerator.vsix extension.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here