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
- Attributes whose constructors have variable number of arguments.
- Methods with variable number of arguments (
params
array). - 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:
[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";
MSBuildWorkspace workspace = MSBuildWorkspace.Create();
Solution solutionToAnalyze =
workspace.OpenSolutionAsync(pathToSolution).Result;
Project sampleProjectToAnalyze =
solutionToAnalyze.Projects
.Where((proj) => proj.Name == projectName)
.FirstOrDefault();
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";
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;
}
string restOfResult = symbol.ContainingNamespace.GetFullNamespace();
string result = symbol.ContainingNamespace.Name;
if (restOfResult != null)
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:
[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
)
{
IParameterSymbol parameterSymbol = attributeData.AttributeConstructor
.Parameters
.Where((constructorParam) => constructorParam.Name == argName).FirstOrDefault();
int parameterIdx = attributeData.AttributeConstructor.Parameters.IndexOf(parameterSymbol);
TypedConstant constructorArg = attributeData.ConstructorArguments[parameterIdx];
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";
MSBuildWorkspace workspace = MSBuildWorkspace.Create();
Solution solutionToAnalyze =
workspace.OpenSolutionAsync(pathToSolution).Result;
Project sampleProjectToAnalyze =
solutionToAnalyze.Projects
.Where((proj) => proj.Name == projectName)
.FirstOrDefault();
Compilation sampleToAnalyzeCompilation =
sampleProjectToAnalyze.GetCompilationAsync().Result;
string classFullName = "SampleToAnalyze.SubNamespace.SimpleClassToAnalyze";
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);
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.