fix(backend): Move McpNotificationHub to Infrastructure layer to fix dependency inversion violation
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:
@@ -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}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user