Análisis de archivos de Visual Studio Solution

¿Cómo puedo analizar los archivos de la solución Visual Studio (SLN) en .NET? Me gustaría escribir una aplicación que combine varias soluciones en una mientras se guarda el orden de comstackción relativo.

La versión .NET 4.0 del ensamblado Microsoft.Build contiene una clase SolutionParser en el espacio de nombres Microsoft.Build.Construction que analiza los archivos de la solución Visual Studio.

Lamentablemente, esta clase es interna, pero he envuelto parte de esa funcionalidad en una clase que utiliza la reflexión para obtener algunas propiedades comunes que pueden ser útiles.

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(); } } 

Tenga en cuenta que debe cambiar su marco de destino a ".NET Framework 4" (no el perfil de cliente) para poder agregar la referencia de Microsoft.Build a su proyecto.

Con Visual Studio 2015 ahora hay una clase SolutionFile acceso público que se puede usar para analizar archivos de soluciones:

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

Esta clase se encuentra en el ensamblado Microsoft.Build.dll 14.0.0.0 . En mi caso, estaba ubicado en:

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

¡Gracias a Phil por señalar esto !

No sé si alguien todavía está buscando soluciones a este problema, pero me topé con un proyecto que parece hacer justo lo que se necesita. https://slntools.codeplex.com/ Una de las funciones de esta herramienta es fusionar múltiples soluciones juntas.

JetBrains (los creadores de Resharper) tienen habilidades de análisis de sln públicas en sus ensamblajes (no se necesita reflexión). Es probablemente más robusto que las soluciones de código abierto existentes que se sugieren aquí (y mucho menos los hacks de ReGex). Todo lo que necesitas hacer es:

  • Descargue las herramientas de línea de comandos de ReSharper (gratis).
  • Agregue lo siguiente como referencias a su proyecto
    • JetBrains.Platform.ProjectModel
    • JetBrains.Platform.Util
    • JetBrains.Platform.Interop.WinApi

La biblioteca no está documentada, pero Reflector (o de hecho, dotPeek) es tu amigo. Por ejemplo:

 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)); } } } } } 

Realmente no puedo ofrecerte una biblioteca y creo que no hay ninguna que exista. Pero he pasado mucho tiempo jugando con archivos .sln en escenarios de edición por lotes y he encontrado que Powershell es una herramienta muy útil para esta tarea. El formato .SLN es bastante simple y se puede analizar casi por completo con algunas expresiones rápidas y sucias. Por ejemplo

Archivos de proyecto incluidos

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

No siempre es bonito, pero es una forma efectiva de hacer un procesamiento por lotes.

Solucionamos un problema similar de fusión de soluciones automáticamente al escribir un plugin de Visual Studio que creó una nueva solución, luego buscamos el archivo * .sln y lo importamos al nuevo usando:

 dte2.Solution.AddFromFile(solutionPath, false); 

Nuestro problema era un poco diferente en el sentido de que queríamos que VS resolviera el pedido de comstackción para nosotros, de modo que luego convertimos cualquier referencia de dll a las referencias de proyecto siempre que fuera posible.

Luego automatizamos esto en un proceso de comstackción ejecutando VS a través de la automatización COM.

Esta solución era un poco Heath Robinson, pero tenía la ventaja de que VS estaba haciendo la edición, por lo que nuestro código no dependía del formato del archivo sln. Lo cual fue útil cuando pasamos de VS 2005 a 2008 y nuevamente a 2010.

Todo es genial, pero también quería obtener la capacidad de generación sln: en la instantánea de código anterior, solo estás analizando archivos .sln, quería hacer algo similar, excepto poder volver a generar sln con ligeras modificaciones en el archivo .sln. . Tales casos podrían ser, por ejemplo, portar el mismo proyecto para diferentes plataformas .NET. Por ahora, es solo una nueva generación de sln, pero más adelante también la ampliaré a proyectos.

Supongo que también quería demostrar el poder de las expresiones regulares y las interfaces nativas. (Menor cantidad de código con más funcionalidad)

Actualización 4.1.2017 Creé un repository svn para analizar la solución .sln: https://sourceforge.net/p/syncproj/code/HEAD/tree/

Debajo está mi propio fragmento de muestra de código (predecesor). Eres libre de usar cualquiera de ellos.

Es posible que en el futuro el código de análisis de la solución basada en svn también se actualice con capacidades de generación.

Actualización 4.2.2017 El código fuente en SVN también admite la generación .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); } } } 

Examiné, determiné que las clases de MSBuild se pueden usar para manipular las estructuras subyacentes. Tendré más código en mi sitio web más 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; } } } } } 

La respuesta de @ john-leidegren es genial. Para pre-VS2015, esto es de gran utilidad. Pero hubo un error menor, ya que faltaba el código para recuperar las configuraciones. Así que quería agregarlo, en caso de que alguien esté luchando por usar este código.
La mejora es muy simple:

  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 ayuda adicional, proporciona un código simple para explorar las propiedades de un System.Type según lo 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; } } 

Gracias a John Leidegren, él ofrece una manera efectiva. Escribo una clase hlper porque no puedo usar su código que no puede encontrar las s_SolutionParser_configurations y los proyectos sin FullName.

El código está en github que puede obtener los proyectos con FullName.

Y el código no puede obtener SolutionConfiguration.

Pero cuando desarrolle un vsx, el vs dirá que no puede encontrar Microsoft.Build.dll , por lo que puede intentar usar dte para obtener todos los proyectos.

El código que usa dte para obtener todos los proyectos está en github

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