Consulta de la API del proyecto

La API de consulta del proyecto VisualStudio.Extensibility permite consultar información desde el sistema de proyecto. Los sistemas de proyectos forman parte de los componentes de Visual Studio para ayudar a los usuarios a trabajar con proyectos y mantenerlos, ejecutar compilaciones para producir resultados y probar la salida.

El objetivo de la API de consulta del proyecto es:

  1. Trabajar con sistemas de proyectos
  2. Recuperar datos de proyectos
  3. Realizar cambios en los proyectos

Entre algunos ejemplos se incluyen el reconocimiento de los archivos incluidos en un proyecto, los paquetes NuGet a los que hace referencia un proyecto, la adición de nuevos archivos a un proyecto o el cambio de las propiedades de un proyecto.

Puede obtener más información sobre los sistemas de proyectos aquí. Puede encontrar documentación conceptual sobre lo que es el sistema de proyecto, sus usos y sus distintos términos aquí.

Trabajar con la API de consulta del proyecto

En esta introducción se describen los principales escenarios para trabajar con la API de consulta del proyecto:

Acceso al espacio de consulta del proyecto

Para poder consultar el sistema de proyecto, debe obtener una instancia del objeto del espacio de consulta del proyecto, que tiene varios métodos asíncronos que consultan o actualizan el sistema de proyecto. El término espacio de consulta del proyecto y el término área de trabajo significan lo mismo: el objeto que proporciona acceso a todos los datos de un proyecto.

Acceso al espacio de consulta del proyecto en una extensión fuera de proceso

Si va a crear una extensión fuera de proceso, use el código siguiente:

WorkspacesExtensibility workSpace = this.Extensibility.Workspaces();

Acceso al espacio de consulta del proyecto en una extensión dentro de proceso

Si va a crear una extensión dentro de proceso, en su lugar tiene acceso al espacio de consulta del proyecto, como se muestra en el ejemplo de código siguiente. A menos que haya creado específicamente una extensión dentro de proceso, utilice el fragmento de la sección anterior para obtener una instancia del objeto de espacio de consulta del proyecto.

En el fragmento de código siguiente, package representa una instancia de AsyncPackage, una clase utilizada en el desarrollo de extensiones de Visual Studio. El método GetServiceAsync se emplea para adquirir de forma asíncrona el servicio de consultas desde el contenedor de servicio de Visual Studio.

IProjectSystemQueryService queryService = await package.GetServiceAsync<IProjectSystemQueryService, IProjectSystemQueryService>();
ProjectQueryableSpace workSpace = queryService.QueryableSpace;

Consulta del sistema de proyecto de un proyecto

El objeto WorkspacesExtensibility le permite consultar un proyecto individual, si tiene el GUID del proyecto. Normalmente, hay dos GUID asociados a un proyecto, uno representa el tipo de proyecto y otro representa de forma única el proyecto. Puede encontrar el GUID único del proyecto en el archivo de solución o desde una extensión. También puede consultar la propiedad Guid como se muestra en la sección siguiente.

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> projectList = workspace
    .ProjectsByProjectGuid(knownGuid) 
    .QueryAsync(cancellationToken);

Especificación de los parámetros del proyecto que se van a incluir en el resultado de la consulta

Al consultar el sistema de proyecto, puede utilizar cláusulas With para controlar qué parámetros o metadatos se incluyen en los resultados de la consulta. Hay varias maneras válidas de especificar qué parámetros se deben incluir.

Ejemplo de uso de una cláusula independiente With para cada parámetro

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> allProjects = workSpace
    .Projects
    .With(p => p.Path)
    .With(p => p.Guid)
    .With(p => p.Kind)      // DTE.Project.Kind
    .With(p => p.Type)      // VSHPROPID_ProjectType
    .With(p => p.TypeGuid)  // VSHPROPID_TypeGuid
    .With(p => p.Capabilities)
    .QueryAsync(cancellationToken);

    await foreach (IQueryResultItem<IProjectSnapshot> project in allProjects)
    {
        var projectGuid = project.Value.Guid;
        // Checking whether 'Capabilities' property has been retrieved.
        // Otherwise, it can throw for projects which do not support it. (Like SQL projects)
        bool capabilities = project.Value.PropertiesAvailableStatus.Capabilities;
    }

Ejemplo de uso de una cláusula única With para especificar varios parámetros

También puede especificar varios parámetros deseados en una sola cláusula With.

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> allProjects = workSpace
    .Projects
    .With(p => new { p.Path, p.Guid, p.Capabilities })
    .QueryAsync(cancellationToken);

Ejemplo de uso de una cláusula WithRequired

Cuando se usa WithRequired, solo se devuelven proyectos con las propiedades requeridas.

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> projectWithFiles = workSpace
    .Projects
    .With(p => new { p.Path, p.Guid })
    .WithRequired(p => p.Files.Where(f => f.FileName == "information.txt"))
    .QueryAsync(cancellationToken);

Ejemplo de cuando no se especifica ninguna propiedad

Cuando no se especifica ninguna propiedad, se devuelve el conjunto predeterminado de propiedades.

IAsyncEnumerable<IQueryResultItem<IPropertySnapshot>> properties = myproject
    .PropertiesByName("RootNamespace", "AssemblyVersion")
    .QueryAsync(cancellationToken);

Filtrado de los resultados de la consulta

Para limitar los resultados de la consulta, hay dos maneras de aplicar el filtrado condicional: instrucciones Where y métodos de consulta con filtrado integrado.

Ejemplo de uso de una cláusula Where

Los distintos tipos de proyecto admiten diferentes conjuntos de funcionalidades. Con una cláusula Where, puede filtrar por proyectos que admitan funcionalidades específicas. Las consultas pueden generar un error si no filtra por proyectos que admiten las funcionalidades pertinentes.

El código siguiente devuelve Path y Guid de todos los proyectos web de .NET Core del área de trabajo:

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> webProjects = workspace
    .Projects
    .Where(p => p.Capabilities.Contains("DotNetCoreWeb"))
    .With(p => new { p.Path, p.Guid })
    .QueryAsync(cancellationToken);

Ejemplo de uso del filtrado integrado

También puede utilizar métodos de consulta como ProjectsByCapabilities, que tienen filtrado integrado en la consulta.

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> webProjects = workspace
        .ProjectsByCapabilities("DotNetCoreWeb | DotNetCoreRazor")
        .With(p => new { p.Path, p.Guid })
        .QueryAsync(cancellationToken);

Uso de consultas anidadas para especificar las propiedades deseadas

Algunos parámetros son colecciones en sí mismos y puede utilizar consultas anidadas para realizar especificaciones y filtros similares en esas colecciones secundarias.

Ejemplo

En el ejemplo siguiente, una consulta anidada permite filtrar y especificar la colección de archivos que se incluirán con cada proyecto devuelto por la consulta externa.

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> projects = workspace
    .ProjectsByCapabilities("CPS")
    .With(p => new { p.Path, p.IsProjectFileSearchable })
    .With(p => p.PropertiesByName("ApplicationIcon")) // Retrieve a single property, if it exists
    .With(p => p.Files // Without any condition, retrieve all files in the project, but filter them
        .Where(f => f.Extension == ".ico")
        .With(f => new { f.Path, f.IsHidden }))
    .QueryAsync(cancellationToken);

    await foreach (IQueryResultItem<IProjectSnapshot> project in projects)
    {
        IPropertySnapshot property = project.Value.Properties.FirstOrDefault();
        string? applicationIcon = (string?)property?.Value;

        foreach (var iconFile in project.Value.Files)
        {
            string filePath = iconFile.Path;
            bool isHidden = iconFile.IsHidden;
        }
    }

Recuperación de una colección secundaria a través del método Get

El modelo de proyecto de Visual Studio tiene colecciones para proyectos, así como colecciones secundarias, por ejemplo, para archivos o funcionalidades de proyectos dentro de proyectos. Para recuperar una colección secundaria en sí misma, puede usar una cláusula Get. Al igual que otros tipos de consulta, la cláusula Get permite utilizar otras cláusulas como la cláusula With para dar forma o limitar los resultados.

IAsyncEnumerable<IQueryResultItem<IFileSnapshot>> files = workspace
    .Projects
    .Where(p => p.Guid == knownGuid)
    .Get(p => p.Files
        .With(f => new { f.Path, f.IsHidden, f.IsSearchable }))
    .QueryAsync(cancellationToken);

    await foreach (var file in files)
    {
        string filePath = file.Value.Path;
    }

Consulta de información adicional de un artículo devuelto anteriormente

Puede utilizar los resultados de una consulta anterior como base para consultas adicionales.

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> allProjects = workSpace
    .Projects
    .With(p => p.Path)
    .With(p => p.Guid)
    .QueryAsync(cancellationToken);

await foreach (IQueryResultItem<IProjectSnapshot> project in allProjects)
{
    // Gets child collections
    IAsyncEnumerable<IQueryResultItem<IFileSnapshot>> files = project.Value
        .Files
        .With(f => new { f.Path, f.ItemType })
        .QueryAsync(cancellationToken);
}

Modificación de un proyecto

Normalmente, los resultados de la consulta son inmutables. También puede utilizar la API de consulta para realizar cambios mediante la cláusula AsUpdatable para tener acceso a versiones mutables de los resultados de la consulta, de modo que pueda realizar cambios en los proyectos y en los elementos del proyecto.

Ejemplo de adición de un archivo a un proyecto en un resultado de consulta

IQueryResult<IProjectSnapshot> updatedProjects = workSpace
    .ProjectsByProjectGuid(knownGuid)
    .AsUpdatable()
    .CreateFile("AdditionalInformation.txt", textContent)
    .ExecuteAsync(cancellationToken);

Ejemplo de adición de un archivo a un proyecto devuelto anteriormente

IQueryResult<IProjectSnapshot> updatedProjects = myproject
    .AsUpdatable()
    .CreateFile("AdditionalInformation2.txt", textContent)
    .ExecuteAsync(cancellationToken);

Consulta de propiedades del proyecto

Puede utilizar una cláusula Get para consultar las propiedades del proyecto. La consulta siguiente devuelve una colección de IPropertySnapshot que contiene entradas para las dos propiedades solicitadas. IPropertySnapshot contiene el nombre de propiedad, el nombre para mostrar y el valor en un momento dado.

// We assume that we can find the "RootNamespace" property in the result.
// However it isn't true from query API point of view.
// The query tries to retrieve items based on the condition, and if there is no such item, it will run successfully, only without returning items.
IAsyncEnumerable<IQueryResultItem<IPropertySnapshot>> properties = myProject
    .AsQueryable()  
    .Get(p => p.PropertiesByName("RootNamespace", "AssemblyVersion"))
    .QueryAsync(cancellationToken);

Consulta de soluciones

Además de trabajar con proyectos como se mostró anteriormente, puede utilizar técnicas similares para trabajar con soluciones.

IAsyncEnumerable<IQueryResultItem<ISolutionSnapshot>> solutions = workSpace
    .Solutions
    .With(s => new { s.Path, s.Guid, s.ActiveConfiguration, s.ActivePlatform })
    .QueryAsync(cancellationToken);

Consulta de carpetas de soluciones

Del mismo modo, puede utilizar una cláusula Get para consultar las carpetas de la solución. La propiedad IsNested le permite incluir o excluir carpetas anidadas de los resultados. El Explorador de soluciones puede tener carpetas anidadas, como para la configuración o los recursos.

IAsyncEnumerable<IQueryResultItem<ISolutionFolderSnapshot>> solutionFolders = workSpace
    .Solutions
    .Get(s => s.SolutionFolders)
    .With(folder => folder.Name)
    .With(folder => folder.IsNested)
    .With(folder => folder.VisualPath) // it's a relative (virtual) path to represent how the folder is nested.
    .QueryAsync(cancellationToken);

Aquí se obtienen todas las carpetas de soluciones anidadas, proyectos y archivos dentro de una carpeta de solución (no anidada de forma recursiva):

IAsyncEnumerable<IQueryResultItem<ISolutionSnapshot>> solutionFoldersWithExtraInformation = mySolutionFolder
    .AsQueryable()
    .With(folder => folder.Files
        .With(f => f.Path))
    .With(folder => folder.Projects
        .With(p => new { p.Name, p.Guid }))
    .With(folder => folder.SolutionFolders
        .With(nested => nested.Name))
    .QueryAsync(cancellationToken);

Aquí se obtienen todas las carpetas de soluciones anidadas de forma recursiva. VisualPath es la ruta de acceso tal como se muestra en el Explorador de soluciones.

string visualPath = mySolutionFolder.VisualPath;
IAsyncEnumerable<IQueryResultItem<ISolutionFolderSnapshot>> recursivelyNestedFolders = await workSpace
    .Solutions
    .Get(s => s.SolutionFolders)
    .Where(f => f.VisualPath.StartsWith(visualPath) && f.VisualPath != visualPath)
    .With(f => f.Name)
    .QueryAsync(cancellationToken);

Enumeración de archivos de origen con información adicional en un proyecto

Este es un ejemplo que enumera todos los archivos .xaml de un proyecto y su generador de código:

IAsyncEnumerable<IQueryResultItem<IFileSnapshot>> files =
workSpace.ProjectsByProjectGuid(knownGuid)
    .Get(p => p.Files)
    .Where(file => file.Extension == ".xaml")
    .With(file => file.Path)
    .With(file => file.PropertiesByName("Generator"))
    .QueryAsync(cancellationToken);

Otro ejemplo consiste en empezar con un proyecto devuelto por la consulta anterior:

IAsyncEnumerable<IQueryResultItem<IFileSnapshot>> files = myProject
    .FilesEndingWith(".xaml")     // use built-in filter instead of 'Where' condition
    .With(file => file.Path)
    .With(file => file.PropertiesByName("Generator"))
    .QueryAsync(cancellationToken);

O para obtener todos los archivos de contenido, que son archivos no compilados necesarios en el tiempo de ejecución, como archivos HTML y CSS.

IAsyncEnumerable<IQueryResultItem<IFileSnapshot>> files =
    myProject.FilesWithItemTypes("Content")
        .With(file => file.Path)
        .QueryAsync(cancellationToken);

O para enumerar todos los archivos con una extensión determinada, como archivos de esquema XML (archivos .xsd) en todos los proyectos:

IAsyncEnumerable<IQueryResultItem<IFileSnapshot>> schemaFiles =
workSpace.Projects
    .Get(proj => proj.FilesEndingWith(".xsd"))
    .With(file => file.Path)
    .QueryAsync(cancellationToken);

    await foreach (IQueryResultItem<IFileSnapshot> fileResult in schemaFiles)
    {
        DoSomething(fileResult.Value.Path);
    }

Consulta de proyectos que poseen un archivo de origen específico

Los proyectos y carpetas tienen información sobre los archivos que poseen o contienen, por lo que puede utilizar una cláusula WithRequired para consultar proyectos que incluyan determinados archivos.

Ejemplo de búsqueda de proyectos que poseen un archivo determinado

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> projects = workspace
    .Projects
    .WithRequired(proj => proj.FilesByPath(myFilePath))
    .With(proj => proj.Guid)
    .QueryAsync(cancellationToken);

Ejemplo de búsqueda de carpetas de soluciones que contienen un archivo determinado

IAsyncEnumerable<IQueryResultItem<ISolutionFolderSnapshot>> solutionFolders = workspace
    .Solutions
    .Get(s => s.SolutionFolders)
    .WithRequired(folder => folder.FilesByPath(myFilePath))
    .With(folder => folder.Name)
    .With(folder => folder.Guid)
    .QueryAsync(cancellationToken);

Consulta de las configuraciones del proyecto y sus propiedades

Los proyectos tienen una propiedad ConfigurationDimension que puede utilizar para buscar información sobre la configuración del proyecto. La información sobre la configuración del proyecto se relaciona con las configuraciones de compilación del proyecto (por ejemplo, Debug y Release).

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> projects = workspace
    .Projects
    .With(p => new { p.Guid, p.Name })
    .With(p => p.Configurations
        .With(c => c.Name)
        .With(c => c.PropertiesByName("OutputPath"))
        .With(c => c.ConfigurationDimensions)) // ConfigurationDimension is essentially Name, Value pairs, both are default properties.
    .QueryAsync(cancellationToken);

    await foreach (IQueryResultItem<IProjectSnapshot> project in projects)
    {
        foreach (var configuration in project.Value.Configuration)
        {
            // ...
        }
    }

Consulta de las referencias entre proyectos

También puede realizar una consulta para buscar proyectos que hagan referencia a un proyecto determinado.

Ejemplo de búsqueda de todos los proyectos a los que hace referencia el proyecto actual

IAsyncEnumerable<IQueryResultItem<IProjectReferenceSnapshot>> projectReferences = myProject
    .ProjectReferences
    .With(r => r.ProjectGuid)
    .With(r => r.ReferencedProjectId)
    .QueryAsync(cancellationToken);

Ejemplo de búsqueda de todos los proyectos que hacen referencia al proyecto actual

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> projects = workSpace
    .Projects
    .With(p => p.Guid)
    .WithRequired(p => p.ProjectReferences
        .Where(r => r.ProjectGuid == knownGuid))
    .QueryAsync(cancellationToken);

Consulta de referencias de paquetes

Del mismo modo, puede consultar las referencias del paquete NuGet.

Ejemplo de búsqueda de todos los paquetes a los que hace referencia el proyecto actual

IAsyncEnumerable<IQueryResultItem<IProjectConfigurationSnapshot>> configurationsWithPackageReferences = myProject
    .ActiveConfigurations
    .With(c => c.Name)
    .With(c => c.PackageReferences
        .With(p => new { p.Name, p.Version }))
    .QueryAsync(cancellationToken);

Ejemplo de búsqueda de todos los proyectos que hacen referencia a un paquete NuGet específico

string packageName = "Newtonsoft.Json";

IAsyncEnumerable<IQueryResultItem<IProjectSnapshot>> projects = workSpace
    .Projects
    .With(p => p.Guid)
    .WithRequired(p => p.ActiveConfigurations
        .WithRequired(c => c.PackageReferences
            .Where(package => package.Name == packageName)))
    .QueryAsync(cancellationToken);

Consulta de grupos de salida del proyecto

Las configuraciones del proyecto contienen información sobre los grupos de salida del proyecto.

// From our list of active configurations, we need to get the first one in the list
IAsyncEnumerable<IQueryResultItem<IProjectConfigurationSnapshot>> configurations = myProject
    .ActiveConfigurations
    .QueryAsync(cancellationToken);

    IProjectConfigurationSnapshot myConfiguration = null;

    await foreach (IQueryResultItem<IProjectConfigurationSnapshot> config in configurations)
    {
        myConfiguration = config.Value;
        break;
    }

    // A multi-target project may have multiple active configurations
    IAsyncEnumerable<IQueryResultItem<IOutputGroupSnapshot>> outputGroups = myConfiguration
        .OutputGroupsByName("Built", "Symbols")
        .With(g => g.Name)
        .With(g => g.Outputs)
        .QueryAsync(cancellationToken);

Pasos siguientes

Revise el código de una extensión que utilice la API de consulta del proyecto en VSProjectQueryAPISample.