Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Use the Project Query API to read and modify projects, files, and properties in a Visual Studio solution.
Note
This guide uses the VisualStudio.Extensibility SDK. See Choose the right extensibility model for model comparison.
Full example
The complete implementation:
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace VSProjectQueryAPISample;
using System.Globalization;
using System.Text;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Commands;
using Microsoft.VisualStudio.Extensibility.Shell;
using Microsoft.VisualStudio.ProjectSystem.Query;
/// <summary>
/// A sample command for querying the project system.
/// </summary>
[VisualStudioContribution]
public class QueryFileCommand : Command
{
/// <inheritdoc />
public override CommandConfiguration CommandConfiguration => new("%VSProjectQueryAPISample.QueryFileCommand.DisplayName%")
{
Placements = [CommandPlacement.KnownPlacements.ToolsMenu],
Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
};
/// <inheritdoc />
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
var result = await this.Extensibility.Workspaces().QueryProjectsAsync(
project => project.With(project => project.Name)
.With(project => project.Path)
.With(project => project.Files.With(file => file.FileName)),
cancellationToken);
StringBuilder message = new StringBuilder($"\n \n === Querying File === \n");
foreach (var project in result)
{
_ = message.Append(CultureInfo.CurrentCulture, $"{project.Name}\n");
foreach (var file in project.Files)
{
_ = message.Append(CultureInfo.CurrentCulture, $" \t {file.FileName}\n");
}
}
await this.Extensibility.Shell().ShowPromptAsync(message.ToString(), PromptOptions.OK, cancellationToken);
}
}
Tip
See the full working sample: VSProjectQueryAPISample ↗
Related concepts: Project concepts — project system architecture and query patterns
Understand the example
Get the workspace
WorkspacesExtensibility workspace = this.Extensibility.Workspaces();
workspace.Queryable gives access to the root project queryable space for advanced queries and subscriptions. For common queries, use the QueryProjectsAsync and QuerySolutionAsync helpers shown below.
Observe solution open and close
QueryableSpaceChanged runs asynchronously when a solution opens or closes. SolutionPath is null on close. Keep the listener alive for as long as you need notifications, and unsubscribe when it is disposed:
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.ProjectSystem.Query;
internal sealed class SolutionChangeListener : IDisposable
{
private readonly WorkspacesExtensibility workspace;
public SolutionChangeListener(VisualStudioExtensibility extensibility)
{
this.workspace = extensibility.Workspaces();
this.workspace.QueryableSpaceChanged += this.OnChangedAsync;
}
private Task OnChangedAsync(ProjectQueryableSpaceChangedEventArgs args)
{
Trace.WriteLine(args.SolutionPath is null
? "Solution closed"
: $"Solution opened: {args.SolutionPath}");
return Task.CompletedTask;
}
public void Dispose()
{
this.workspace.QueryableSpaceChanged -= this.OnChangedAsync;
}
}
Get the active document and project
In a command, the client context captures the current IDE state. The active document URI and project snapshot can both be absent:
Uri? documentUri = await context.GetActiveDocumentUriAsync(cancellationToken);
IProjectSnapshot? activeProject = await context.GetActiveProjectAsync(cancellationToken);
See the active project command ↗ for an example that handles a missing project. The document URI reflects the active selection when the context was created, not a live view of later changes.
Query projects with properties
// Get all projects with name, path, and GUID
IQueryResults<IProjectSnapshot> projects = await workspace.QueryProjectsAsync(
project => project.With(p => p.Name)
.With(p => p.Path)
.With(p => p.Guid),
cancellationToken);
Filter projects by capability
// Get only .NET Core web projects
IQueryResults<IProjectSnapshot> webProjects = await workspace.QueryProjectsAsync(
project => project.Where(p => p.Capabilities.Contains("DotNetCoreWeb"))
.With(p => p.Path)
.With(p => p.Guid),
cancellationToken);
Query project files
// Get all files in a specific project
IQueryResults<IFileSnapshot> files = await workspace.QueryProjectsAsync(
project => project.Where(p => p.Guid == knownGuid)
.Get(p => p.Files.With(f => f.FileName)),
cancellationToken);
Add a file to a project
IQueryResults<IProjectSnapshot> updated = await workspace.UpdateProjectsAsync(
project => project.Where(p => p.Guid == knownGuid),
project => project.CreateFile("NewFile.cs"),
cancellationToken);
Rename a file
await workspace.UpdateProjectsAsync(
project => project.Where(p => p.Guid == knownGuid),
project => project.RenameFile(oldPath, newName),
cancellationToken);
Build a project
// Build entire solution
IQueryResults<ISolutionSnapshot> solutions = await this.Extensibility.Workspaces().QuerySolutionAsync(
s => s, cancellationToken);
foreach (var solution in solutions)
{
await solution.AsQueryable().BuildAsync(cancellationToken);
}
// Build a single project by name
var result = await this.Extensibility.Workspaces().QueryProjectsAsync(
project => project.Where(p => p.Name == "MyApp"),
cancellationToken);
await result.First().BuildAsync(cancellationToken);
Query NuGet packages
// Get all package references for a project
IQueryResults<IProjectSnapshot> result = await workspace.QueryProjectByGuidAsync(
project => project.With(p => p.ActiveConfigurations
.With(c => c.PackageReferences
.With(pkg => pkg.Name)
.With(pkg => pkg.Version))),
knownGuid,
cancellationToken);
Query project properties
IQueryResults<IProjectSnapshot> props = await workspace.QueryProjectsAsync(
project => project.With(p => p.PropertiesByName("RootNamespace", "AssemblyVersion")),
cancellationToken);
Query the solution
IQueryResults<ISolutionSnapshot> solution = await workspace.QuerySolutionAsync(
s => s.With(sol => sol.Path)
.With(sol => sol.Guid)
.With(sol => sol.ActiveConfiguration),
cancellationToken);
Tips
✅ DO
- Always use
.With()to specify which properties you need - Filter by capability before accessing capability-specific properties
- Use
UpdateProjectsAsyncfor mutations - Pass
CancellationTokento all async calls
❌ DON'T
- Don't query without filtering — large solutions will be slow
- Don't assume all project types support all properties (for example, SQL projects)
- Don't try to modify
IProjectSnapshotdirectly — it's immutable - Don't use
CancellationToken.Nonein production extensions
🔧 Troubleshooting
| Symptom | Fix |
|---|---|
| Property throws an exception | Check PropertiesAvailableStatus first |
| Query returns empty results | The filter matched nothing — verify project capabilities or names |
CreateFile fails |
Verify the project is loaded and writable |
| Capability-specific properties missing | Use WithRequired to filter to projects that actually have the property |