Analisando arquivos do Visual Studio Solution

Como posso analisar arquivos de solução do Visual Studio (SLN) no .NET? Eu gostaria de escrever um aplicativo que mescla várias soluções em uma ao salvar a ordem de construção relativa.

A versão .NET 4.0 do assembly Microsoft.Build contém uma class SolutionParser no namespace Microsoft.Build.Construction que analisa arquivos de solução do Visual Studio.

Infelizmente, essa class é interna, mas envolvi parte dessa funcionalidade em uma class que usa reflection para obter algumas propriedades comuns que você pode achar úteis.

public class Solution { //internal class SolutionParser //Name: Microsoft.Build.Construction.SolutionParser //Assembly: Microsoft.Build, Version=4.0.0.0 static readonly Type s_SolutionParser; static readonly PropertyInfo s_SolutionParser_solutionReader; static readonly MethodInfo s_SolutionParser_parseSolution; static readonly PropertyInfo s_SolutionParser_projects; static Solution() { s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); if (s_SolutionParser != null) { s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance); } } public List Projects { get; private set; } public Solution(string solutionFileName) { if (s_SolutionParser == null) { throw new InvalidOperationException("Can not find type 'Microsoft.Build.Construction.SolutionParser' are you missing a assembly reference to 'Microsoft.Build.dll'?"); } var solutionParser = s_SolutionParser.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(null); using (var streamReader = new StreamReader(solutionFileName)) { s_SolutionParser_solutionReader.SetValue(solutionParser, streamReader, null); s_SolutionParser_parseSolution.Invoke(solutionParser, null); } var projects = new List(); var array = (Array)s_SolutionParser_projects.GetValue(solutionParser, null); for (int i = 0; i < array.Length; i++) { projects.Add(new SolutionProject(array.GetValue(i))); } this.Projects = projects; } } [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class SolutionProject { static readonly Type s_ProjectInSolution; static readonly PropertyInfo s_ProjectInSolution_ProjectName; static readonly PropertyInfo s_ProjectInSolution_RelativePath; static readonly PropertyInfo s_ProjectInSolution_ProjectGuid; static readonly PropertyInfo s_ProjectInSolution_ProjectType; static SolutionProject() { s_ProjectInSolution = Type.GetType("Microsoft.Build.Construction.ProjectInSolution, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); if (s_ProjectInSolution != null) { s_ProjectInSolution_ProjectName = s_ProjectInSolution.GetProperty("ProjectName", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_RelativePath = s_ProjectInSolution.GetProperty("RelativePath", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectGuid = s_ProjectInSolution.GetProperty("ProjectGuid", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectType = s_ProjectInSolution.GetProperty("ProjectType", BindingFlags.NonPublic | BindingFlags.Instance); } } public string ProjectName { get; private set; } public string RelativePath { get; private set; } public string ProjectGuid { get; private set; } public string ProjectType { get; private set; } public SolutionProject(object solutionProject) { this.ProjectName = s_ProjectInSolution_ProjectName.GetValue(solutionProject, null) as string; this.RelativePath = s_ProjectInSolution_RelativePath.GetValue(solutionProject, null) as string; this.ProjectGuid = s_ProjectInSolution_ProjectGuid.GetValue(solutionProject, null) as string; this.ProjectType = s_ProjectInSolution_ProjectType.GetValue(solutionProject, null).ToString(); } } 

Observe que você precisa alterar sua estrutura de destino para ".NET Framework 4" (não o perfil de cliente) para poder adicionar a referência Microsoft.Build ao seu projeto.

Com o Visual Studio 2015, agora há uma class SolutionFile acessível publicamente que pode ser usada para analisar arquivos de solução:

 using Microsoft.Build.Construction; var _solutionFile = SolutionFile.Parse(path); 

Esta class é encontrada no assembly Microsoft.Build.dll 14.0.0.0 . No meu caso, estava localizado em:

 C:\Program Files (x86)\Reference Assemblies\Microsoft\MSBuild\v14.0\Microsoft.Build.dll 

Obrigado ao Phil por apontar isso !

Não sei se alguém ainda está procurando soluções para esse problema, mas encontrei um projeto que parece fazer exatamente o que é necessário. https://slntools.codeplex.com/ Uma das funções dessa ferramenta é mesclar várias soluções juntas.

Os JetBrains (os criadores do Resharper) têm habilidades de análise de sln em suas montagens (não é necessário nenhum reflexo). É provavelmente mais robusto do que as soluções de código aberto existentes sugeridas aqui (sem falar nos hacks ReGex). Tudo o que você precisa fazer é:

  • Baixe as ferramentas de linha de comando ReSharper (grátis).
  • Adicione o seguinte como referências ao seu projeto
    • JetBrains.Platform.ProjectModel
    • JetBrains.Platform.Util
    • JetBrains.Platform.Interop.WinApi

A biblioteca não está documentada, mas o Reflector (ou mesmo a dotPeek) é seu amigo. Por exemplo:

 public static void PrintProjects(string solutionPath) { var slnFile = SolutionFileParser.ParseFile(FileSystemPath.Parse(solutionPath)); foreach (var project in slnFile.Projects) { Console.WriteLine(project.ProjectName); Console.WriteLine(project.ProjectGuid); Console.WriteLine(project.ProjectTypeGuid); foreach (var kvp in project.ProjectSections) { Console.WriteLine(kvp.Key); foreach (var projectSection in kvp.Value) { Console.WriteLine(projectSection.SectionName); Console.WriteLine(projectSection.SectionValue); foreach (var kvpp in projectSection.Properties) { Console.WriteLine(kvpp.Key); Console.WriteLine(string.Join(",", kvpp.Value)); } } } } } 

Eu realmente não posso oferecer-lhe uma biblioteca e meu palpite é que não existe um que existe lá fora. Mas passei um bom tempo mexendo com arquivos .sln em cenários de edição de lote e descobri que o Powershell é uma ferramenta muito útil para essa tarefa. O formato .SLN é bastante simples e pode ser quase completamente analisado com algumas expressões rápidas e sujas. Por exemplo

Arquivos de projeto incluídos.

 gc ConsoleApplication30.sln | ? { $_ -match "^Project" } | %{ $_ -match ".*=(.*)$" | out-null ; $matches[1] } | %{ $_.Split(",")[1].Trim().Trim('"') } 

Nem sempre é bonito, mas é uma maneira eficiente de fazer o processamento em lote.

Nós resolvemos um problema semelhante de mesclar soluções automaticamente escrevendo um plugin do Visual Studio que criou uma nova solução, em seguida, pesquisamos o arquivo * .sln e os importamos para o novo usando:

 dte2.Solution.AddFromFile(solutionPath, false); 

Nosso problema era um pouco diferente, pois queríamos que o VS classificasse a ordem de construção para nós, então convertíamos quaisquer referências de dll para referências de projeto sempre que possível.

Em seguida, automatizamos isso em um processo de criação, executando o VS por meio da automação COM.

Esta solução foi um pouco Heath Robinson, mas tinha a vantagem que o VS estava fazendo a edição para que nosso código não dependesse do formato do arquivo sln. O que foi útil quando nos mudamos do VS 2005 para 2008 e novamente para 2010.

Tudo é ótimo, mas eu também queria obter capacidade de geração de sln – no instantâneo de código acima você está apenas analisando arquivos .sln – eu queria fazer algo semelhante exceto para ser capaz de gerar sln com pequenas modificações de volta para o arquivo .sln . Tais casos poderiam ser, por exemplo, portar o mesmo projeto para diferentes plataformas .NET. Por enquanto, é apenas uma nova geração, mas depois vou expandi-la para projetos também.

Eu acho que também queria demonstrar o poder das expressões regulares e das interfaces nativas. (Menor quantidade de código com mais funcionalidade)

Atualização 4.1.2017 Eu criei um repository svn separado para analisar a solução .sln: https://sourceforge.net/p/syncproj/code/HEAD/tree/

Abaixo está meu próprio trecho de amostra de código (predecessor). Você está livre para usar qualquer um deles.

É possível que, no futuro, o código de análise da solução baseada em svn também seja atualizado com resources de geração.

Atualização 4.2.2017 O código fonte no SVN também suporta a geração .sln.

 using System; using System.Linq; using System.Collections.Generic; using System.IO; using System.Diagnostics; using System.Text.RegularExpressions; using System.Text; public class Program { [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class SolutionProject { public string ParentProjectGuid; public string ProjectName; public string RelativePath; public string ProjectGuid; public string AsSlnString() { return "Project(\"" + ParentProjectGuid + "\") = \"" + ProjectName + "\", \"" + RelativePath + "\", \"" + ProjectGuid + "\""; } } ///  /// .sln loaded into class. ///  public class Solution { public List slnLines; // List of either String (line format is not intresting to us), or SolutionProject. ///  /// Loads visual studio .sln solution ///  ///  /// The file specified in path was not found. public Solution( string solutionFileName ) { slnLines = new List(); String slnTxt = File.ReadAllText(solutionFileName); string[] lines = slnTxt.Split('\n'); //Match string like: Project("{66666666-7777-8888-9999-AAAAAAAAAAAA}") = "ProjectName", "projectpath.csproj", "{11111111-2222-3333-4444-555555555555}" Regex projMatcher = new Regex("Project\\(\"(?{[A-F0-9-]+})\"\\) = \"(?.*?)\", \"(?.*?)\", \"(?{[A-F0-9-]+})"); Regex.Replace(slnTxt, "^(.*?)[\n\r]*$", new MatchEvaluator(m => { String line = m.Groups[1].Value; Match m2 = projMatcher.Match(line); if (m2.Groups.Count < 2) { slnLines.Add(line); return ""; } SolutionProject s = new SolutionProject(); foreach (String g in projMatcher.GetGroupNames().Where(x => x != "0")) /* "0" - RegEx special kind of group */ s.GetType().GetField(g).SetValue(s, m2.Groups[g].ToString()); slnLines.Add(s); return ""; }), RegexOptions.Multiline ); } ///  /// Gets list of sub-projects in solution. ///  /// true if get also sub-folders. public List GetProjects( bool bGetAlsoFolders = false ) { var q = slnLines.Where( x => x is SolutionProject ).Select( i => i as SolutionProject ); if( !bGetAlsoFolders ) // Filter away folder names in solution. q = q.Where( x => x.RelativePath != x.ProjectName ); return q.ToList(); } ///  /// Saves solution as file. ///  public void SaveAs( String asFilename ) { StringBuilder s = new StringBuilder(); for( int i = 0; i < slnLines.Count; i++ ) { if( slnLines[i] is String ) s.Append(slnLines[i]); else s.Append((slnLines[i] as SolutionProject).AsSlnString() ); if( i != slnLines.Count ) s.AppendLine(); } File.WriteAllText(asFilename, s.ToString()); } } static void Main() { String projectFile = @"yourown.sln"; try { String outProjectFile = Path.Combine(Path.GetDirectoryName(projectFile), Path.GetFileNameWithoutExtension(projectFile) + "_2.sln"); Solution s = new Solution(projectFile); foreach( var proj in s.GetProjects() ) { Console.WriteLine( proj.RelativePath ); } SolutionProject p = s.GetProjects().Where( x => x.ProjectName.Contains("Plugin") ).First(); p.RelativePath = Path.Combine( Path.GetDirectoryName(p.RelativePath) , Path.GetFileNameWithoutExtension(p.RelativePath) + "_Variation" + ".csproj"); s.SaveAs(outProjectFile); } catch (Exception ex) { Console.WriteLine("Error: " + ex.Message); } } } 

Eu expus, determinado que as classs MSBuild podem ser usadas para manipular as estruturas subjacentes. Eu terei mais código no meu site mais tarde.

 // VSSolution using System; using System.Reflection; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using System.IO; using AbstractX.Contracts; namespace VSProvider { public class VSSolution : IVSSolution { //internal class SolutionParser //Name: Microsoft.Build.Construction.SolutionParser //Assembly: Microsoft.Build, Version=4.0.0.0 static readonly Type s_SolutionParser; static readonly PropertyInfo s_SolutionParser_solutionReader; static readonly MethodInfo s_SolutionParser_parseSolution; static readonly PropertyInfo s_SolutionParser_projects; private string solutionFileName; private List projects; public string Name { get { return Path.GetFileNameWithoutExtension(solutionFileName); } } public IEnumerable Projects { get { return projects; } } static VSSolution() { s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance); } public string SolutionPath { get { var file = new FileInfo(solutionFileName); return file.DirectoryName; } } public VSSolution(string solutionFileName) { if (s_SolutionParser == null) { throw new InvalidOperationException("Can not find type 'Microsoft.Build.Construction.SolutionParser' are you missing a assembly reference to 'Microsoft.Build.dll'?"); } var solutionParser = s_SolutionParser.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(null); using (var streamReader = new StreamReader(solutionFileName)) { s_SolutionParser_solutionReader.SetValue(solutionParser, streamReader, null); s_SolutionParser_parseSolution.Invoke(solutionParser, null); } this.solutionFileName = solutionFileName; projects = new List(); var array = (Array)s_SolutionParser_projects.GetValue(solutionParser, null); for (int i = 0; i < array.Length; i++) { projects.Add(new VSProject(this, array.GetValue(i))); } } public void Dispose() { } } } // VSProject using System; using System.Reflection; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using System.IO; using System.Xml; using AbstractX.Contracts; using System.Collections; namespace VSProvider { [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class VSProject : IVSProject { static readonly Type s_ProjectInSolution; static readonly Type s_RootElement; static readonly Type s_ProjectRootElement; static readonly Type s_ProjectRootElementCache; static readonly PropertyInfo s_ProjectInSolution_ProjectName; static readonly PropertyInfo s_ProjectInSolution_ProjectType; static readonly PropertyInfo s_ProjectInSolution_RelativePath; static readonly PropertyInfo s_ProjectInSolution_ProjectGuid; static readonly PropertyInfo s_ProjectRootElement_Items; private VSSolution solution; private string projectFileName; private object internalSolutionProject; private List items; public string Name { get; private set; } public string ProjectType { get; private set; } public string RelativePath { get; private set; } public string ProjectGuid { get; private set; } public string FileName { get { return projectFileName; } } static VSProject() { s_ProjectInSolution = Type.GetType("Microsoft.Build.Construction.ProjectInSolution, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectInSolution_ProjectName = s_ProjectInSolution.GetProperty("ProjectName", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectType = s_ProjectInSolution.GetProperty("ProjectType", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_RelativePath = s_ProjectInSolution.GetProperty("RelativePath", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectInSolution_ProjectGuid = s_ProjectInSolution.GetProperty("ProjectGuid", BindingFlags.NonPublic | BindingFlags.Instance); s_ProjectRootElement = Type.GetType("Microsoft.Build.Construction.ProjectRootElement, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectRootElementCache = Type.GetType("Microsoft.Build.Evaluation.ProjectRootElementCache, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectRootElement_Items = s_ProjectRootElement.GetProperty("Items", BindingFlags.Public | BindingFlags.Instance); } public IEnumerable Items { get { return items; } } public VSProject(VSSolution solution, object internalSolutionProject) { this.Name = s_ProjectInSolution_ProjectName.GetValue(internalSolutionProject, null) as string; this.ProjectType = s_ProjectInSolution_ProjectType.GetValue(internalSolutionProject, null).ToString(); this.RelativePath = s_ProjectInSolution_RelativePath.GetValue(internalSolutionProject, null) as string; this.ProjectGuid = s_ProjectInSolution_ProjectGuid.GetValue(internalSolutionProject, null) as string; this.solution = solution; this.internalSolutionProject = internalSolutionProject; this.projectFileName = Path.Combine(solution.SolutionPath, this.RelativePath); items = new List(); if (this.ProjectType == "KnownToBeMSBuildFormat") { this.Parse(); } } private void Parse() { var stream = File.OpenRead(projectFileName); var reader = XmlReader.Create(stream); var cache = s_ProjectRootElementCache.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(new object[] { true }); var rootElement = s_ProjectRootElement.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).First().Invoke(new object[] { reader, cache }); stream.Close(); var collection = (ICollection)s_ProjectRootElement_Items.GetValue(rootElement, null); foreach (var item in collection) { items.Add(new VSProjectItem(this, item)); } } public IEnumerable EDMXModels { get { return this.items.Where(i => i.ItemType == "EntityDeploy"); } } public void Dispose() { } } } // VSProjectItem using System; using System.Reflection; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using System.IO; using System.Xml; using AbstractX.Contracts; namespace VSProvider { [DebuggerDisplay("{ProjectName}, {RelativePath}, {ProjectGuid}")] public class VSProjectItem : IVSProjectItem { static readonly Type s_ProjectItemElement; static readonly PropertyInfo s_ProjectItemElement_ItemType; static readonly PropertyInfo s_ProjectItemElement_Include; private VSProject project; private object internalProjectItem; private string fileName; static VSProjectItem() { s_ProjectItemElement = Type.GetType("Microsoft.Build.Construction.ProjectItemElement, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); s_ProjectItemElement_ItemType = s_ProjectItemElement.GetProperty("ItemType", BindingFlags.Public | BindingFlags.Instance); s_ProjectItemElement_Include = s_ProjectItemElement.GetProperty("Include", BindingFlags.Public | BindingFlags.Instance); } public string ItemType { get; private set; } public string Include { get; private set; } public VSProjectItem(VSProject project, object internalProjectItem) { this.ItemType = s_ProjectItemElement_ItemType.GetValue(internalProjectItem, null) as string; this.Include = s_ProjectItemElement_Include.GetValue(internalProjectItem, null) as string; this.project = project; this.internalProjectItem = internalProjectItem; // todo - expand this if (this.ItemType == "Compile" || this.ItemType == "EntityDeploy") { var file = new FileInfo(project.FileName); fileName = Path.Combine(file.DirectoryName, this.Include); } } public byte[] FileContents { get { return File.ReadAllBytes(fileName); } } public string Name { get { if (fileName != null) { var file = new FileInfo(fileName); return file.Name; } else { return this.Include; } } } } } 

Responder por @ john-leidegren é ótimo. Para o pré-VS2015, isso é de grande utilidade. Mas houve um pequeno erro, pois faltava o código para recuperar as configurações. Então, queria adicioná-lo, caso alguém esteja com dificuldades para usar esse código.
O aprimoramento é muito simples:

  public class Solution { //internal class SolutionParser //Name: Microsoft.Build.Construction.SolutionParser //Assembly: Microsoft.Build, Version=4.0.0.0 static readonly Type s_SolutionParser; static readonly PropertyInfo s_SolutionParser_solutionReader; static readonly MethodInfo s_SolutionParser_parseSolution; static readonly PropertyInfo s_SolutionParser_projects; static readonly PropertyInfo s_SolutionParser_configurations;//this was missing in john's answer static Solution() { s_SolutionParser = Type.GetType("Microsoft.Build.Construction.SolutionParser, Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", false, false); if ( s_SolutionParser != null ) { s_SolutionParser_solutionReader = s_SolutionParser.GetProperty("SolutionReader", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_projects = s_SolutionParser.GetProperty("Projects", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_parseSolution = s_SolutionParser.GetMethod("ParseSolution", BindingFlags.NonPublic | BindingFlags.Instance); s_SolutionParser_configurations = s_SolutionParser.GetProperty("SolutionConfigurations", BindingFlags.NonPublic | BindingFlags.Instance); //this was missing in john's answer // additional info: var PropNameLst = GenHlp_PropBrowser.PropNamesOfType(s_SolutionParser); // the above call would yield something like this: // [ 0] "SolutionParserWarnings" string // [ 1] "SolutionParserComments" string // [ 2] "SolutionParserErrorCodes" string // [ 3] "Version" string // [ 4] "ContainsWebProjects" string // [ 5] "ContainsWebDeploymentProjects" string // [ 6] "ProjectsInOrder" string // [ 7] "ProjectsByGuid" string // [ 8] "SolutionFile" string // [ 9] "SolutionFileDirectory" string // [10] "SolutionReader" string // [11] "Projects" string // [12] "SolutionConfigurations" string } } public List Projects { get; private set; } public List Configurations { get; private set; } //... //... //... no change in the rest of the code } 

Como ajuda adicional, fornecendo código simples para navegar pelas propriedades de um System.Type como sugerido por @ oasten.

 public class GenHlp_PropBrowser { public static List PropNamesOfClass(object anObj) { return anObj == null ? null : PropNamesOfType(anObj.GetType()); } public static List PropNamesOfType(System.Type aTyp) { List retLst = new List(); foreach ( var p in aTyp.GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) ) { retLst.Add(p.Name); } return retLst; } } 

Obrigado John Leidegren, ele oferece uma maneira eficaz. Eu escrevo uma class hlper porque eu não posso usar seu código que não posso encontrar o s_SolutionParser_configurations e os projetos sem FullName.

O código está no github que pode obter os projetos com o FullName.

E o código não pode obter SolutionConfiguration.

Mas quando você dev a vsx o vs vai dizer que não pode encontrar o Microsoft.Build.dll , então você pode tentar usar dte para obter todos os projetos.

O código que usa o dte para obter todos os projetos está no github

Veja: http://www.wwwlicious.com/2011/03/29/envdte-getting-all-projects-html/