fix(backend): Move McpNotificationHub to Infrastructure layer to fix dependency inversion violation
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled

Fixed compilation error where Infrastructure layer was referencing API layer (ColaFlow.API.Hubs).
This violated the dependency inversion principle and Clean Architecture layering rules.

Changes:
- Moved McpNotificationHub from ColaFlow.API/Hubs to ColaFlow.Modules.Mcp.Infrastructure/Hubs
- Updated McpNotificationHub to inherit directly from Hub instead of BaseHub
- Copied necessary helper methods (GetCurrentUserId, GetCurrentTenantId, GetTenantGroupName) to avoid cross-layer dependency
- Updated McpNotificationService to use new namespace (ColaFlow.Modules.Mcp.Infrastructure.Hubs)
- Updated Program.cs to import new Hub namespace
- Updated McpNotificationServiceTests to use new namespace
- Kept BaseHub in API layer for ProjectHub and NotificationHub

Architecture Impact:
- Infrastructure layer no longer depends on API layer
- Proper dependency flow: API -> Infrastructure -> Application -> Domain
- McpNotificationHub is now properly encapsulated within the MCP module

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-09 18:37:08 +01:00
parent 61e0f1249c
commit 1d6e732018
4 changed files with 55 additions and 16 deletions

View File

@@ -0,0 +1,128 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace ColaFlow.Modules.Mcp.Infrastructure.Hubs;
/// <summary>
/// SignalR Hub for MCP real-time notifications
/// Supports notifying AI agents and users about PendingChange status updates
/// </summary>
[Authorize]
public class McpNotificationHub : Hub
{
private readonly ILogger<McpNotificationHub> _logger;
public McpNotificationHub(ILogger<McpNotificationHub> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public override async Task OnConnectedAsync()
{
try
{
var connectionId = Context.ConnectionId;
var userId = GetCurrentUserId();
var tenantId = GetCurrentTenantId();
// Automatically join tenant group (tenant isolation)
await Groups.AddToGroupAsync(Context.ConnectionId, GetTenantGroupName(tenantId));
_logger.LogInformation(
"MCP client connected - ConnectionId={ConnectionId}, UserId={UserId}, TenantId={TenantId}",
connectionId, userId, tenantId);
await base.OnConnectedAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "MCP client connection error");
Context.Abort();
}
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var connectionId = Context.ConnectionId;
if (exception != null)
{
_logger.LogError(exception,
"MCP client disconnected with error - ConnectionId={ConnectionId}",
connectionId);
}
else
{
_logger.LogInformation(
"MCP client disconnected - ConnectionId={ConnectionId}",
connectionId);
}
await base.OnDisconnectedAsync(exception);
}
/// <summary>
/// Subscribe to receive notifications for a specific pending change
/// </summary>
/// <param name="pendingChangeId">The pending change ID to subscribe to</param>
public async Task SubscribeToPendingChange(Guid pendingChangeId)
{
var groupName = GetPendingChangeGroupName(pendingChangeId);
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
_logger.LogInformation(
"Client subscribed - ConnectionId={ConnectionId}, GroupName={GroupName}, PendingChangeId={PendingChangeId}",
Context.ConnectionId, groupName, pendingChangeId);
}
/// <summary>
/// Unsubscribe from receiving notifications for a specific pending change
/// </summary>
/// <param name="pendingChangeId">The pending change ID to unsubscribe from</param>
public async Task UnsubscribeFromPendingChange(Guid pendingChangeId)
{
var groupName = GetPendingChangeGroupName(pendingChangeId);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
_logger.LogInformation(
"Client unsubscribed - ConnectionId={ConnectionId}, GroupName={GroupName}, PendingChangeId={PendingChangeId}",
Context.ConnectionId, groupName, pendingChangeId);
}
// Helper methods (copied from BaseHub to avoid cross-layer dependency)
protected Guid GetCurrentUserId()
{
var userIdClaim = Context.User?.FindFirst("sub")
?? Context.User?.FindFirst("user_id");
if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId))
{
throw new UnauthorizedAccessException("User ID not found in token");
}
return userId;
}
protected Guid GetCurrentTenantId()
{
var tenantIdClaim = Context.User?.FindFirst("tenant_id");
if (tenantIdClaim == null || !Guid.TryParse(tenantIdClaim.Value, out var tenantId))
{
throw new UnauthorizedAccessException("Tenant ID not found in token");
}
return tenantId;
}
protected string GetTenantGroupName(Guid tenantId)
{
return $"tenant-{tenantId}";
}
private static string GetPendingChangeGroupName(Guid pendingChangeId)
{
return $"pending-change-{pendingChangeId}";
}
}

View File

@@ -1,6 +1,6 @@
using ColaFlow.API.Hubs;
using ColaFlow.Modules.Mcp.Application.DTOs.Notifications;
using ColaFlow.Modules.Mcp.Application.Services;
using ColaFlow.Modules.Mcp.Infrastructure.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;