Files
RoslynBridge/RoslynBridge.WebApi/Services/RoslynBridgeClient.cs
AJ Isaacs 1cbfba3893 Add WebAPI middleware for multi-instance Roslyn Bridge routing
Add RoslynBridge.WebApi - ASP.NET Core 8.0 middleware that:
- Provides a centralized REST API for accessing multiple VS instances
- Manages instance registry with discovery by port, solution, or PID
- Proxies requests to the appropriate VS instance
- Tracks request/response history for debugging
- Auto-cleanup of stale instances via background service

Features:
- Health endpoints: /api/health, /api/health/ping
- Roslyn endpoints: /api/roslyn/projects, /api/roslyn/diagnostics, etc.
- Instance management: /api/instances (register, heartbeat, unregister)
- History tracking: /api/history, /api/history/stats
- Swagger UI at root (/) for API documentation
- CORS enabled for web applications

Services:
- InstanceRegistryService: Thread-safe registry of VS instances
- HistoryService: In-memory request/response history (max 1000 entries)
- InstanceCleanupService: Background service to remove stale instances
- RoslynBridgeClient: HTTP client for proxying to VS instances

Update RoslynBridge.sln to include RoslynBridge.WebApi project.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 23:51:33 -04:00

175 lines
6.2 KiB
C#

using System.Text;
using System.Text.Json;
using RoslynBridge.WebApi.Models;
namespace RoslynBridge.WebApi.Services;
/// <summary>
/// HTTP client for communicating with the Roslyn Bridge Visual Studio plugin
/// </summary>
public class RoslynBridgeClient : IRoslynBridgeClient
{
private readonly HttpClient _httpClient;
private readonly IInstanceRegistryService _registryService;
private readonly ILogger<RoslynBridgeClient> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public RoslynBridgeClient(
HttpClient httpClient,
IInstanceRegistryService registryService,
ILogger<RoslynBridgeClient> logger)
{
_httpClient = httpClient;
_registryService = registryService;
_logger = logger;
_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public async Task<RoslynQueryResponse> ExecuteQueryAsync(
RoslynQueryRequest request,
int? instancePort = null,
CancellationToken cancellationToken = default)
{
try
{
var targetPort = await ResolveInstancePortAsync(instancePort, request);
if (targetPort == null)
{
return new RoslynQueryResponse
{
Success = false,
Error = "No Visual Studio instance available"
};
}
_logger.LogInformation("Executing query: {QueryType} on port {Port}", request.QueryType, targetPort);
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"http://localhost:{targetPort}/query";
var response = await _httpClient.PostAsync(url, content, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError("Query failed with status {StatusCode}: {Error}", response.StatusCode, errorContent);
return new RoslynQueryResponse
{
Success = false,
Error = $"Request failed with status {response.StatusCode}: {errorContent}"
};
}
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
var result = JsonSerializer.Deserialize<RoslynQueryResponse>(responseContent, _jsonOptions);
if (result == null)
{
return new RoslynQueryResponse
{
Success = false,
Error = "Failed to deserialize response"
};
}
_logger.LogInformation("Query executed successfully: {Success}", result.Success);
return result;
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "HTTP request failed while executing query");
return new RoslynQueryResponse
{
Success = false,
Error = $"Failed to connect to Roslyn Bridge server: {ex.Message}"
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while executing query");
return new RoslynQueryResponse
{
Success = false,
Error = $"Unexpected error: {ex.Message}"
};
}
}
public async Task<bool> IsHealthyAsync(int? instancePort = null, CancellationToken cancellationToken = default)
{
try
{
var targetPort = await ResolveInstancePortAsync(instancePort, null);
if (targetPort == null)
{
return false;
}
var request = new RoslynQueryRequest { QueryType = "health" };
var json = JsonSerializer.Serialize(request, _jsonOptions);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var url = $"http://localhost:{targetPort}/health";
var response = await _httpClient.PostAsync(url, content, cancellationToken);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Health check failed");
return false;
}
}
/// <summary>
/// Resolves which VS instance port to use based on provided hints
/// </summary>
private Task<int?> ResolveInstancePortAsync(int? explicitPort, RoslynQueryRequest? request)
{
// If explicit port specified, use it
if (explicitPort.HasValue)
{
return Task.FromResult<int?>(explicitPort.Value);
}
// Try to find instance by solution path from request
if (request != null && !string.IsNullOrEmpty(request.FilePath))
{
// Extract solution path by looking for .sln file in the path hierarchy
var directory = Path.GetDirectoryName(request.FilePath);
while (!string.IsNullOrEmpty(directory))
{
var solutionFiles = Directory.GetFiles(directory, "*.sln");
if (solutionFiles.Length > 0)
{
var instance = _registryService.GetBySolutionPath(solutionFiles[0]);
if (instance != null)
{
_logger.LogDebug("Found instance by solution path: {SolutionPath}", solutionFiles[0]);
return Task.FromResult<int?>(instance.Port);
}
}
directory = Path.GetDirectoryName(directory);
}
}
// Fall back to first available instance
var instances = _registryService.GetAllInstances().ToList();
if (instances.Any())
{
_logger.LogDebug("Using first available instance: port {Port}", instances[0].Port);
return Task.FromResult<int?>(instances[0].Port);
}
_logger.LogWarning("No Visual Studio instances registered");
return Task.FromResult<int?>(null);
}
}