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>
This commit is contained in:
2025-10-26 23:51:33 -04:00
parent 716827a665
commit 1cbfba3893
76 changed files with 4322 additions and 0 deletions

View File

@@ -0,0 +1,115 @@
using System.Collections.Concurrent;
using RoslynBridge.WebApi.Models;
namespace RoslynBridge.WebApi.Services;
/// <summary>
/// Thread-safe in-memory registry for Visual Studio instances
/// </summary>
public class InstanceRegistryService : IInstanceRegistryService
{
private readonly ConcurrentDictionary<int, VSInstanceInfo> _instances = new();
private readonly ILogger<InstanceRegistryService> _logger;
public InstanceRegistryService(ILogger<InstanceRegistryService> logger)
{
_logger = logger;
}
public void Register(VSInstanceInfo instance)
{
instance.RegisteredAt = DateTime.UtcNow;
instance.LastHeartbeat = DateTime.UtcNow;
_instances.AddOrUpdate(instance.ProcessId, instance, (_, existing) =>
{
// Update existing instance
existing.Port = instance.Port;
existing.SolutionPath = instance.SolutionPath;
existing.SolutionName = instance.SolutionName;
existing.Projects = instance.Projects;
existing.LastHeartbeat = DateTime.UtcNow;
return existing;
});
_logger.LogInformation(
"Registered VS instance: PID={ProcessId}, Port={Port}, Solution={Solution}",
instance.ProcessId,
instance.Port,
instance.SolutionName ?? "None");
}
public bool Unregister(int processId)
{
var removed = _instances.TryRemove(processId, out var instance);
if (removed)
{
_logger.LogInformation(
"Unregistered VS instance: PID={ProcessId}, Solution={Solution}",
processId,
instance?.SolutionName ?? "None");
}
return removed;
}
public bool UpdateHeartbeat(int processId)
{
if (_instances.TryGetValue(processId, out var instance))
{
instance.LastHeartbeat = DateTime.UtcNow;
_logger.LogDebug("Updated heartbeat for VS instance: PID={ProcessId}", processId);
return true;
}
return false;
}
public IEnumerable<VSInstanceInfo> GetAllInstances()
{
return _instances.Values.ToList();
}
public VSInstanceInfo? GetByProcessId(int processId)
{
_instances.TryGetValue(processId, out var instance);
return instance;
}
public VSInstanceInfo? GetBySolutionPath(string solutionPath)
{
if (string.IsNullOrEmpty(solutionPath))
return null;
var normalizedPath = Path.GetFullPath(solutionPath).ToLowerInvariant();
return _instances.Values.FirstOrDefault(i =>
!string.IsNullOrEmpty(i.SolutionPath) &&
Path.GetFullPath(i.SolutionPath).ToLowerInvariant() == normalizedPath);
}
public VSInstanceInfo? GetByPort(int port)
{
return _instances.Values.FirstOrDefault(i => i.Port == port);
}
public void RemoveStaleInstances(TimeSpan timeout)
{
var cutoff = DateTime.UtcNow - timeout;
var staleInstances = _instances.Values
.Where(i => i.LastHeartbeat < cutoff)
.ToList();
foreach (var instance in staleInstances)
{
if (_instances.TryRemove(instance.ProcessId, out _))
{
_logger.LogWarning(
"Removed stale VS instance: PID={ProcessId}, LastHeartbeat={LastHeartbeat}",
instance.ProcessId,
instance.LastHeartbeat);
}
}
}
}