Getting a SemanticModel of a cshtml file? - c#

I'd like to use Roslyn to analyze semantic information within the context of a block of C# code inside a Razor View.
Is there any way (within Visual Studio 2015, or even in a unit test) to get the SemanticModel that represents this code?

Razor files contain a C# projection buffer with the generated C# code (including the parts that you don't write yourself). This buffer has full Roslyn services and is exactly what you're looking for.
You need to walk through the TextView's BufferGraph and find the CSharp buffer; you can then get its Document and semantic model.
If you're starting from the cursor location, you need simply need to map that location to a CSharp buffer.
Note that it is perfectly legal for a TextView to contain multiple CSharp buffers. (although the Razor editor will never do that)
If you aren't working in a TextView, you need to do all of this yourself; you need to run the Razor source through the Razor compiler to get the generated C# source, then compile that with Roslyn to get a semantic model.

Extract the code representing the view from the Razor view file using RazorTemplateEngine.GenerateCode and CSharpCodeProvider.GenerateCodeFromCompileUnit (or the VBCodeProvider if you want the intermediate source as VB.NET). You can then use Roslyn to parse the code.
There's an example of using Roslyn with Razor view files here.
Take note that GenerateCode carries a caveat:
This type/member supports the .NET Framework infrastructure and is not intended to be used directly from your code.

Just in case anyone else gets stuck on this, I have mini sample app which may help.
I had a CMS class like this:
public partial class CMS
{
public static string SomeKey
{
get { return (string) ResourceProvider.GetResource("some_key"); }
}
// ... and many more ...
}
... and I wanted to find out which of these were used throughout my solution for a report ... Enter Roslyn!
The following app will print out the count for the used and unused references:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Razor;
namespace TranslationSniffer
{
class Program
{
static void Main(string[] args)
{
new Program().Go().Wait();
}
public async Task Go()
{
// Roslyn!
var ws = MSBuildWorkspace.Create();
// Store the translation keys...
List<string> used = new List<string>();
List<string> delete = new List<string>();
string solutionRoot = #"C:\_Code\PathToProject\";
string sln = solutionRoot + "MySolution.sln";
// Load the solution, and find all the cshtml Razor views...
var solution = await ws.OpenSolutionAsync(sln);
var mainProj = solution.Projects.Where(x => x.Name == "ConsumerWeb").Single();
FileInfo[] cshtmls = new DirectoryInfo(solutionRoot).GetFiles("*.cshtml", SearchOption.AllDirectories);
// Go through each Razor View - generate the equivalent CS and add to the project for compilation.
var host = new RazorEngineHost(RazorCodeLanguage.Languages["cshtml"]);
var razor = new RazorTemplateEngine(host);
var cs = new CSharpCodeProvider();
var csOptions = new CodeGeneratorOptions();
foreach (var cshtml in cshtmls)
{
using (StreamReader re = new StreamReader(cshtml.FullName))
{
try
{
// Let Razor do it's thang...
var compileUnit = razor.GenerateCode(re).GeneratedCode;
// Pull the code into a stringbuilder, and append to the main project:
StringBuilder sb = new StringBuilder();
using (StringWriter rw = new StringWriter(sb))
{
cs.GenerateCodeFromCompileUnit(compileUnit, rw, csOptions);
}
// Get the new immutable project
var doc = mainProj.AddDocument(cshtml.Name + ".cs", sb.ToString());
mainProj = doc.Project;
}
catch(Exception ex)
{
Console.WriteLine("Compile fail for: {0}", cshtml.Name);
// throw;
}
continue;
}
}
// We now have a new immutable solution, as we have changed the project instance...
solution = mainProj.Solution;
// Pull out our application translation list (its in a static class called 'CMS'):
var mainCompile = await mainProj.GetCompilationAsync();
var mainModel = mainCompile.GetTypeByMetadataName("Resources.CMS");
var translations = mainModel.GetMembers().Where(x => x.Kind == SymbolKind.Property).ToList();
foreach (var translation in translations)
{
var references = await SymbolFinder.FindReferencesAsync(translation, solution) ;
if (!references.First().Locations.Any())
{
Console.WriteLine("{0} translation is not used!", translation.Name);
delete.Add(translation.Name);
}
else
{
Console.WriteLine("{0} :in: {1}", translation.Name, references.First().Locations.First().Document.Name);
used.Add(translation.Name);
}
}
Console.WriteLine();
Console.WriteLine("Used references {0}. Unused references: {1}", used.Count, delete.Count);
return;
}
}
}

Roslyn only models cshtml files while they are open, but during that time they are similar to every other source file in the Workspace model.
Is there something specific you have tried that isn't working?

Related

How to manipulate CSHTML with Roslyn

I'm trying to use Roslyn to do some mass refactoring on my code.
The idea is to remove a specific using and insert them directly in the code.
For example
using My.Awesome.Namespace;
...
var temp = MyType.Prop;
would become
var temp = My.Awesome.Namespace.MyType.Prop;
I already have a working solution for .cs files using MSBuildWorkspace to parse my solution, find the using reference and replace them in the file. But I can't find how to do the same on the cshtml files.
They do not appear in the Documents property of my project.
Any idea?
Here is the code I'm using to parse the solution
public void Process(string solutionPath, string projectName, string baseNamespace)
{
//Force import csharp projects
MSBuildLocator.RegisterDefaults();
var _ = typeof(Microsoft.CodeAnalysis.CSharp.Formatting.CSharpFormattingOption
using (var msWorkspace = MSBuildWorkspace.Create())
{
var solution = msWorkspace.OpenSolutionAsync(solutionPath).Result;
var project = solution.Projects.FirstOrDefault(x => x.Name == projectName)
if (project == null)
throw new InvalidOperationException();
foreach (var document in project.Documents)
{
if (document.SourceCodeKind != SourceCodeKind.Regular)
continue;
Console.WriteLine("Fixing file " + document.Name);
// Remove using of baseNamespace from doc
var newDoc = RemoveUsing(document, baseNamespace);
solution = solution.WithDocumentSyntaxRoot(document.Id, newDoc);
}
msWorkspace.TryApplyChanges(solution);
}
}
Here is a decent solution for scanning, parsing, compiling, and getting the semantic models for a solution's .cshtml files: Getting a SemanticModel of a cshtml file?

Cannot get SyntaxTree from Compilation object

I'm a beginner of roslyn, so I tried to start learning it by making a very simple console application, which is introduced in the famous tutorial site. (https://riptutorial.com/roslyn/example/16545/introspective-analysis-of-an-analyzer-in-csharp), and it didn't work well.
The Cosole Application I made is of .NET Framework (target Framework version is 4.7.2), and not of .NET Core nor .NET standard.
I added the NuGet package Microsoft.CodeAnalysis, and Microsoft.CodeAnalysis.Workspaces.MSBuild, then wrote a simple code as I show below.
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
using System;
using System.Linq;
namespace SimpleRoslynConsole
{
class Program
{
static void Main(string[] args)
{
// Declaring a variable with the current project file path.
// *** You have to change this path to fit your development environment.
const string projectPath =
#"C:\Users\[MyName]\Source\Repos\RoslynTrialConsole01\RoslynTrialConsole01.csproj";
var workspace = MSBuildWorkspace.Create();
var project = workspace.OpenProjectAsync(projectPath).Result;
// [**1]Getting the compilation.
var compilation = project.GetCompilationAsync().Result;
// [**2]As this is a simple single file program, the first syntax tree will be the current file.
var syntaxTree = compilation.SyntaxTrees.FirstOrDefault();
if (syntaxTree != null)
{
var rootSyntaxNode = syntaxTree.GetRootAsync().Result;
var firstLocalVariablesDeclaration = rootSyntaxNode.DescendantNodesAndSelf()
.OfType<LocalDeclarationStatementSyntax>().First();
var firstVariable = firstLocalVariablesDeclaration.Declaration.Variables.First();
var variableInitializer = firstVariable.Initializer.Value.GetFirstToken().ValueText;
Console.WriteLine(variableInitializer);
}
else
{
Console.WriteLine("Could not get SyntaxTrees from this projects.");
}
Console.WriteLine("Hit any key.");
Console.ReadKey();
}
}
}
My problem is that, SyntaxTrees property of Compilation object returns null in [**2]mark. Naturally, following FirstOrDefault method returns null.
I've tried several other code. I found I could get SyntaxTree from CSharp code text, by using CSharpSyntaxTree.ParseText method. But I couldn't get any from source code, by the sequence of
var workspace = MSBuildWorkspace.Create();
var project = workspace.OpenProjectAsync(projectPath).Result;
var compilation = project.GetCompilationAsync().Result;
What I'd like to know is if I miss something to get Syntax information from source code by using above process.
I'll appreciate someone give me a good advice.
I think the issue is that .net framework projects have their source files paths within their .csproj. And opening project works right away.
For .net core project you have no such information and, maybe, this is why Workspace instance doesn't know what to load and so loads nothing.
At least specifying .cs files as added documents does the trick. Try to apply this:
static class ProjectExtensions
{
public static Project AddDocuments(this Project project, IEnumerable<string> files)
{
foreach (string file in files)
{
project = project.AddDocument(file, File.ReadAllText(file)).Project;
}
return project;
}
private static IEnumerable<string> GetAllSourceFiles(string directoryPath)
{
var res = Directory.GetFiles(directoryPath, "*.cs", SearchOption.AllDirectories);
return res;
}
public static Project WithAllSourceFiles(this Project project)
{
string projectDirectory = Directory.GetParent(project.FilePath).FullName;
var files = GetAllSourceFiles(projectDirectory);
var newProject = project.AddDocuments(files);
return newProject;
}
}
Method WithAllsourceFiles will return you the project, compilation of which will in its turn have all syntax trees you would expect of it, as you would have in Visual Studio
MsBuildWorkspace won't work correctly unless you have all the same redirects in your app's app.config file that msbuild.exe.config has in it. Without the redirects, it's probably failing to load the msbuild libraries. You need to find the msbuild.exe.config file that is on your system and copy the <assemblyBinding> elements related to Microsoft.Build assemblies into your app.config. Make sure you place them under the correct elements configuration/runtime.
I searched various sample programs on the net and found the most reliable and safest method. The solution is to create a static method which returns SyntaxTrees in designated File as follow.
private static Compilation CreateTestCompilation()
{
var found = false;
var di = new DirectoryInfo(Environment.CurrentDirectory);
var fi = di.GetFiles().Where((crt) => { return crt.Name.Equals("program.cs", StringComparison.CurrentCultureIgnoreCase); }).FirstOrDefault();
while ((fi == null) || (di.Parent == null))
{
di = new DirectoryInfo(di.Parent.FullName);
fi = di.GetFiles().Where((crt) => { return crt.Name.Equals("program.cs", StringComparison.CurrentCultureIgnoreCase); }).FirstOrDefault();
if (fi != null)
{
found = true;
break;
}
}
if (!found)
{
return null;
}
var targetPath = di.FullName + #"\Program.cs";
var targetText = File.ReadAllText(targetPath);
var targetTree =
CSharpSyntaxTree.ParseText(targetText)
.WithFilePath(targetPath);
var target2Path = di.FullName + #"\TypeInferenceRewriter.cs";
var target2Text = File.ReadAllText(target2Path);
var target2Tree =
CSharpSyntaxTree.ParseText(target2Text)
.WithFilePath(target2Path);
SyntaxTree[] sourceTrees = { programTree, target2Tree };
MetadataReference mscorlib =
MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
MetadataReference codeAnalysis =
MetadataReference.CreateFromFile(typeof(SyntaxTree).Assembly.Location);
MetadataReference csharpCodeAnalysis =
MetadataReference.CreateFromFile(typeof(CSharpSyntaxTree).Assembly.Location);
MetadataReference[] references = { mscorlib, codeAnalysis, csharpCodeAnalysis };
return CSharpCompilation.Create("TransformationCS",
sourceTrees,
references,
new CSharpCompilationOptions(
OutputKind.ConsoleApplication));
}
And the caller program will be like this.
static void Main(string[] args)
{
var test = CreateTestCompilation();
if (test == null)
{
return;
}
foreach (SyntaxTree sourceTree in test.SyntaxTrees)
{
Console.WriteLine(souceTree.ToFullString());
}
}
Of course, many improvements are needed to put it to practical use.

Locate models in Stanford NLP for .NET

I am trying to use Stanford NLP for .NET. I am very new to this and I am having trouble in loading the models.
I have read the same kind of issue in https://sergey-tihon.github.io/Stanford.NLP.NET//faq.html and in Stanford.NLP for .NET not loading models. But I have no idea where they have saved the "stanford-corenlp-full-2016-10-31" folder.
This is where I have the C# code. C:\Users\Kabi\source\repos\Search\Search
And the stanford-corenlp-full-2017-06-09 folder is here - C:\Users\Kabi\source\repos\Search.
I have extracted the stanford-corenlp-3.8.0-models.jar in C:\Users\Kabi\source\repos\Search\stanford-corenlp-full-2017-06-09
This is my C# code.
Browse.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.IO;
using java.util;
using java.io;
using edu.stanford.nlp.pipeline;
using Console = System.Console;
namespace Search
{
class Browse
{
public void StanfordNLP()
{
// Path to the folder with models extracted from `stanford-corenlp-3.8.0-models.jar`
var jarRoot = #"..\stanford-corenlp-full-2017-06-09";
// Text for processing
var text = "Kosgi Santosh sent an email to Stanford University. He didn't get a reply.";
// Annotation pipeline configuration
var props = new Properties();
props.setProperty("annotators", "tokenize, ssplit, pos, lemma, ner, parse, dcoref");
props.setProperty("ner.useSUTime", "0");
// We should change current directory, so StanfordCoreNLP could find all the model files automatically
var curDir = Environment.CurrentDirectory;
Directory.SetCurrentDirectory(jarRoot);
var pipeline = new StanfordCoreNLP(props);
Directory.SetCurrentDirectory(curDir);
// Annotation
var annotation = new Annotation(text);
pipeline.annotate(annotation);
// Result - Pretty Print
using (var stream = new ByteArrayOutputStream())
{
pipeline.prettyPrint(annotation, new PrintWriter(stream));
Console.WriteLine(stream.toString());
stream.close();
}
}
}
}
How can I properly load the models?
It seems you are not able to locate your modals files try using this :-
I had copied stanford-corenlp-full-2017-06-09 under the root folder of my project i.e. NLP as displayed in image 1 in the picture itself the second one shows how I have extracted the stanford-corenlp-3.8.0-models.jar
The directory Structure of files
after doing so use
var jarRoot = #"......\stanford-corenlp-full-2016-10-31\stanford-corenlp-3.7.0-models";
It works Simply

Visual Studio 2015 build drop location

Our company recently updated TFS to 2015 update 1. After that context menu item named Drop folder disappeared from completed builds. I found nothing about it and how to bring it back. When I click Open on completed build, VS opens web version of TFS where I forced to click through the menus and copy drop folder path manually. So I decided to write a simple extension that will add this item to the menu.
Some googling brought me to this page. But it seems that the example code is quite old and not working in VS2015:
IVsTeamFoundationBuild vsTfBuild = (IVsTeamFoundationBuild)GetService(typeof(IVsTeamFoundationBuild));
IBuildDetail[] builds = vsTfBuild.BuildExplorer.CompletedView.SelectedBuilds;
Property SelectedBuilds is always empty. I suppose that it relates to old window from VS2010. It returns items that are instance of IBuildDetail interface.
So I found this piece of code here:
var teamExplorer = (ITeamExplorer)ServiceProvider.GetService(typeof(ITeamExplorer));
var page = teamExplorer.CurrentPage;
var buildsPageExt = (IBuildsPageExt)page.GetExtensibilityService(typeof(IBuildsPageExt));
var build = buildsPageExt.SelectedBuilds[0];
Here build is the instance of IBuildModel interface. It lacks DropLocation property.
Is there any way to found drop location of selected build? Or maybe latest build?
You can use IBuildDedetail.DropLocation in .NET client libraries for Visual Studio Team Services (and TFS). Basic code for your reference:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Client;
namespace BuildAPI
{
class Program
{
static void Main(string[] args)
{
string project = "http://xxx.xxx.xxx.xxx";
TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(new Uri(project));
IBuildServer ibs = tpc.GetService<IBuildServer>();
var builds = ibs.QueryBuilds("TeamProjectName");
foreach (IBuildDetail ibd in builds)
{
Console.WriteLine(ibd.DropLocation);
Console.ReadLine();
}
}
}
}
So, after digging through TFS API, I ended up with this workaround.
private void MenuItemCallback(object sender, EventArgs e)
{
var context = (ITeamFoundationContextManager)ServiceProvider.GetService(typeof(ITeamFoundationContextManager));
IBuildServer buildServer = context.CurrentContext.TeamProjectCollection.GetService<IBuildServer>();
var teamExplorer = (ITeamExplorer)ServiceProvider.GetService(typeof(ITeamExplorer));
var buildsPageExt = (IBuildsPageExt)teamExplorer.CurrentPage.GetExtensibilityService(typeof(IBuildsPageExt));
var menuCommand = (MenuCommand)sender;
if (menuCommand.CommandID.Guid == CommandSetCompleted)
{
foreach (var buildDetail in buildsPageExt.SelectedBuilds)
Process.Start("explorer.exe", GetBuild(buildServer, buildDetail).DropLocation);
}
if (menuCommand.CommandID.Guid == CommandSetFavorite)
{
var definitions = buildsPageExt.SelectedFavoriteDefinitions.Concat(buildsPageExt.SelectedXamlDefinitions).ToArray();
foreach (var build in GetLatestSuccessfulBuild(buildServer, definitions))
Process.Start("explorer.exe", build.DropLocation);
}
}
private IBuildDetail GetBuild(IBuildServer buildServer, IBuildModel buildModel)
{
Uri buildUri = new Uri(buildModel.GetType().GetProperty("UriToOpen").GetValue(buildModel).ToString());
return buildServer.GetBuild(buildUri);
}
private IBuildDetail[] GetLatestSuccessfulBuild(IBuildServer buildServer, IDefinitionModel[] buildDefinitions)
{
var spec = buildServer.CreateBuildDetailSpec(buildDefinitions.Select(bd => bd.Uri));
spec.MaxBuildsPerDefinition = 1;
spec.QueryOrder = BuildQueryOrder.FinishTimeDescending;
spec.Status = BuildStatus.Succeeded;
var builds = buildServer.QueryBuilds(spec);
return builds.Builds;
}

Roslyn / Find References - Can't properly load Workspace

I'm trying to write some code to find all method invocations of any given method as I am looking to create an open source UML Sequence Diagramming tool. I'm having trouble, however, getting past the first few lines of code :/
The API appears to have changed drastically and I can't seem to infer proper usage by looking at the code.
When I do:
var workspace = new CustomWorkspace();
string solutionPath = #"C:\Workspace\RoslynTest\RoslynTest.sln";
var solution = workspace.CurrentSolution;
I find that workspace.CurrentSolution has 0 Projects. I figured this would be the equivalent to what was previously Workspace.LoadSolution( string solutionFile ) which would then supposedly contain any Projects in the Solution, but I am not finding any success with this path.
I am terribly confused 0.o
If someone could offer some additional guidance as to how I can use the FindReferences API to identify all invocations of a particular method, it would be very much appreciated!
Alternatively, would I be better off taking a static-analysis approach? I would like to support things like lambdas, iterator methods and async.
====================================================================
Edit -
Here is a full example based on the accepted answer:
using System.Linq;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
using Microsoft.CodeAnalysis.FindSymbols;
using System.Diagnostics;
namespace RoslynTest
{
class Program
{
static void Main(string[] args)
{
string solutionPath = #"C:\Workspace\RoslynTest\RoslynTest.sln";
var workspace = MSBuildWorkspace.Create();
var solution = workspace.OpenSolutionAsync(solutionPath).Result;
var project = solution.Projects.Where(p => p.Name == "RoslynTest").First();
var compilation = project.GetCompilationAsync().Result;
var programClass = compilation.GetTypeByMetadataName("RoslynTest.Program");
var barMethod = programClass.GetMembers("Bar").First();
var fooMethod = programClass.GetMembers("Foo").First();
var barResult = SymbolFinder.FindReferencesAsync(barMethod, solution).Result.ToList();
var fooResult = SymbolFinder.FindReferencesAsync(fooMethod, solution).Result.ToList();
Debug.Assert(barResult.First().Locations.Count() == 1);
Debug.Assert(fooResult.First().Locations.Count() == 0);
}
public bool Foo()
{
return "Bar" == Bar();
}
public string Bar()
{
return "Bar";
}
}
}
CustomWorkspace is
A workspace that allows manual addition of projects and documents.
Since you're trying to load a solution, you should use the MSBuildWorkspace, which is
A workspace that can be populated by opening MSBuild solution and project files.
You can create a new MSBuildWorkspace and call OpenSolutionAsync with your solutionPath. For the reference finding part, take a look at the SymbolFinder.
Solutions are an MSBuild concept.
You need to create an MSBuildWorkspace and call OpenSolutionAsync().
string solutionPath = #"C:\Workspace\RoslynTest\RoslynTest.sln";
creates a local variable. It has no influence on your CustomWorkspace object.

Categories

Resources