using Collaboration.Application.Interfaces;
using Collaboration.Application.Services;
using Collaboration.Hubs;
using Collaboration.Infrastructure.Persistence;
using Collaboration.Infrastructure.Repositories;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Collaboration.Api;
public static class CollabModule
{
///
/// Register all Collaboration module services.
/// Call from your main application's Program.cs:
/// builder.Services.AddCollaborationModule(builder.Configuration);
///
public static IServiceCollection AddCollaborationModule(
this IServiceCollection services,
IConfiguration configuration)
{
// DbContext — use separate connection string or share your main one
services.AddDbContext(options =>
options.UseSqlServer(
configuration.GetConnectionString("CollabConnection")
?? configuration.GetConnectionString("DefaultConnection"),
sql => sql.MigrationsHistoryTable("__CollabMigrationsHistory", "collab")
));
// Repositories
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
// Application services
services.AddScoped();
services.AddScoped();
// SignalR
services.AddSignalR(options =>
{
options.EnableDetailedErrors = false; // Set true in Development
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
options.HandshakeTimeout = TimeSpan.FromSeconds(10);
options.MaximumReceiveMessageSize = 32 * 1024; // 32KB — enough for payloads
});
// Background service for session cleanup
services.AddHostedService();
return services;
}
///
/// Map Collaboration module endpoints.
/// Call from your main application's Program.cs after app.UseAuthorization():
/// app.UseCollaborationModule();
///
public static WebApplication UseCollaborationModule(this WebApplication app)
{
app.MapHub("/hubs/collab", options =>
{
options.Transports =
Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets |
Microsoft.AspNetCore.Http.Connections.HttpTransportType.LongPolling;
});
return app;
}
}
// ── Background cleanup service ───────────────────────────────────────────────
///
/// Runs every 5 minutes to auto-abandon timed-out sessions.
/// Configurable timeout per org is respected.
///
public class CollabSessionCleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger _logger;
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(5);
public CollabSessionCleanupService(
IServiceScopeFactory scopeFactory,
ILogger logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(Interval, stoppingToken);
await RunCleanupAsync(stoppingToken);
}
}
private async Task RunCleanupAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var sessions = scope.ServiceProvider.GetRequiredService();
var hub = scope.ServiceProvider.GetRequiredService<
IHubContext>();
try
{
// Use default timeout of 30 mins for the scan —
// fine-grained per-org timeout can be added when needed
var candidates = await sessions.GetAbandonedCandidatesAsync(timeoutMinutes: 30, ct);
foreach (var session in candidates)
{
session.End(Guid.Empty, isAbandoned: true);
await hub.Clients.Group($"session:{session.SessionId}")
.SessionEnded("Session ended due to inactivity.");
}
if (candidates.Count > 0)
{
await sessions.SaveChangesAsync(ct);
_logger.LogInformation("Abandoned {Count} inactive sessions", candidates.Count);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during session cleanup");
}
}
}