Adds GET /api/roslyn/solution/overview/summary with json|text|yaml formats. Parses getsolutionoverview payload and renders compact summary for agents.
411 lines
16 KiB
C#
411 lines
16 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using RoslynBridge.WebApi.Models;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Linq;
|
|
using RoslynBridge.WebApi.Services;
|
|
|
|
namespace RoslynBridge.WebApi.Controllers;
|
|
|
|
/// <summary>
|
|
/// Controller for Roslyn code analysis operations
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
[Produces("application/json")]
|
|
public class RoslynController : ControllerBase
|
|
{
|
|
private readonly IRoslynBridgeClient _bridgeClient;
|
|
private readonly ILogger<RoslynController> _logger;
|
|
|
|
public RoslynController(IRoslynBridgeClient bridgeClient, ILogger<RoslynController> logger)
|
|
{
|
|
_bridgeClient = bridgeClient;
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execute a Roslyn query
|
|
/// </summary>
|
|
/// <param name="request">The query request</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>The query result</returns>
|
|
/// <response code="200">Query executed successfully</response>
|
|
/// <response code="400">Invalid request</response>
|
|
/// <response code="500">Internal server error</response>
|
|
[HttpPost("query")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> ExecuteQuery(
|
|
[FromBody] RoslynQueryRequest request,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!ModelState.IsValid)
|
|
{
|
|
return BadRequest(ModelState);
|
|
}
|
|
|
|
_logger.LogInformation("Received query request: {QueryType}", request.QueryType);
|
|
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
|
|
if (!result.Success)
|
|
{
|
|
_logger.LogWarning("Query failed: {Error}", result.Error);
|
|
}
|
|
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all projects in the solution
|
|
/// </summary>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of projects</returns>
|
|
[HttpGet("projects")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> GetProjects(
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest { QueryType = "getprojects" };
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get solution overview
|
|
/// </summary>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Solution statistics and overview</returns>
|
|
[HttpGet("solution/overview")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> GetSolutionOverview(
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest { QueryType = "getsolutionoverview" };
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a compact summary of the solution overview
|
|
/// </summary>
|
|
/// <param name="topNProjects">How many projects to list (by file count)</param>
|
|
/// <param name="includeNamespaces">Include top-level namespaces in summary</param>
|
|
/// <param name="format">One of: json | text | yaml</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Compact summary as JSON or plain text</returns>
|
|
[HttpGet("solution/overview/summary")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetSolutionOverviewSummary(
|
|
[FromQuery] int topNProjects = 5,
|
|
[FromQuery] bool includeNamespaces = true,
|
|
[FromQuery] string? format = "json",
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest { QueryType = "getsolutionoverview" };
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
|
|
if (!result.Success)
|
|
{
|
|
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return Ok(result);
|
|
}
|
|
var err = result.Error ?? "Unknown error";
|
|
return Content($"error: {err}", "text/plain", Encoding.UTF8);
|
|
}
|
|
|
|
// Expect Data as JsonElement; shape per Models.SolutionOverview
|
|
int projectCount = 0;
|
|
int documentCount = 0;
|
|
var projectsSummary = new List<(string Name, int FileCount)>();
|
|
var topLevelNamespaces = new List<string>();
|
|
|
|
if (result.Data is JsonElement dataEl && dataEl.ValueKind == JsonValueKind.Object)
|
|
{
|
|
if (dataEl.TryGetProperty("projectCount", out var pc)) projectCount = pc.GetInt32();
|
|
if (dataEl.TryGetProperty("documentCount", out var dc)) documentCount = dc.GetInt32();
|
|
if (includeNamespaces && dataEl.TryGetProperty("topLevelNamespaces", out var nsEl) && nsEl.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var ns in nsEl.EnumerateArray())
|
|
{
|
|
if (ns.ValueKind == JsonValueKind.String)
|
|
{
|
|
topLevelNamespaces.Add(ns.GetString() ?? string.Empty);
|
|
}
|
|
}
|
|
}
|
|
if (dataEl.TryGetProperty("projects", out var prEl) && prEl.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var p in prEl.EnumerateArray())
|
|
{
|
|
string name = p.TryGetProperty("name", out var nEl) && nEl.ValueKind == JsonValueKind.String ? (nEl.GetString() ?? string.Empty) : string.Empty;
|
|
int files = p.TryGetProperty("fileCount", out var fEl) && fEl.ValueKind == JsonValueKind.Number ? fEl.GetInt32() : 0;
|
|
projectsSummary.Add((name, files));
|
|
}
|
|
}
|
|
}
|
|
|
|
var topProjects = projectsSummary
|
|
.OrderByDescending(p => p.FileCount)
|
|
.ThenBy(p => p.Name)
|
|
.Take(Math.Max(0, topNProjects))
|
|
.ToList();
|
|
|
|
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var compact = new
|
|
{
|
|
projectCount,
|
|
documentCount,
|
|
projects = topProjects.Select(p => new { name = p.Name, files = p.FileCount }).ToList(),
|
|
topLevelNamespaces = includeNamespaces ? topLevelNamespaces : null
|
|
};
|
|
|
|
return Ok(new RoslynQueryResponse
|
|
{
|
|
Success = true,
|
|
Message = "Compact solution overview",
|
|
Data = compact,
|
|
Error = null
|
|
});
|
|
}
|
|
|
|
// text or yaml: render a compact string
|
|
var sb = new StringBuilder();
|
|
if (string.Equals(format, "yaml", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
sb.AppendLine($"projectCount: {projectCount}");
|
|
sb.AppendLine($"documentCount: {documentCount}");
|
|
if (includeNamespaces)
|
|
{
|
|
sb.Append("topLevelNamespaces: [");
|
|
sb.Append(string.Join(", ", topLevelNamespaces));
|
|
sb.AppendLine("]");
|
|
}
|
|
sb.AppendLine("projects:");
|
|
foreach (var p in topProjects)
|
|
{
|
|
sb.AppendLine($" - name: {p.Name}");
|
|
sb.AppendLine($" files: {p.FileCount}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// text
|
|
sb.AppendLine($"Projects: {projectCount} | Documents: {documentCount}");
|
|
if (includeNamespaces && topLevelNamespaces.Count > 0)
|
|
{
|
|
sb.AppendLine($"TopNamespaces: {string.Join(", ", topLevelNamespaces)}");
|
|
}
|
|
sb.AppendLine("Top Projects:");
|
|
foreach (var p in topProjects)
|
|
{
|
|
sb.AppendLine($" - {p.Name} ({p.FileCount})");
|
|
}
|
|
}
|
|
|
|
return Content(sb.ToString(), "text/plain", Encoding.UTF8);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get diagnostics (errors and warnings)
|
|
/// </summary>
|
|
/// <param name="filePath">Optional file path to filter diagnostics</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of diagnostics</returns>
|
|
[HttpGet("diagnostics")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> GetDiagnostics(
|
|
[FromQuery] string? filePath = null,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest
|
|
{
|
|
QueryType = "getdiagnostics",
|
|
FilePath = filePath
|
|
};
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get symbol information at a specific position
|
|
/// </summary>
|
|
/// <param name="filePath">File path</param>
|
|
/// <param name="line">Line number (1-based)</param>
|
|
/// <param name="column">Column number (0-based)</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Symbol information</returns>
|
|
[HttpGet("symbol")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> GetSymbol(
|
|
[FromQuery] string filePath,
|
|
[FromQuery] int line,
|
|
[FromQuery] int column,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest
|
|
{
|
|
QueryType = "getsymbol",
|
|
FilePath = filePath,
|
|
Line = line,
|
|
Column = column
|
|
};
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find all references to a symbol
|
|
/// </summary>
|
|
/// <param name="filePath">File path</param>
|
|
/// <param name="line">Line number (1-based)</param>
|
|
/// <param name="column">Column number (0-based)</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of references</returns>
|
|
[HttpGet("references")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> FindReferences(
|
|
[FromQuery] string filePath,
|
|
[FromQuery] int line,
|
|
[FromQuery] int column,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest
|
|
{
|
|
QueryType = "findreferences",
|
|
FilePath = filePath,
|
|
Line = line,
|
|
Column = column
|
|
};
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Search for symbols by name
|
|
/// </summary>
|
|
/// <param name="symbolName">Symbol name or pattern</param>
|
|
/// <param name="kind">Optional symbol kind filter</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of matching symbols</returns>
|
|
[HttpGet("symbol/search")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> FindSymbol(
|
|
[FromQuery] string symbolName,
|
|
[FromQuery] string? kind = null,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest
|
|
{
|
|
QueryType = "findsymbol",
|
|
SymbolName = symbolName
|
|
};
|
|
|
|
if (!string.IsNullOrEmpty(kind))
|
|
{
|
|
request.Parameters = new Dictionary<string, string> { ["kind"] = kind };
|
|
}
|
|
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Format a document
|
|
/// </summary>
|
|
/// <param name="filePath">File path to format</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Format operation result</returns>
|
|
[HttpPost("format")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> FormatDocument(
|
|
[FromBody] string filePath,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest
|
|
{
|
|
QueryType = "formatdocument",
|
|
FilePath = filePath
|
|
};
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a NuGet package to a project
|
|
/// </summary>
|
|
/// <param name="projectName">Project name</param>
|
|
/// <param name="packageName">NuGet package name</param>
|
|
/// <param name="version">Optional package version</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Operation result</returns>
|
|
[HttpPost("project/package/add")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> AddNuGetPackage(
|
|
[FromQuery] string projectName,
|
|
[FromQuery] string packageName,
|
|
[FromQuery] string? version = null,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest
|
|
{
|
|
QueryType = "addnugetpackage",
|
|
ProjectName = projectName,
|
|
PackageName = packageName,
|
|
Version = version
|
|
};
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build a project
|
|
/// </summary>
|
|
/// <param name="projectName">Project name</param>
|
|
/// <param name="configuration">Build configuration (Debug/Release)</param>
|
|
/// <param name="instancePort">Optional: specific VS instance port to target</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>Build result</returns>
|
|
[HttpPost("project/build")]
|
|
[ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)]
|
|
public async Task<ActionResult<RoslynQueryResponse>> BuildProject(
|
|
[FromQuery] string projectName,
|
|
[FromQuery] string? configuration = null,
|
|
[FromQuery] int? instancePort = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var request = new RoslynQueryRequest
|
|
{
|
|
QueryType = "buildproject",
|
|
ProjectName = projectName,
|
|
Configuration = configuration
|
|
};
|
|
var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken);
|
|
return Ok(result);
|
|
}
|
|
}
|