From 44b107d4eb4dee7873c43f041e7ade95066d72c7 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 19 Dec 2025 15:15:31 +0100 Subject: [PATCH] Refactor to CQRS --- .../Commands/DestroyBatchScenesCommand.cs | 36 ++++ .../SeederApi/Commands/DestroySceneCommand.cs | 57 ++++++ .../Interfaces/IDestroyBatchScenesCommand.cs | 14 ++ .../Interfaces/IDestroySceneCommand.cs | 15 ++ util/SeederApi/Controllers/QueryController.cs | 7 +- util/SeederApi/Controllers/SeedController.cs | 22 ++- util/SeederApi/Execution/IQueryExecutor.cs | 22 +++ util/SeederApi/Execution/ISceneExecutor.cs | 22 +++ util/SeederApi/Execution/JsonConfiguration.cs | 19 ++ .../QueryExecutor.cs} | 20 +- util/SeederApi/Execution/SceneExecutor.cs | 78 ++++++++ .../Extensions/ServiceCollectionExtensions.cs | 21 +++ util/SeederApi/Queries/GetAllPlayIdsQuery.cs | 15 ++ .../Queries/Interfaces/IGetAllPlayIdsQuery.cs | 13 ++ util/SeederApi/Services/IQueryService.cs | 24 --- util/SeederApi/Services/ISceneService.cs | 46 ----- util/SeederApi/Services/SceneService.cs | 171 ------------------ util/SeederApi/Startup.cs | 6 +- 18 files changed, 341 insertions(+), 267 deletions(-) create mode 100644 util/SeederApi/Commands/DestroyBatchScenesCommand.cs create mode 100644 util/SeederApi/Commands/DestroySceneCommand.cs create mode 100644 util/SeederApi/Commands/Interfaces/IDestroyBatchScenesCommand.cs create mode 100644 util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs create mode 100644 util/SeederApi/Execution/IQueryExecutor.cs create mode 100644 util/SeederApi/Execution/ISceneExecutor.cs create mode 100644 util/SeederApi/Execution/JsonConfiguration.cs rename util/SeederApi/{Services/QueryService.cs => Execution/QueryExecutor.cs} (83%) create mode 100644 util/SeederApi/Execution/SceneExecutor.cs create mode 100644 util/SeederApi/Queries/GetAllPlayIdsQuery.cs create mode 100644 util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs delete mode 100644 util/SeederApi/Services/IQueryService.cs delete mode 100644 util/SeederApi/Services/ISceneService.cs delete mode 100644 util/SeederApi/Services/SceneService.cs diff --git a/util/SeederApi/Commands/DestroyBatchScenesCommand.cs b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs new file mode 100644 index 0000000000..50f6142a98 --- /dev/null +++ b/util/SeederApi/Commands/DestroyBatchScenesCommand.cs @@ -0,0 +1,36 @@ +using Bit.SeederApi.Commands.Interfaces; + +namespace Bit.SeederApi.Commands; + +public class DestroyBatchScenesCommand( + ILogger logger, + IDestroySceneCommand destroySceneCommand) : IDestroyBatchScenesCommand +{ + public async Task DestroyAsync(IEnumerable playIds) + { + var exceptions = new List(); + + var deleteTasks = playIds.Select(async playId => + { + try + { + await destroySceneCommand.DestroyAsync(playId); + } + catch (Exception ex) + { + lock (exceptions) + { + exceptions.Add(ex); + } + logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId); + } + }); + + await Task.WhenAll(deleteTasks); + + if (exceptions.Count > 0) + { + throw new AggregateException("One or more errors occurred while deleting seeded data", exceptions); + } + } +} diff --git a/util/SeederApi/Commands/DestroySceneCommand.cs b/util/SeederApi/Commands/DestroySceneCommand.cs new file mode 100644 index 0000000000..f70fd861fc --- /dev/null +++ b/util/SeederApi/Commands/DestroySceneCommand.cs @@ -0,0 +1,57 @@ +using Bit.Core.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.SeederApi.Commands.Interfaces; +using Bit.SeederApi.Services; + +namespace Bit.SeederApi.Commands; + +public class DestroySceneCommand( + DatabaseContext databaseContext, + ILogger logger, + IUserRepository userRepository, + IPlayDataRepository playDataRepository, + IOrganizationRepository organizationRepository) : IDestroySceneCommand +{ + public async Task DestroyAsync(string playId) + { + // Note, delete cascade will remove PlayData entries + + var playData = await playDataRepository.GetByPlayIdAsync(playId); + var userIds = playData.Select(pd => pd.UserId).Distinct().ToList(); + var organizationIds = playData.Select(pd => pd.OrganizationId).Distinct().ToList(); + + // Delete Users before Organizations to respect foreign key constraints + if (userIds.Count > 0) + { + var users = databaseContext.Users.Where(u => userIds.Contains(u.Id)); + await userRepository.DeleteManyAsync(users); + } + + if (organizationIds.Count > 0) + { + var organizations = databaseContext.Organizations.Where(o => organizationIds.Contains(o.Id)); + var aggregateException = new AggregateException(); + foreach (var org in organizations) + { + try + { + await organizationRepository.DeleteAsync(org); + } + catch (Exception ex) + { + aggregateException = new AggregateException(aggregateException, ex); + } + } + if (aggregateException.InnerExceptions.Count > 0) + { + throw new SceneExecutionException( + $"One or more errors occurred while deleting organizations for seed ID {playId}", + aggregateException); + } + } + + logger.LogInformation("Successfully destroyed seeded data with ID {PlayId}", playId); + + return new { PlayId = playId }; + } +} diff --git a/util/SeederApi/Commands/Interfaces/IDestroyBatchScenesCommand.cs b/util/SeederApi/Commands/Interfaces/IDestroyBatchScenesCommand.cs new file mode 100644 index 0000000000..ce43f26a54 --- /dev/null +++ b/util/SeederApi/Commands/Interfaces/IDestroyBatchScenesCommand.cs @@ -0,0 +1,14 @@ +namespace Bit.SeederApi.Commands.Interfaces; + +/// +/// Command for destroying multiple scenes in parallel. +/// +public interface IDestroyBatchScenesCommand +{ + /// + /// Destroys multiple scenes by their play IDs in parallel. + /// + /// The list of play IDs to destroy + /// Thrown when one or more scenes fail to destroy + Task DestroyAsync(IEnumerable playIds); +} diff --git a/util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs b/util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs new file mode 100644 index 0000000000..a3b0800bb2 --- /dev/null +++ b/util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs @@ -0,0 +1,15 @@ +namespace Bit.SeederApi.Commands.Interfaces; + +/// +/// Command for destroying data created by a single scene. +/// +public interface IDestroySceneCommand +{ + /// + /// Destroys data created by a scene using the seeded data ID. + /// + /// The ID of the seeded data to destroy + /// The result of the destroy operation + /// Thrown when there's an error destroying the seeded data + Task DestroyAsync(string playId); +} diff --git a/util/SeederApi/Controllers/QueryController.cs b/util/SeederApi/Controllers/QueryController.cs index ac07981b95..22bf84e5b7 100644 --- a/util/SeederApi/Controllers/QueryController.cs +++ b/util/SeederApi/Controllers/QueryController.cs @@ -1,11 +1,12 @@ -using Bit.SeederApi.Models.Request; +using Bit.SeederApi.Execution; +using Bit.SeederApi.Models.Request; using Bit.SeederApi.Services; using Microsoft.AspNetCore.Mvc; namespace Bit.SeederApi.Controllers; [Route("query")] -public class QueryController(ILogger logger, IQueryService queryService) : Controller +public class QueryController(ILogger logger, IQueryExecutor queryExecutor) : Controller { [HttpPost] public IActionResult Query([FromBody] QueryRequestModel request) @@ -14,7 +15,7 @@ public class QueryController(ILogger logger, IQueryService quer try { - var result = queryService.ExecuteQuery(request.Template, request.Arguments); + var result = queryExecutor.Execute(request.Template, request.Arguments); return Json(result); } diff --git a/util/SeederApi/Controllers/SeedController.cs b/util/SeederApi/Controllers/SeedController.cs index 28096fc6d6..44f0dbaf2c 100644 --- a/util/SeederApi/Controllers/SeedController.cs +++ b/util/SeederApi/Controllers/SeedController.cs @@ -1,11 +1,19 @@ -using Bit.SeederApi.Models.Request; +using Bit.SeederApi.Commands.Interfaces; +using Bit.SeederApi.Execution; +using Bit.SeederApi.Models.Request; +using Bit.SeederApi.Queries.Interfaces; using Bit.SeederApi.Services; using Microsoft.AspNetCore.Mvc; namespace Bit.SeederApi.Controllers; [Route("seed")] -public class SeedController(ILogger logger, ISceneService sceneService) : Controller +public class SeedController( + ILogger logger, + ISceneExecutor sceneExecutor, + IDestroySceneCommand destroySceneCommand, + IDestroyBatchScenesCommand destroyBatchScenesCommand, + IGetAllPlayIdsQuery getAllPlayIdsQuery) : Controller { [HttpPost] public async Task SeedAsync([FromBody] SeedRequestModel request) @@ -14,7 +22,7 @@ public class SeedController(ILogger logger, ISceneService sceneS try { - var response = await sceneService.ExecuteSceneAsync(request.Template, request.Arguments); + var response = await sceneExecutor.ExecuteAsync(request.Template, request.Arguments); return Json(response); } @@ -36,7 +44,7 @@ public class SeedController(ILogger logger, ISceneService sceneS try { - await sceneService.DestroyScenesAsync(playIds); + await destroyBatchScenesCommand.DestroyAsync(playIds); return Ok(new { Message = "Batch delete completed successfully" }); } catch (AggregateException ex) @@ -56,7 +64,7 @@ public class SeedController(ILogger logger, ISceneService sceneS try { - var result = await sceneService.DestroySceneAsync(playId); + var result = await destroySceneCommand.DestroyAsync(playId); return Json(result); } @@ -73,11 +81,11 @@ public class SeedController(ILogger logger, ISceneService sceneS { logger.LogInformation("Deleting all seeded data"); - var playIds = sceneService.GetAllPlayIds(); + var playIds = getAllPlayIdsQuery.GetAllPlayIds(); try { - await sceneService.DestroyScenesAsync(playIds); + await destroyBatchScenesCommand.DestroyAsync(playIds); return NoContent(); } catch (AggregateException ex) diff --git a/util/SeederApi/Execution/IQueryExecutor.cs b/util/SeederApi/Execution/IQueryExecutor.cs new file mode 100644 index 0000000000..ebd971bbb7 --- /dev/null +++ b/util/SeederApi/Execution/IQueryExecutor.cs @@ -0,0 +1,22 @@ +using System.Text.Json; + +namespace Bit.SeederApi.Execution; + +/// +/// Executor for dynamically resolving and executing queries by name. +/// This is an infrastructure component that orchestrates query execution, +/// not a domain-level query. +/// +public interface IQueryExecutor +{ + /// + /// Executes a query with the given query name and arguments. + /// Queries are read-only and do not track entities or create seed IDs. + /// + /// The name of the query (e.g., "EmergencyAccessInviteQuery") + /// Optional JSON arguments to pass to the query's Execute method + /// The result of the query execution + /// Thrown when the query is not found + /// Thrown when there's an error executing the query + object Execute(string queryName, JsonElement? arguments); +} diff --git a/util/SeederApi/Execution/ISceneExecutor.cs b/util/SeederApi/Execution/ISceneExecutor.cs new file mode 100644 index 0000000000..f15909ea79 --- /dev/null +++ b/util/SeederApi/Execution/ISceneExecutor.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using Bit.SeederApi.Models.Response; + +namespace Bit.SeederApi.Execution; + +/// +/// Executor for dynamically resolving and executing scenes by template name. +/// This is an infrastructure component that orchestrates scene execution, +/// not a domain-level command. +/// +public interface ISceneExecutor +{ + /// + /// Executes a scene with the given template name and arguments. + /// + /// The name of the scene template (e.g., "SingleUserScene") + /// Optional JSON arguments to pass to the scene's Seed method + /// A scene response model containing the result and mangle map + /// Thrown when the scene template is not found + /// Thrown when there's an error executing the scene + Task ExecuteAsync(string templateName, JsonElement? arguments); +} diff --git a/util/SeederApi/Execution/JsonConfiguration.cs b/util/SeederApi/Execution/JsonConfiguration.cs new file mode 100644 index 0000000000..beef36e62a --- /dev/null +++ b/util/SeederApi/Execution/JsonConfiguration.cs @@ -0,0 +1,19 @@ +using System.Text.Json; + +namespace Bit.SeederApi.Execution; + +/// +/// Provides shared JSON serialization configuration for executors. +/// +internal static class JsonConfiguration +{ + /// + /// Standard JSON serializer options used for deserializing scene and query request models. + /// Uses case-insensitive property matching and camelCase naming policy. + /// + internal static readonly JsonSerializerOptions Options = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; +} diff --git a/util/SeederApi/Services/QueryService.cs b/util/SeederApi/Execution/QueryExecutor.cs similarity index 83% rename from util/SeederApi/Services/QueryService.cs rename to util/SeederApi/Execution/QueryExecutor.cs index 7314f1066a..5473586c22 100644 --- a/util/SeederApi/Services/QueryService.cs +++ b/util/SeederApi/Execution/QueryExecutor.cs @@ -1,20 +1,15 @@ using System.Text.Json; using Bit.Seeder; +using Bit.SeederApi.Services; -namespace Bit.SeederApi.Services; +namespace Bit.SeederApi.Execution; -public class QueryService( - ILogger logger, - IServiceProvider serviceProvider) - : IQueryService +public class QueryExecutor( + ILogger logger, + IServiceProvider serviceProvider) : IQueryExecutor { - private static readonly JsonSerializerOptions _jsonOptions = new() - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - public object ExecuteQuery(string queryName, JsonElement? arguments) + public object Execute(string queryName, JsonElement? arguments) { try { @@ -22,7 +17,6 @@ public class QueryService( ?? throw new QueryNotFoundException(queryName); var requestType = query.GetRequestType(); - var requestModel = DeserializeRequestModel(queryName, requestType, arguments); var result = query.Execute(requestModel); @@ -47,7 +41,7 @@ public class QueryService( try { - var requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions); + var requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, JsonConfiguration.Options); if (requestModel == null) { throw new QueryExecutionException( diff --git a/util/SeederApi/Execution/SceneExecutor.cs b/util/SeederApi/Execution/SceneExecutor.cs new file mode 100644 index 0000000000..f31dd7d943 --- /dev/null +++ b/util/SeederApi/Execution/SceneExecutor.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using Bit.Seeder; +using Bit.SeederApi.Models.Response; +using Bit.SeederApi.Services; + +namespace Bit.SeederApi.Execution; + +public class SceneExecutor( + ILogger logger, + IServiceProvider serviceProvider) : ISceneExecutor +{ + + public async Task ExecuteAsync(string templateName, JsonElement? arguments) + { + try + { + var scene = serviceProvider.GetKeyedService(templateName) + ?? throw new SceneNotFoundException(templateName); + + var requestType = scene.GetRequestType(); + var requestModel = DeserializeRequestModel(templateName, requestType, arguments); + var result = await scene.SeedAsync(requestModel); + + logger.LogInformation("Successfully executed scene: {TemplateName}", templateName); + return SceneResponseModel.FromSceneResult(result); + } + catch (Exception ex) when (ex is not SceneNotFoundException and not SceneExecutionException) + { + logger.LogError(ex, "Unexpected error executing scene: {TemplateName}", templateName); + throw new SceneExecutionException( + $"An unexpected error occurred while executing scene '{templateName}'", + ex.InnerException ?? ex); + } + } + + private object DeserializeRequestModel(string templateName, Type requestType, JsonElement? arguments) + { + if (arguments == null) + { + return CreateDefaultRequestModel(templateName, requestType); + } + + try + { + var requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, JsonConfiguration.Options); + if (requestModel == null) + { + throw new SceneExecutionException( + $"Failed to deserialize request model for scene '{templateName}'"); + } + return requestModel; + } + catch (JsonException ex) + { + throw new SceneExecutionException( + $"Failed to deserialize request model for scene '{templateName}': {ex.Message}", ex); + } + } + + private object CreateDefaultRequestModel(string templateName, Type requestType) + { + try + { + var requestModel = Activator.CreateInstance(requestType); + if (requestModel == null) + { + throw new SceneExecutionException( + $"Arguments are required for scene '{templateName}'"); + } + return requestModel; + } + catch + { + throw new SceneExecutionException( + $"Arguments are required for scene '{templateName}'"); + } + } +} diff --git a/util/SeederApi/Extensions/ServiceCollectionExtensions.cs b/util/SeederApi/Extensions/ServiceCollectionExtensions.cs index eae01abdf6..052da28dfc 100644 --- a/util/SeederApi/Extensions/ServiceCollectionExtensions.cs +++ b/util/SeederApi/Extensions/ServiceCollectionExtensions.cs @@ -1,11 +1,32 @@ using System.Reflection; using Bit.Seeder; +using Bit.SeederApi.Commands; +using Bit.SeederApi.Commands.Interfaces; +using Bit.SeederApi.Execution; +using Bit.SeederApi.Queries; +using Bit.SeederApi.Queries.Interfaces; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Bit.SeederApi.Extensions; public static class ServiceCollectionExtensions { + /// + /// Registers SeederApi executors, commands, and queries. + /// + public static IServiceCollection AddSeederApiServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + + return services; + } + /// /// Dynamically registers all scene types that implement IScene<TRequest> from the Seeder assembly. /// Scenes are registered as keyed scoped services using their class name as the key. diff --git a/util/SeederApi/Queries/GetAllPlayIdsQuery.cs b/util/SeederApi/Queries/GetAllPlayIdsQuery.cs new file mode 100644 index 0000000000..c21b5ba6a3 --- /dev/null +++ b/util/SeederApi/Queries/GetAllPlayIdsQuery.cs @@ -0,0 +1,15 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.SeederApi.Queries.Interfaces; + +namespace Bit.SeederApi.Queries; + +public class GetAllPlayIdsQuery(DatabaseContext databaseContext) : IGetAllPlayIdsQuery +{ + public List GetAllPlayIds() + { + return databaseContext.PlayData + .Select(pd => pd.PlayId) + .Distinct() + .ToList(); + } +} diff --git a/util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs b/util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs new file mode 100644 index 0000000000..ea9c44991a --- /dev/null +++ b/util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs @@ -0,0 +1,13 @@ +namespace Bit.SeederApi.Queries.Interfaces; + +/// +/// Query for retrieving all play IDs for currently tracked seeded data. +/// +public interface IGetAllPlayIdsQuery +{ + /// + /// Retrieves all play IDs for currently tracked seeded data. + /// + /// A list of play IDs representing active seeded data that can be destroyed. + List GetAllPlayIds(); +} diff --git a/util/SeederApi/Services/IQueryService.cs b/util/SeederApi/Services/IQueryService.cs deleted file mode 100644 index e13f7fff63..0000000000 --- a/util/SeederApi/Services/IQueryService.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json; - -namespace Bit.SeederApi.Services; - -/// -/// Service for executing query operations. -/// -/// -/// The query service provides a mechanism to execute read-only query operations by name with optional JSON arguments. -/// Queries retrieve existing data from the system without modifying state or tracking entities. -/// -public interface IQueryService -{ - /// - /// Executes a query with the given query name and arguments. - /// Queries are read-only and do not track entities or create seed IDs. - /// - /// The name of the query (e.g., "EmergencyAccessInviteQuery") - /// Optional JSON arguments to pass to the query's Execute method - /// The result of the query execution - /// Thrown when the query is not found - /// Thrown when there's an error executing the query - object ExecuteQuery(string queryName, JsonElement? arguments); -} diff --git a/util/SeederApi/Services/ISceneService.cs b/util/SeederApi/Services/ISceneService.cs deleted file mode 100644 index 1b7f208b21..0000000000 --- a/util/SeederApi/Services/ISceneService.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Text.Json; -using Bit.SeederApi.Models.Response; - -namespace Bit.SeederApi.Services; - -/// -/// Service for executing and managing scene operations. -/// -/// -/// The scene service provides a mechanism to execute scene operations by name with optional JSON arguments. -/// Scenes create and configure test data, track entities for cleanup, and support destruction of seeded data. -/// Each scene execution can be assigned a play ID for tracking and subsequent cleanup operations. -/// -public interface ISceneService -{ - /// - /// Executes a scene with the given template name and arguments. - /// - /// The name of the scene template (e.g., "SingleUserScene") - /// Optional JSON arguments to pass to the scene's Seed method - /// A tuple containing the result and optional seed ID for tracked entities - /// Thrown when the scene template is not found - /// Thrown when there's an error executing the scene - Task ExecuteSceneAsync(string templateName, JsonElement? arguments); - - /// - /// Destroys data created by a scene using the seeded data ID. - /// - /// The ID of the seeded data to destroy - /// The result of the destroy operation - /// Thrown when there's an error destroying the seeded data - Task DestroySceneAsync(string playId); - - /// - /// Retrieves all play IDs for currently tracked seeded data. - /// - /// A list of play IDs representing active seeded data that can be destroyed. - List GetAllPlayIds(); - - /// - /// Destroys multiple scenes by their play IDs. - /// - /// The list of play IDs to destroy - /// Thrown when one or more scenes fail to destroy - Task DestroyScenesAsync(IEnumerable playIds); -} diff --git a/util/SeederApi/Services/SceneService.cs b/util/SeederApi/Services/SceneService.cs deleted file mode 100644 index b9760750db..0000000000 --- a/util/SeederApi/Services/SceneService.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System.Text.Json; -using Bit.Core.Repositories; -using Bit.Infrastructure.EntityFramework.Repositories; -using Bit.Seeder; -using Bit.SeederApi.Models.Response; - -namespace Bit.SeederApi.Services; - -public class SceneService( - DatabaseContext databaseContext, - ILogger logger, - IServiceProvider serviceProvider, - IUserRepository userRepository, - IPlayDataRepository playDataRepository, - IOrganizationRepository organizationRepository) - : ISceneService -{ - private static readonly JsonSerializerOptions _jsonOptions = new() - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - - public List GetAllPlayIds() - { - return databaseContext.PlayData - .Select(pd => pd.PlayId) - .Distinct() - .ToList(); - } - - public async Task ExecuteSceneAsync(string templateName, JsonElement? arguments) - { - var result = await ExecuteSceneMethodAsync(templateName, arguments, "Seed"); - - return SceneResponseModel.FromSceneResult(result); - } - - public async Task DestroySceneAsync(string playId) - { - // Note, delete cascade will remove PlayData entries - - var playData = await playDataRepository.GetByPlayIdAsync(playId); - var userIds = playData.Select(pd => pd.UserId).Distinct().ToList(); - var organizationIds = playData.Select(pd => pd.OrganizationId).Distinct().ToList(); - - // Delete Users before Organizations to respect foreign key constraints - if (userIds.Count > 0) - { - var users = databaseContext.Users.Where(u => userIds.Contains(u.Id)); - await userRepository.DeleteManyAsync(users); - } - - if (organizationIds.Count > 0) - { - var organizations = databaseContext.Organizations.Where(o => organizationIds.Contains(o.Id)); - var aggregateException = new AggregateException(); - foreach (var org in organizations) - { - try - { - await organizationRepository.DeleteAsync(org); - } - catch (Exception ex) - { - aggregateException = new AggregateException(aggregateException, ex); - } - } - if (aggregateException.InnerExceptions.Count > 0) - { - throw new SceneExecutionException( - $"One or more errors occurred while deleting organizations for seed ID {playId}", - aggregateException); - } - } - - logger.LogInformation("Successfully destroyed seeded data with ID {PlayId}", - playId); - - return new { PlayId = playId }; - } - - public async Task DestroyScenesAsync(IEnumerable playIds) - { - var exceptions = new List(); - - var deleteTasks = playIds.Select(async playId => - { - try - { - await DestroySceneAsync(playId); - } - catch (Exception ex) - { - lock (exceptions) - { - exceptions.Add(ex); - } - logger.LogError(ex, "Error deleting seeded data: {PlayId}", playId); - } - }); - - await Task.WhenAll(deleteTasks); - - if (exceptions.Count > 0) - { - throw new AggregateException("One or more errors occurred while deleting seeded data", exceptions); - } - } - - private async Task> ExecuteSceneMethodAsync(string templateName, JsonElement? arguments, string methodName) - { - try - { - var scene = serviceProvider.GetKeyedService(templateName) - ?? throw new SceneNotFoundException(templateName); - - var requestType = scene.GetRequestType(); - - // Deserialize the arguments into the request model - object? requestModel; - if (arguments == null) - { - // Try to create an instance with default values - try - { - requestModel = Activator.CreateInstance(requestType); - if (requestModel == null) - { - throw new SceneExecutionException( - $"Arguments are required for scene '{templateName}'"); - } - } - catch - { - throw new SceneExecutionException( - $"Arguments are required for scene '{templateName}'"); - } - } - else - { - try - { - requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions); - if (requestModel == null) - { - throw new SceneExecutionException( - $"Failed to deserialize request model for scene '{templateName}'"); - } - } - catch (JsonException ex) - { - throw new SceneExecutionException( - $"Failed to deserialize request model for scene '{templateName}': {ex.Message}", ex); - } - } - - var result = await scene.SeedAsync(requestModel); - - logger.LogInformation("Successfully executed {MethodName} on scene: {TemplateName}", methodName, templateName); - return result; - } - catch (Exception ex) when (ex is not SceneNotFoundException and not SceneExecutionException) - { - logger.LogError(ex, "Unexpected error executing {MethodName} on scene: {TemplateName}", methodName, templateName); - throw new SceneExecutionException( - $"An unexpected error occurred while executing {methodName} on scene '{templateName}'", - ex.InnerException ?? ex); - } - } -} diff --git a/util/SeederApi/Startup.cs b/util/SeederApi/Startup.cs index f27b3a006a..2b4343464f 100644 --- a/util/SeederApi/Startup.cs +++ b/util/SeederApi/Startup.cs @@ -3,7 +3,6 @@ using Bit.Core.Settings; using Bit.Seeder; using Bit.Seeder.Factories; using Bit.SeederApi.Extensions; -using Bit.SeederApi.Services; using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -39,8 +38,9 @@ public class Startup services.AddSingleton(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + + services.AddSeederApiServices(); + services.AddScoped(_ => new MangleId()); services.AddScenes(); services.AddQueries();