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; /// /// Controller for Roslyn code analysis operations /// [ApiController] [Route("api/[controller]")] [Produces("application/json")] public class RoslynController : ControllerBase { private readonly IRoslynBridgeClient _bridgeClient; private readonly ILogger _logger; public RoslynController(IRoslynBridgeClient bridgeClient, ILogger logger) { _bridgeClient = bridgeClient; _logger = logger; } /// /// Execute a Roslyn query /// /// The query request /// Optional: specific VS instance port to target /// Cancellation token /// The query result /// Query executed successfully /// Invalid request /// Internal server error [HttpPost("query")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> 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); } /// /// Get all projects in the solution /// /// Optional: specific VS instance port to target /// Cancellation token /// List of projects [HttpGet("projects")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } /// /// Get solution overview /// /// Optional: specific VS instance port to target /// Cancellation token /// Solution statistics and overview [HttpGet("solution/overview")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } /// /// Get a compact summary of the solution overview /// /// How many projects to list (by file count) /// Include top-level namespaces in summary /// One of: json | text | yaml /// Optional: specific VS instance port to target /// Cancellation token /// Compact summary as JSON or plain text [HttpGet("solution/overview/summary")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task 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(); 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); } /// /// Get diagnostics (errors and warnings) /// /// Optional file path to filter diagnostics /// Optional: specific VS instance port to target /// Cancellation token /// List of diagnostics [HttpGet("diagnostics")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } /// /// Get symbol information at a specific position /// /// File path /// Line number (1-based) /// Column number (0-based) /// Optional: specific VS instance port to target /// Cancellation token /// Symbol information [HttpGet("symbol")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } /// /// Find all references to a symbol /// /// File path /// Line number (1-based) /// Column number (0-based) /// Optional: specific VS instance port to target /// Cancellation token /// List of references [HttpGet("references")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } /// /// Search for symbols by name /// /// Symbol name or pattern /// Optional symbol kind filter /// Optional: specific VS instance port to target /// Cancellation token /// List of matching symbols [HttpGet("symbol/search")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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 { ["kind"] = kind }; } var result = await _bridgeClient.ExecuteQueryAsync(request, instancePort, cancellationToken); return Ok(result); } /// /// Format a document /// /// File path to format /// Optional: specific VS instance port to target /// Cancellation token /// Format operation result [HttpPost("format")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } /// /// Add a NuGet package to a project /// /// Project name /// NuGet package name /// Optional package version /// Optional: specific VS instance port to target /// Cancellation token /// Operation result [HttpPost("project/package/add")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } /// /// Build a project /// /// Project name /// Build configuration (Debug/Release) /// Optional: specific VS instance port to target /// Cancellation token /// Build result [HttpPost("project/build")] [ProducesResponseType(typeof(RoslynQueryResponse), StatusCodes.Status200OK)] public async Task> 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); } }