Skip to content

Instantly share code, notes, and snippets.

@MarkKharitonov
Last active February 13, 2021 20:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MarkKharitonov/65ddbbbec94b45aa9d03c0352a714e74 to your computer and use it in GitHub Desktop.
Save MarkKharitonov/65ddbbbec94b45aa9d03c0352a714e74 to your computer and use it in GitHub Desktop.
How to map all the types in the C# project to their respective source files using MSBuild and Roslyn APIs

Note: the following code ignores nested types.

Without further ado, here is the code:

using ManyConsole;
using Microsoft.Build.Evaluation;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace CSTool
{
    class TypeItem
    {
        public string TypeName { get; set; }
        public string FilePath { get; set; }
    }
    class TypeMapItem
    {
        public string AssemblyName { get; set; }
        public IEnumerable<TypeItem> Types { get; set; }
    }

    class MapTypesCmd : ConsoleCommand
    {
        private string m_projFilePath;

        public MapTypesCmd()
        {
            IsCommand("map-types", "Maps all the types of the given assembly.");

            HasRequiredOption("p|proj=", "The project source file.", v => m_projFilePath = Path.GetFullPath(v));
        }

        public override int Run(string[] remainingArguments)
        {
            Run(m_projFilePath);
            return 0;
        }

        public static void Run(string projFilePath)
        {
            var project = new Project(projFilePath);
            var res = new TypeMapItem
            {
                AssemblyName = project.GetPropertyValue("AssemblyName"),
                Types = YieldTypeItems(project)
            };
            var serializer = JsonSerializer.CreateDefault();
            serializer.Formatting = Formatting.Indented;
            using var w = new JsonTextWriter(Console.Out);
            serializer.Serialize(w, res);
        }

        private static IEnumerable<TypeItem> YieldTypeItems(Project project)
        {
            var curDir = Path.GetFullPath($"{project.FullPath}\\..");
            Directory.SetCurrentDirectory(curDir);
            curDir += "\\";
            curDir = curDir.Substring(project.GetPropertyValue("WorkspaceRoot").Length);
            foreach (var compileItem in project.AllEvaluatedItems.Cast<ProjectItem>().Where(item => item.ItemType == "Compile"))
            {
                var filePath = compileItem.EvaluatedInclude;
                var tree = CSharpSyntaxTree.ParseText(File.ReadAllText(filePath));
                var root = tree.GetCompilationUnitRoot();
                foreach (var typeItem in YieldTypes(filePath, root.ChildNodes(), "").Select(t => new TypeItem
                {
                    TypeName = t,
                    FilePath = curDir + filePath
                }))
                {
                    yield return typeItem;
                }
            }
        }

        private static IEnumerable<string> YieldTypes(string filePath, IEnumerable<SyntaxNode> nodes, string prefix)
        {
            foreach (var member in nodes.OfType<MemberDeclarationSyntax>())
            {
                SyntaxToken id;
                TypeParameterListSyntax typeParamList = null;
                if (member is DelegateDeclarationSyntax del)
                {
                    id = del.Identifier;
                    typeParamList = del.TypeParameterList;
                }
                else if (member is TypeDeclarationSyntax type)
                {
                    id = type.Identifier;
                    typeParamList = type.TypeParameterList;
                }
                else if (member is EnumDeclarationSyntax enumeration)
                {
                    id = enumeration.Identifier;
                }
                else if (member is NamespaceDeclarationSyntax ns)
                {
                    foreach (var t in YieldTypes(filePath, ns.ChildNodes(), prefix + ns.Name.ToString() + "."))
                    {
                        yield return t;
                    }
                    continue;
                }
                else
                {
                    throw new ApplicationException($"Unexpected declaration found in {filePath} @ {member.SpanStart} - {member}");
                }
                var typeParamListText = "";
                if (typeParamList != null)
                {
                    typeParamListText = "<" + string.Join(",", typeParamList.Parameters.Select(o => o.Identifier.ToString())) + ">";
                }
                yield return prefix + id.ToString() + typeParamListText;
            }
        }
    }
}

Here is the project file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="ManyConsole" Version="2.0.1" />
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
    <PackageReference Include="System.ComponentModel.Composition" Version="5.0.0" />
    <PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
  </ItemGroup>

  <ItemGroup>
    <Reference Include="Microsoft.Build">
      <HintPath>..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Build.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Build.Framework">
      <HintPath>..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Build.Framework.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Build.Utilities.Core">
      <HintPath>..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Microsoft.Build.Utilities.Core.dll</HintPath>
    </Reference>
  </ItemGroup>

</Project>

A very important note - I could not make the MSBuild API NuGet packages work for me. My experience is described in this SO question - https://stackoverflow.com/questions/66146972/whats-the-correct-usage-of-microsoft-build-evaluation-fast-forward-to-2021. As a result I take the MSBuild DLLs directly from VS 2019 installation folder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment