using System.Security.Cryptography; using Akka.Actor; using Newtonsoft.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.Commons.Entities.InboundApi; using ScadaLink.Commons.Entities.Instances; using ScadaLink.Commons.Entities.Notifications; using ScadaLink.Commons.Entities.Security; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Messages.Management; using ScadaLink.DeploymentManager; using ScadaLink.HealthMonitoring; using ScadaLink.TemplateEngine; using ScadaLink.TemplateEngine.Services; namespace ScadaLink.ManagementService; /// /// Central actor that handles all management commands from the CLI (via ClusterClient). /// Receives ManagementEnvelope messages, authorizes based on roles, then delegates to /// the appropriate service or repository using scoped DI. /// public class ManagementActor : ReceiveActor { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public ManagementActor(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; Receive(HandleEnvelope); } private void HandleEnvelope(ManagementEnvelope envelope) { var sender = Sender; var correlationId = envelope.CorrelationId; var user = envelope.User; // Check authorization var requiredRole = GetRequiredRole(envelope.Command); if (requiredRole != null && !user.Roles.Contains(requiredRole, StringComparer.OrdinalIgnoreCase)) { sender.Tell(new ManagementUnauthorized(correlationId, $"Role '{requiredRole}' required for {envelope.Command.GetType().Name}")); return; } // Process command asynchronously with scoped DI Task.Run(async () => { using var scope = _serviceProvider.CreateScope(); try { var result = await DispatchCommand(scope.ServiceProvider, envelope.Command, user.Username); var json = JsonConvert.SerializeObject(result, Formatting.None); sender.Tell(new ManagementSuccess(correlationId, json)); } catch (Exception ex) { _logger.LogError(ex, "Management command {Command} failed (CorrelationId={CorrelationId})", envelope.Command.GetType().Name, correlationId); sender.Tell(new ManagementError(correlationId, ex.Message, "COMMAND_FAILED")); } }); } private static string? GetRequiredRole(object command) => command switch { // Admin operations CreateSiteCommand or UpdateSiteCommand or DeleteSiteCommand or CreateAreaCommand or DeleteAreaCommand or ListRoleMappingsCommand or CreateRoleMappingCommand or UpdateRoleMappingCommand or DeleteRoleMappingCommand or ListApiKeysCommand or CreateApiKeyCommand or DeleteApiKeyCommand => "Admin", // Design operations CreateTemplateCommand or UpdateTemplateCommand or DeleteTemplateCommand or ValidateTemplateCommand or CreateExternalSystemCommand or UpdateExternalSystemCommand or DeleteExternalSystemCommand or CreateNotificationListCommand or UpdateNotificationListCommand or DeleteNotificationListCommand or UpdateSmtpConfigCommand or CreateDataConnectionCommand or UpdateDataConnectionCommand or DeleteDataConnectionCommand or AssignDataConnectionToSiteCommand or UnassignDataConnectionFromSiteCommand => "Design", // Deployment operations CreateInstanceCommand or MgmtDeployInstanceCommand or MgmtEnableInstanceCommand or MgmtDisableInstanceCommand or MgmtDeleteInstanceCommand or SetConnectionBindingsCommand or MgmtDeployArtifactsCommand => "Deployment", // Read-only queries -- any authenticated user _ => null }; private async Task DispatchCommand(IServiceProvider sp, object command, string user) { return command switch { // Templates ListTemplatesCommand => await HandleListTemplates(sp), GetTemplateCommand cmd => await HandleGetTemplate(sp, cmd), CreateTemplateCommand cmd => await HandleCreateTemplate(sp, cmd, user), UpdateTemplateCommand cmd => await HandleUpdateTemplate(sp, cmd, user), DeleteTemplateCommand cmd => await HandleDeleteTemplate(sp, cmd, user), ValidateTemplateCommand cmd => await HandleValidateTemplate(sp, cmd), // Instances ListInstancesCommand cmd => await HandleListInstances(sp, cmd), GetInstanceCommand cmd => await HandleGetInstance(sp, cmd), CreateInstanceCommand cmd => await HandleCreateInstance(sp, cmd, user), MgmtDeployInstanceCommand cmd => await HandleDeployInstance(sp, cmd, user), MgmtEnableInstanceCommand cmd => await HandleEnableInstance(sp, cmd, user), MgmtDisableInstanceCommand cmd => await HandleDisableInstance(sp, cmd, user), MgmtDeleteInstanceCommand cmd => await HandleDeleteInstance(sp, cmd, user), SetConnectionBindingsCommand cmd => await HandleSetConnectionBindings(sp, cmd, user), // Sites ListSitesCommand => await HandleListSites(sp), GetSiteCommand cmd => await HandleGetSite(sp, cmd), CreateSiteCommand cmd => await HandleCreateSite(sp, cmd), UpdateSiteCommand cmd => await HandleUpdateSite(sp, cmd), DeleteSiteCommand cmd => await HandleDeleteSite(sp, cmd), ListAreasCommand cmd => await HandleListAreas(sp, cmd), CreateAreaCommand cmd => await HandleCreateArea(sp, cmd), DeleteAreaCommand cmd => await HandleDeleteArea(sp, cmd), // Data Connections ListDataConnectionsCommand => await HandleListDataConnections(sp), GetDataConnectionCommand cmd => await HandleGetDataConnection(sp, cmd), CreateDataConnectionCommand cmd => await HandleCreateDataConnection(sp, cmd), UpdateDataConnectionCommand cmd => await HandleUpdateDataConnection(sp, cmd), DeleteDataConnectionCommand cmd => await HandleDeleteDataConnection(sp, cmd), AssignDataConnectionToSiteCommand cmd => await HandleAssignDataConnectionToSite(sp, cmd), UnassignDataConnectionFromSiteCommand cmd => await HandleUnassignDataConnectionFromSite(sp, cmd), // External Systems ListExternalSystemsCommand => await HandleListExternalSystems(sp), GetExternalSystemCommand cmd => await HandleGetExternalSystem(sp, cmd), CreateExternalSystemCommand cmd => await HandleCreateExternalSystem(sp, cmd), UpdateExternalSystemCommand cmd => await HandleUpdateExternalSystem(sp, cmd), DeleteExternalSystemCommand cmd => await HandleDeleteExternalSystem(sp, cmd), // Notification Lists ListNotificationListsCommand => await HandleListNotificationLists(sp), GetNotificationListCommand cmd => await HandleGetNotificationList(sp, cmd), CreateNotificationListCommand cmd => await HandleCreateNotificationList(sp, cmd), UpdateNotificationListCommand cmd => await HandleUpdateNotificationList(sp, cmd), DeleteNotificationListCommand cmd => await HandleDeleteNotificationList(sp, cmd), ListSmtpConfigsCommand => await HandleListSmtpConfigs(sp), UpdateSmtpConfigCommand cmd => await HandleUpdateSmtpConfig(sp, cmd), // Security ListRoleMappingsCommand => await HandleListRoleMappings(sp), CreateRoleMappingCommand cmd => await HandleCreateRoleMapping(sp, cmd), UpdateRoleMappingCommand cmd => await HandleUpdateRoleMapping(sp, cmd), DeleteRoleMappingCommand cmd => await HandleDeleteRoleMapping(sp, cmd), ListApiKeysCommand => await HandleListApiKeys(sp), CreateApiKeyCommand cmd => await HandleCreateApiKey(sp, cmd), DeleteApiKeyCommand cmd => await HandleDeleteApiKey(sp, cmd), // Deployments MgmtDeployArtifactsCommand cmd => await HandleDeployArtifacts(sp, cmd, user), QueryDeploymentsCommand cmd => await HandleQueryDeployments(sp, cmd), // Audit Log QueryAuditLogCommand cmd => await HandleQueryAuditLog(sp, cmd), // Health GetHealthSummaryCommand => HandleGetHealthSummary(sp), GetSiteHealthCommand cmd => HandleGetSiteHealth(sp, cmd), _ => throw new NotSupportedException($"Unknown command type: {command.GetType().Name}") }; } // ======================================================================== // Template handlers // ======================================================================== private static async Task HandleListTemplates(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllTemplatesAsync(); } private static async Task HandleGetTemplate(IServiceProvider sp, GetTemplateCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetTemplateWithChildrenAsync(cmd.TemplateId); } private static async Task HandleCreateTemplate(IServiceProvider sp, CreateTemplateCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.CreateTemplateAsync(cmd.Name, cmd.Description, cmd.ParentTemplateId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleUpdateTemplate(IServiceProvider sp, UpdateTemplateCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.UpdateTemplateAsync(cmd.TemplateId, cmd.Name, cmd.Description, cmd.ParentTemplateId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleDeleteTemplate(IServiceProvider sp, DeleteTemplateCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.DeleteTemplateAsync(cmd.TemplateId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleValidateTemplate(IServiceProvider sp, ValidateTemplateCommand cmd) { var svc = sp.GetRequiredService(); return await svc.DetectCollisionsAsync(cmd.TemplateId); } // ======================================================================== // Instance handlers // ======================================================================== private static async Task HandleListInstances(IServiceProvider sp, ListInstancesCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetInstancesFilteredAsync(cmd.SiteId, cmd.TemplateId, cmd.SearchTerm); } private static async Task HandleGetInstance(IServiceProvider sp, GetInstanceCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetInstanceByIdAsync(cmd.InstanceId); } private static async Task HandleCreateInstance(IServiceProvider sp, CreateInstanceCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.CreateInstanceAsync(cmd.UniqueName, cmd.TemplateId, cmd.SiteId, cmd.AreaId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleDeployInstance(IServiceProvider sp, MgmtDeployInstanceCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.DeployInstanceAsync(cmd.InstanceId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleEnableInstance(IServiceProvider sp, MgmtEnableInstanceCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.EnableInstanceAsync(cmd.InstanceId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleDisableInstance(IServiceProvider sp, MgmtDisableInstanceCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.DisableInstanceAsync(cmd.InstanceId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleDeleteInstance(IServiceProvider sp, MgmtDeleteInstanceCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.DeleteInstanceAsync(cmd.InstanceId, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleSetConnectionBindings(IServiceProvider sp, SetConnectionBindingsCommand cmd, string user) { var svc = sp.GetRequiredService(); var result = await svc.SetConnectionBindingsAsync(cmd.InstanceId, cmd.Bindings, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } // ======================================================================== // Site handlers // ======================================================================== private static async Task HandleListSites(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllSitesAsync(); } private static async Task HandleGetSite(IServiceProvider sp, GetSiteCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetSiteByIdAsync(cmd.SiteId); } private static async Task HandleCreateSite(IServiceProvider sp, CreateSiteCommand cmd) { var repo = sp.GetRequiredService(); var site = new Site(cmd.Name, cmd.SiteIdentifier) { Description = cmd.Description }; await repo.AddSiteAsync(site); await repo.SaveChangesAsync(); return site; } private static async Task HandleUpdateSite(IServiceProvider sp, UpdateSiteCommand cmd) { var repo = sp.GetRequiredService(); var site = await repo.GetSiteByIdAsync(cmd.SiteId) ?? throw new InvalidOperationException($"Site with ID {cmd.SiteId} not found."); site.Name = cmd.Name; site.Description = cmd.Description; await repo.UpdateSiteAsync(site); await repo.SaveChangesAsync(); return site; } private static async Task HandleDeleteSite(IServiceProvider sp, DeleteSiteCommand cmd) { var repo = sp.GetRequiredService(); // Check for instances referencing this site var instances = await repo.GetInstancesBySiteIdAsync(cmd.SiteId); if (instances.Count > 0) throw new InvalidOperationException( $"Cannot delete site {cmd.SiteId}: it has {instances.Count} instance(s)."); await repo.DeleteSiteAsync(cmd.SiteId); await repo.SaveChangesAsync(); return true; } private static async Task HandleListAreas(IServiceProvider sp, ListAreasCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetAreaTreeBySiteIdAsync(cmd.SiteId); } private static async Task HandleCreateArea(IServiceProvider sp, CreateAreaCommand cmd) { var repo = sp.GetRequiredService(); var area = new Area(cmd.Name) { SiteId = cmd.SiteId, ParentAreaId = cmd.ParentAreaId }; await repo.AddAreaAsync(area); await repo.SaveChangesAsync(); return area; } private static async Task HandleDeleteArea(IServiceProvider sp, DeleteAreaCommand cmd) { var repo = sp.GetRequiredService(); await repo.DeleteAreaAsync(cmd.AreaId); await repo.SaveChangesAsync(); return true; } // ======================================================================== // Data Connection handlers // ======================================================================== private static async Task HandleListDataConnections(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllDataConnectionsAsync(); } private static async Task HandleGetDataConnection(IServiceProvider sp, GetDataConnectionCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetDataConnectionByIdAsync(cmd.DataConnectionId); } private static async Task HandleCreateDataConnection(IServiceProvider sp, CreateDataConnectionCommand cmd) { var repo = sp.GetRequiredService(); var conn = new DataConnection(cmd.Name, cmd.Protocol) { Configuration = cmd.Configuration }; await repo.AddDataConnectionAsync(conn); await repo.SaveChangesAsync(); return conn; } private static async Task HandleUpdateDataConnection(IServiceProvider sp, UpdateDataConnectionCommand cmd) { var repo = sp.GetRequiredService(); var conn = await repo.GetDataConnectionByIdAsync(cmd.DataConnectionId) ?? throw new InvalidOperationException($"DataConnection with ID {cmd.DataConnectionId} not found."); conn.Name = cmd.Name; conn.Protocol = cmd.Protocol; conn.Configuration = cmd.Configuration; await repo.UpdateDataConnectionAsync(conn); await repo.SaveChangesAsync(); return conn; } private static async Task HandleDeleteDataConnection(IServiceProvider sp, DeleteDataConnectionCommand cmd) { var repo = sp.GetRequiredService(); await repo.DeleteDataConnectionAsync(cmd.DataConnectionId); await repo.SaveChangesAsync(); return true; } private static async Task HandleAssignDataConnectionToSite(IServiceProvider sp, AssignDataConnectionToSiteCommand cmd) { var repo = sp.GetRequiredService(); var assignment = new SiteDataConnectionAssignment { SiteId = cmd.SiteId, DataConnectionId = cmd.DataConnectionId }; await repo.AddSiteDataConnectionAssignmentAsync(assignment); await repo.SaveChangesAsync(); return assignment; } private static async Task HandleUnassignDataConnectionFromSite(IServiceProvider sp, UnassignDataConnectionFromSiteCommand cmd) { var repo = sp.GetRequiredService(); await repo.DeleteSiteDataConnectionAssignmentAsync(cmd.AssignmentId); await repo.SaveChangesAsync(); return true; } // ======================================================================== // External System handlers // ======================================================================== private static async Task HandleListExternalSystems(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllExternalSystemsAsync(); } private static async Task HandleGetExternalSystem(IServiceProvider sp, GetExternalSystemCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetExternalSystemByIdAsync(cmd.ExternalSystemId); } private static async Task HandleCreateExternalSystem(IServiceProvider sp, CreateExternalSystemCommand cmd) { var repo = sp.GetRequiredService(); var def = new ExternalSystemDefinition(cmd.Name, cmd.EndpointUrl, cmd.AuthType) { AuthConfiguration = cmd.AuthConfiguration }; await repo.AddExternalSystemAsync(def); await repo.SaveChangesAsync(); return def; } private static async Task HandleUpdateExternalSystem(IServiceProvider sp, UpdateExternalSystemCommand cmd) { var repo = sp.GetRequiredService(); var def = await repo.GetExternalSystemByIdAsync(cmd.ExternalSystemId) ?? throw new InvalidOperationException($"ExternalSystem with ID {cmd.ExternalSystemId} not found."); def.Name = cmd.Name; def.EndpointUrl = cmd.EndpointUrl; def.AuthType = cmd.AuthType; def.AuthConfiguration = cmd.AuthConfiguration; await repo.UpdateExternalSystemAsync(def); await repo.SaveChangesAsync(); return def; } private static async Task HandleDeleteExternalSystem(IServiceProvider sp, DeleteExternalSystemCommand cmd) { var repo = sp.GetRequiredService(); await repo.DeleteExternalSystemAsync(cmd.ExternalSystemId); await repo.SaveChangesAsync(); return true; } // ======================================================================== // Notification handlers // ======================================================================== private static async Task HandleListNotificationLists(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllNotificationListsAsync(); } private static async Task HandleGetNotificationList(IServiceProvider sp, GetNotificationListCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetNotificationListByIdAsync(cmd.NotificationListId); } private static async Task HandleCreateNotificationList(IServiceProvider sp, CreateNotificationListCommand cmd) { var repo = sp.GetRequiredService(); var list = new NotificationList(cmd.Name); foreach (var email in cmd.RecipientEmails) { list.Recipients.Add(new NotificationRecipient(email, email)); } await repo.AddNotificationListAsync(list); await repo.SaveChangesAsync(); return list; } private static async Task HandleUpdateNotificationList(IServiceProvider sp, UpdateNotificationListCommand cmd) { var repo = sp.GetRequiredService(); var list = await repo.GetNotificationListByIdAsync(cmd.NotificationListId) ?? throw new InvalidOperationException($"NotificationList with ID {cmd.NotificationListId} not found."); list.Name = cmd.Name; // Remove existing recipients and re-add var existingRecipients = await repo.GetRecipientsByListIdAsync(cmd.NotificationListId); foreach (var r in existingRecipients) { await repo.DeleteRecipientAsync(r.Id); } foreach (var email in cmd.RecipientEmails) { await repo.AddRecipientAsync(new NotificationRecipient(email, email) { NotificationListId = cmd.NotificationListId }); } await repo.UpdateNotificationListAsync(list); await repo.SaveChangesAsync(); return list; } private static async Task HandleDeleteNotificationList(IServiceProvider sp, DeleteNotificationListCommand cmd) { var repo = sp.GetRequiredService(); await repo.DeleteNotificationListAsync(cmd.NotificationListId); await repo.SaveChangesAsync(); return true; } private static async Task HandleListSmtpConfigs(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllSmtpConfigurationsAsync(); } private static async Task HandleUpdateSmtpConfig(IServiceProvider sp, UpdateSmtpConfigCommand cmd) { var repo = sp.GetRequiredService(); var config = await repo.GetSmtpConfigurationByIdAsync(cmd.SmtpConfigId) ?? throw new InvalidOperationException($"SmtpConfiguration with ID {cmd.SmtpConfigId} not found."); config.Host = cmd.Server; config.Port = cmd.Port; config.AuthType = cmd.AuthMode; config.FromAddress = cmd.FromAddress; await repo.UpdateSmtpConfigurationAsync(config); await repo.SaveChangesAsync(); return config; } // ======================================================================== // Security handlers // ======================================================================== private static async Task HandleListRoleMappings(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllMappingsAsync(); } private static async Task HandleCreateRoleMapping(IServiceProvider sp, CreateRoleMappingCommand cmd) { var repo = sp.GetRequiredService(); var mapping = new LdapGroupMapping(cmd.LdapGroupName, cmd.Role); await repo.AddMappingAsync(mapping); await repo.SaveChangesAsync(); return mapping; } private static async Task HandleUpdateRoleMapping(IServiceProvider sp, UpdateRoleMappingCommand cmd) { var repo = sp.GetRequiredService(); var mapping = await repo.GetMappingByIdAsync(cmd.MappingId) ?? throw new InvalidOperationException($"RoleMapping with ID {cmd.MappingId} not found."); mapping.LdapGroupName = cmd.LdapGroupName; mapping.Role = cmd.Role; await repo.UpdateMappingAsync(mapping); await repo.SaveChangesAsync(); return mapping; } private static async Task HandleDeleteRoleMapping(IServiceProvider sp, DeleteRoleMappingCommand cmd) { var repo = sp.GetRequiredService(); await repo.DeleteMappingAsync(cmd.MappingId); await repo.SaveChangesAsync(); return true; } private static async Task HandleListApiKeys(IServiceProvider sp) { var repo = sp.GetRequiredService(); return await repo.GetAllApiKeysAsync(); } private static async Task HandleCreateApiKey(IServiceProvider sp, CreateApiKeyCommand cmd) { var repo = sp.GetRequiredService(); var keyValue = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)); var apiKey = new ApiKey(cmd.Name, keyValue) { IsEnabled = true }; await repo.AddApiKeyAsync(apiKey); await repo.SaveChangesAsync(); return apiKey; } private static async Task HandleDeleteApiKey(IServiceProvider sp, DeleteApiKeyCommand cmd) { var repo = sp.GetRequiredService(); await repo.DeleteApiKeyAsync(cmd.ApiKeyId); await repo.SaveChangesAsync(); return true; } // ======================================================================== // Deployment handlers // ======================================================================== private static async Task HandleDeployArtifacts(IServiceProvider sp, MgmtDeployArtifactsCommand cmd, string user) { var svc = sp.GetRequiredService(); var command = await svc.BuildDeployArtifactsCommandAsync(); var result = await svc.DeployToAllSitesAsync(command, user); return result.IsSuccess ? result.Value : throw new InvalidOperationException(result.Error); } private static async Task HandleQueryDeployments(IServiceProvider sp, QueryDeploymentsCommand cmd) { var repo = sp.GetRequiredService(); if (cmd.InstanceId.HasValue) return await repo.GetDeploymentsByInstanceIdAsync(cmd.InstanceId.Value); return await repo.GetAllDeploymentRecordsAsync(); } // ======================================================================== // Audit Log handler // ======================================================================== private static async Task HandleQueryAuditLog(IServiceProvider sp, QueryAuditLogCommand cmd) { var repo = sp.GetRequiredService(); return await repo.GetAuditLogEntriesAsync( cmd.User, cmd.EntityType, cmd.Action, cmd.From, cmd.To, page: cmd.Page, pageSize: cmd.PageSize); } // ======================================================================== // Health handlers // ======================================================================== private static object? HandleGetHealthSummary(IServiceProvider sp) { var aggregator = sp.GetRequiredService(); return aggregator.GetAllSiteStates(); } private static object? HandleGetSiteHealth(IServiceProvider sp, GetSiteHealthCommand cmd) { var aggregator = sp.GetRequiredService(); return aggregator.GetSiteState(cmd.SiteIdentifier); } }