Getting a SemanticModel of a cshtml file?
Asked Answered
M

4

8

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?

Maharanee answered 8/2, 2015 at 8:37 Comment(3)
What the purpose of analyzing the SemanticModel?Paiz
@ErikPhilips I'm authoring a Visual Studio extension that needs to be able to ask semantic questions (call GetSymbolInfo, GetTypeInfo etc) given the context of the next statement (the 'yellow line') in the debugger. Currently, for Razor Views, I haven't been able to do this.Maharanee
@OmerRaviv Perhaps your question is closer to How do I get the SemanticModel of the current document? Alternatively, do you need Visual Studio extension? Can you not add your code as a Roslyn analyser? E.g. if you care about certain types of style violations you can enforce it effectively through an analyser instead of an extension. And at that point, having an extension is unnecessary.Tabor
C
8

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.

Cotoneaster answered 8/2, 2015 at 13:52 Comment(0)
T
7

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.

Tabor answered 8/2, 2015 at 8:42 Comment(1)
There is some more information about Roslyn and ASP.NET hereTabor
H
6

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;
        }
    }
}
Harve answered 8/6, 2015 at 14:31 Comment(0)
A
1

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?

Alexandria answered 8/2, 2015 at 12:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.