Showing posts with label Roslyn. Show all posts
Showing posts with label Roslyn. Show all posts

14/05/2018

How to use Roslyn with .NET Core?

Home

Title: Baths of Caracalla in Rome, Source: own resources, Authors: Agnieszka and Michał Komorowscy

When we started PlatformX project, we were using full .NET Framework and it was natural to use Roslyn in order to read and analyse C# code. I have to admit that Roslyn, despite some initial problems (see other posts about Roslyn), managed to do the job. However, later on we decided to migrate PlatformX to .NET Core. There were a few reasons behind this decision:
  • We anticipated that in future we may want move to Linux server for example to reduce costs.
  • .NET Core is faster. To give you an example. After migration builds turned out to be 1.7 times faster and tests 2.6 times faster!
  • Before migration we had to support 2 versions of our infrastructure libraries (.NET Framework + .NET Core) what was a little bit cumbersome. Now we support only one.
  • If we had waited, we would have spent more time on the migration later.
Despite all these advantages the migration to .NET Core meant a problem with using Roslyn. This problem is called MsBuildWorkspace. It is the class that allows you to point a solution or a project, read it and then analyse it a file after a file. Unfortunately MsBuildWorkspace is not supported in .NET Core. I think that the main issue is that under the hood it uses MS Build which is not cross-platform.

Because nowadays .NET Core projects can actually reference full .NET Framework libraries (of course it kills portability), firstly we simply tried to do so. But without much success. MsBuildWorkspace simply cannot handle .NET Core projects. The actual effect was that according to MsBuildWorkspace there were no files in a project being analysed.

Fortunately, the rescue came from Buildalyzer project which does exactly what MsBuildWorkspace should do and it works like a charm. Here is how we use Buildalyzer:
var sb = new StringBuilder();
var writer = new StringWriter(sb);

var manager = new AnalyzerManager(solutionPath,
   new AnalyzerManagerOptions
   {
      LogWriter = writer
   });

foreach (var prj in manager.Projects.Values)
{
   // GetWorkspace returns Microsoft.CodeAnalysis.AdhocWorkspace which can be used with Roslyn
   var workspace = prj.GetWorkspace();

   // Starting from here the code does not depend on Buildalyzer
   var sln = workspace.CurrentSolution;

   await AnalyzeProject(sln.Projects.First());
}

writer.Close();

_logger.Write(sb.ToString());
As to AnalyzeProject method, it is responsible for analyzing all files in the project. What is important it DOES NOT depend on Buildalyzer at all. I really didn't have to change a line after switching from MsBuildWorkspace to Buildalyzer and it is amazing. Just a few lines of extra code and you can take all advantages of Roslyn in .NET Core projects in order to analyse other .NET Core projects.

*The picture at the beginning of the post comes from own resources and shows Baths of Caracalla in Rome.

14/08/2017

Roslyn tricked me again

Home



A while ago 2 of my SpecFlow tests started failing on the build server. At the same time, on a few local machines no problems were observed. I also didn't find any exceptions in the log, so I decided to log into the server and debug a problem there.

Quite soon I figured out that the problem is in the algorithm that uses Roslyn to analyse and understand the code. Here is a simplified code that finds all local variables within a method body and tries to determine their exact types.

20/04/2017

How I removed 50% of the code

Home

Title: Azuleyo tiles somewhere in Portugal, Source: own resources, Authors: Agnieszka and Michał Komorowscy


My last 2 posts were about problems with using Roslyn. Nonetheless, even if I sometime hate it, I'm still using it so the time has come to show some practical example of using Roslyn. Recently, I've been working on the task that can be summed up as: Take this ugly code and do something with it. i.e. more or less the refactoring task.

Now I'll give you some intuition of what I have to deal with. The code that I have to refactor was generated automatically based on XML schema. These were actually DTO classes used to communicate with the external service. Here are some statistics:
  • 28.7 thousands lines of code in 23 files.
  • 2200 classes and 920 enums.
  • Many classes and enums seems to me identical or very similar.

10/04/2017

Why I hate Roslyn even more

Home



In my previous post I wrote about my problem with "empty" projects and Roslyn. The symptom was that in some cases according to Roslyn my C# projects didn't contain any files. For quite a long time, I haven't been able to find a solution. Especially because I couldn't reproduce problem on my local machine. Fortunately, today I noticed exactly the same problem on another computer.

29/03/2017

Why I hate Roslyn

Home



The more I work with Roslyn the more I appreciate the possibilities it gives and the more I hate it. And I hate it for the same thing as many other projects I worked with in the past. What is it? Well, I like when a system fails fast, fails loudly and fails in the clear way. Unfortunately, Roslyn can do something completely different what sometimes makes working with it the pain in ass. I'll give you some examples.

Issue 1 - Problem with "empty" projects

Here is the code that shows how I usually process documents/files for a given project. It's pretty easy.
var workspace = MSBuildWorkspace.Create();

var sln = await workspace.OpenSolutionAsync(path);
     
foreach (var projectId in sln.ProjectIds)
{
   var project = sln.GetProject(projectId);

   foreach (var documentId in project.DocumentIds)
   {
      // Process a document
   }
}
It works quite well but only on my machine :) On 2 other machines I'm observing problems. In general I have an example solution with 2 test projects. One is WPF application and the another is WebAPI.

The problem is that on some machines I can only read and analyze WPF application. If I try to do exactly the same thing with WebAPI application, then the project loaded by Roslyn is empty i.e. contains no documents (DocumentIds property is empty)! I've already tried to load this project in a different way but without success.

To be honest currently I'm stuck and I have no idea what is wrong here. Any suggestions?

Issue 2 - the semantic analysis does not work

With Roslyn we can perfrom the syntax analysis and the semantic analysis of the code. The syntax analysis, with a syntaxt tree, allows you to only see a structure of a program. The semantic analysis is more powerfull and allows you to understand more. For example, having a code like that:

SomeClass x;

With the semantic analysis you can check that SomeClass is defined within SomeNamespace and has X members (methods, properties). For example, here we have a code showing how to use the semantic analysis to check what interfaces are implemented by a given class at any level of the inheritance.
var compilation = await project.GetCompilationAsync();

foreach (var documentId in project.DocumentIds)
{
   var document = project.GetDocument(documentId);

   // Get a syntax tree
   var tree = await document.GetSyntaxTreeAsync(); 

   // Get a root of the syntax tree
   var root = await tree.GetRootAsync(); 
   
   // Find a node of the syntaxt tree for a first class in a file/document
   var classNode= root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault(); 

   if(classNode== null) continue;

   // Get a semantic model for the syntax tree
   var semanticModel = compilation.GetSemanticModel(tree); 

   // Use the semantic model to get symbol info for the found class node
   var symbol = semanticModel.GetDeclaredSymbol(classNode); 

   // Check what inerfaces are implemnted by the class at any level
   foreach(var @interface in symbol.AllInterfaces)  
   {
      // ...
   }
}
If you run this code as it is, it again will not throw any exceptions. However, you'll noticed that any found class doesn't implement any interface according to Roslyn. Where is the problem this time?

It's quite obvious if you know that. To perform the semantic analysis Roslyn needs to analyse assemblies used by the project. However, it's not enough to compile the project. You have to explicitly register all required assemblies. I do it in the easy way. I simply register all assemblies found in the output folder.
var compilation = await project.GetCompilationAsync();

// Let's register mscorlib
compilation = compilation.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));

if (Directory.Exists("PATH TO OUTPUT DIRECTORY"))
{
   var files = Directory.GetFiles(directory, "*.dll").ToList(); // You can also look for *.exe files

   foreach (var f in files)
      compilation = compilation.AddReferences(MetadataReference.CreateFromFile(f));
}
And again if the semantic analysis can not be performed without that why no exception is thrown?

Issue 3 - Problem with reading projects/solutions

This one I've already described in more details in the post about Roslyn and unit tests. The problem was that:
  • MSBuildWorkspace.OpenSolutionAsync method was returning an empty solution if a particular assembly was missing (not fast, not loud)
  • MSBuildWorkspace.OpenProjectAsync method was returning the error The language 'C#' is not supported (not in the clear way).
These issues were caused by a missing assembly i.e. Microsoft.CodeAnalysis.CSharp.Workspaces.dll. However, wouldn't it be easier to just throw an exception saying that it is missing. Or at least saying that it was not possible to find assembly responsible for reading C# projects and solutions.


Remember failing fast, loudly and in the clear way does not cost much but can save a lot of time.


*The picture at the beginning of the post comes from own resources and shows cliffs near Cabo da Roca - the westernmost extent of mainland Portugal.

08/03/2017

Roslyn and unit tests suck

Home


Title: Imperial Gardens in Tokyo, Source: own resources, Authors: Agnieszka and Michał Komorowscy

I'm working on the project where I have an opportunity to use Roslyn compiler as a service. It is very good :) However yesterday it took me more than 2 hours to write working unit tests (based on MSTest) for my code! Here are some tips that may save your time.

Let's start with the simle thing. When I run unit tests for the first time the following exception was thrown:

System.IO.FileNotFoundException: Could not load file or assembly 'System.Runtime...' or one of its dependencies.

To fix this problem I simply installed the following packages via Nuget:
  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.CSharp.Workspaces
Later it was harder. In my code I use MSBuildWorkspace.OpenSolutionAsync and MSBuildWorkspace.OpenProjectAsync methods to respectively open the entire solution or a single project for further processing.

The next issue was that the first method called from within a unit test was returning an empty solution i.e. without any projects. Whereas the second one was throwing an exception with the message: The language 'C#' is not supported. What was strange these problems occurred only in unit tests! To investigate a problem I opened Exception settings window in Visual Studio and selected a check box next to Common Language Runtime Exceptions. Then, I run the unit tests one more time and Visual Studio quickly reported the exception in the line with MSBuildWorkspace.OpenProjectAsync:

System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.CodeAnalysis.CSharp.Workspaces...' or one of its dependencies.

It was even more strange because my unit test project was actually referencing Microsoft.CodeAnalysis.CSharp.Workspaces.dll! To double check, I went to the unit tests working directory. It is a folder called TestResults which by default is located in the solution directory. To my surprised this dll was missing!

Fortunately, I reminded myself the similar situation from the past. The problem is that MSTest doesn't copy all assemblies to the output directory by default. As far as I know it tries to figure out which assemblies are really needed by the code being tested. Here, I'm not sure but Microsoft.CodeAnalysis.CSharp.Workspaces.dll may be cumbersome because it is not directly referenced by other Roslyn assemblies. Instead, it is probably loaded dynamically when needed.

To fix a problem you can use the simple hack i.e. use directly any code from Microsoft.CodeAnalysis.CSharp.Workspaces.dll in your unit tests in the following way:

[ClassInitialize]
public static void ClassInitialize(TestContext ctx)
{
   var t = typeof(Microsoft.CodeAnalysis.CSharp.Formatting.LabelPositionOptions);
}

Why did I use LabelPositionOptions? Because majority of types defined in aforementioned assembly is internal and this one was the first public type I found :)

31/10/2016

Roslyn - How to create a custom debuggable scripting language 2?

Home


A screenshot comes from Visual Studio 2015

In the previous post I explained how to create a simple debuggable scripting language based on Roslyn compiler as a service. By debuggable I mean that it can be debugged in Visual Studio as any "normal" program for example written in C#.

27/10/2016

Roslyn - How to create a custom debuggable scripting language?

Home


A screenshot comes from Visual Studio 2015

Sometime ago I decided to play a little bit with Cakebuild. It's a build automation tool/system that allows you to write build scripts using C# domain specific language. What's more it is possible to debug these scripts in Visual Studio. It is interesting because Cake scripts are neither "normal" C# files nor are added to projects (csproj). I was curious how it was achieved and it is result of my analysis. I'll tell you how to create a simple debuggable scripting language. By debuggable I mean that it'll be possible to debug scripts in our language in Visual Studio almost as any "normal" program in C#. Cakebuild uses Roslyn i.e. a compiler as a service from Microsft and we'll do the same.