mirror of
https://github.com/bitwarden/server
synced 2025-12-31 23:53:17 +00:00
Refactor to CQRS
This commit is contained in:
36
util/SeederApi/Commands/DestroyBatchScenesCommand.cs
Normal file
36
util/SeederApi/Commands/DestroyBatchScenesCommand.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Bit.SeederApi.Commands.Interfaces;
|
||||
|
||||
namespace Bit.SeederApi.Commands;
|
||||
|
||||
public class DestroyBatchScenesCommand(
|
||||
ILogger<DestroyBatchScenesCommand> logger,
|
||||
IDestroySceneCommand destroySceneCommand) : IDestroyBatchScenesCommand
|
||||
{
|
||||
public async Task DestroyAsync(IEnumerable<string> playIds)
|
||||
{
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
util/SeederApi/Commands/DestroySceneCommand.cs
Normal file
57
util/SeederApi/Commands/DestroySceneCommand.cs
Normal file
@@ -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<DestroySceneCommand> logger,
|
||||
IUserRepository userRepository,
|
||||
IPlayDataRepository playDataRepository,
|
||||
IOrganizationRepository organizationRepository) : IDestroySceneCommand
|
||||
{
|
||||
public async Task<object?> 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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Bit.SeederApi.Commands.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command for destroying multiple scenes in parallel.
|
||||
/// </summary>
|
||||
public interface IDestroyBatchScenesCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Destroys multiple scenes by their play IDs in parallel.
|
||||
/// </summary>
|
||||
/// <param name="playIds">The list of play IDs to destroy</param>
|
||||
/// <exception cref="AggregateException">Thrown when one or more scenes fail to destroy</exception>
|
||||
Task DestroyAsync(IEnumerable<string> playIds);
|
||||
}
|
||||
15
util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs
Normal file
15
util/SeederApi/Commands/Interfaces/IDestroySceneCommand.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.SeederApi.Commands.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command for destroying data created by a single scene.
|
||||
/// </summary>
|
||||
public interface IDestroySceneCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Destroys data created by a scene using the seeded data ID.
|
||||
/// </summary>
|
||||
/// <param name="playId">The ID of the seeded data to destroy</param>
|
||||
/// <returns>The result of the destroy operation</returns>
|
||||
/// <exception cref="Services.SceneExecutionException">Thrown when there's an error destroying the seeded data</exception>
|
||||
Task<object?> DestroyAsync(string playId);
|
||||
}
|
||||
@@ -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<QueryController> logger, IQueryService queryService) : Controller
|
||||
public class QueryController(ILogger<QueryController> logger, IQueryExecutor queryExecutor) : Controller
|
||||
{
|
||||
[HttpPost]
|
||||
public IActionResult Query([FromBody] QueryRequestModel request)
|
||||
@@ -14,7 +15,7 @@ public class QueryController(ILogger<QueryController> logger, IQueryService quer
|
||||
|
||||
try
|
||||
{
|
||||
var result = queryService.ExecuteQuery(request.Template, request.Arguments);
|
||||
var result = queryExecutor.Execute(request.Template, request.Arguments);
|
||||
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
@@ -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<SeedController> logger, ISceneService sceneService) : Controller
|
||||
public class SeedController(
|
||||
ILogger<SeedController> logger,
|
||||
ISceneExecutor sceneExecutor,
|
||||
IDestroySceneCommand destroySceneCommand,
|
||||
IDestroyBatchScenesCommand destroyBatchScenesCommand,
|
||||
IGetAllPlayIdsQuery getAllPlayIdsQuery) : Controller
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> SeedAsync([FromBody] SeedRequestModel request)
|
||||
@@ -14,7 +22,7 @@ public class SeedController(ILogger<SeedController> 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<SeedController> 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<SeedController> 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<SeedController> 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)
|
||||
|
||||
22
util/SeederApi/Execution/IQueryExecutor.cs
Normal file
22
util/SeederApi/Execution/IQueryExecutor.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bit.SeederApi.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// Executor for dynamically resolving and executing queries by name.
|
||||
/// This is an infrastructure component that orchestrates query execution,
|
||||
/// not a domain-level query.
|
||||
/// </summary>
|
||||
public interface IQueryExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a query with the given query name and arguments.
|
||||
/// Queries are read-only and do not track entities or create seed IDs.
|
||||
/// </summary>
|
||||
/// <param name="queryName">The name of the query (e.g., "EmergencyAccessInviteQuery")</param>
|
||||
/// <param name="arguments">Optional JSON arguments to pass to the query's Execute method</param>
|
||||
/// <returns>The result of the query execution</returns>
|
||||
/// <exception cref="Services.QueryNotFoundException">Thrown when the query is not found</exception>
|
||||
/// <exception cref="Services.QueryExecutionException">Thrown when there's an error executing the query</exception>
|
||||
object Execute(string queryName, JsonElement? arguments);
|
||||
}
|
||||
22
util/SeederApi/Execution/ISceneExecutor.cs
Normal file
22
util/SeederApi/Execution/ISceneExecutor.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Text.Json;
|
||||
using Bit.SeederApi.Models.Response;
|
||||
|
||||
namespace Bit.SeederApi.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// Executor for dynamically resolving and executing scenes by template name.
|
||||
/// This is an infrastructure component that orchestrates scene execution,
|
||||
/// not a domain-level command.
|
||||
/// </summary>
|
||||
public interface ISceneExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a scene with the given template name and arguments.
|
||||
/// </summary>
|
||||
/// <param name="templateName">The name of the scene template (e.g., "SingleUserScene")</param>
|
||||
/// <param name="arguments">Optional JSON arguments to pass to the scene's Seed method</param>
|
||||
/// <returns>A scene response model containing the result and mangle map</returns>
|
||||
/// <exception cref="Services.SceneNotFoundException">Thrown when the scene template is not found</exception>
|
||||
/// <exception cref="Services.SceneExecutionException">Thrown when there's an error executing the scene</exception>
|
||||
Task<SceneResponseModel> ExecuteAsync(string templateName, JsonElement? arguments);
|
||||
}
|
||||
19
util/SeederApi/Execution/JsonConfiguration.cs
Normal file
19
util/SeederApi/Execution/JsonConfiguration.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bit.SeederApi.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// Provides shared JSON serialization configuration for executors.
|
||||
/// </summary>
|
||||
internal static class JsonConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard JSON serializer options used for deserializing scene and query request models.
|
||||
/// Uses case-insensitive property matching and camelCase naming policy.
|
||||
/// </summary>
|
||||
internal static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
}
|
||||
@@ -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<QueryService> logger,
|
||||
IServiceProvider serviceProvider)
|
||||
: IQueryService
|
||||
public class QueryExecutor(
|
||||
ILogger<QueryExecutor> 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(
|
||||
78
util/SeederApi/Execution/SceneExecutor.cs
Normal file
78
util/SeederApi/Execution/SceneExecutor.cs
Normal file
@@ -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<SceneExecutor> logger,
|
||||
IServiceProvider serviceProvider) : ISceneExecutor
|
||||
{
|
||||
|
||||
public async Task<SceneResponseModel> ExecuteAsync(string templateName, JsonElement? arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scene = serviceProvider.GetKeyedService<IScene>(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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers SeederApi executors, commands, and queries.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSeederApiServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISceneExecutor, SceneExecutor>();
|
||||
services.AddScoped<IQueryExecutor, QueryExecutor>();
|
||||
|
||||
services.AddScoped<IDestroySceneCommand, DestroySceneCommand>();
|
||||
services.AddScoped<IDestroyBatchScenesCommand, DestroyBatchScenesCommand>();
|
||||
|
||||
services.AddScoped<IGetAllPlayIdsQuery, GetAllPlayIdsQuery>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
|
||||
15
util/SeederApi/Queries/GetAllPlayIdsQuery.cs
Normal file
15
util/SeederApi/Queries/GetAllPlayIdsQuery.cs
Normal file
@@ -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<string> GetAllPlayIds()
|
||||
{
|
||||
return databaseContext.PlayData
|
||||
.Select(pd => pd.PlayId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
13
util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs
Normal file
13
util/SeederApi/Queries/Interfaces/IGetAllPlayIdsQuery.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Bit.SeederApi.Queries.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Query for retrieving all play IDs for currently tracked seeded data.
|
||||
/// </summary>
|
||||
public interface IGetAllPlayIdsQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves all play IDs for currently tracked seeded data.
|
||||
/// </summary>
|
||||
/// <returns>A list of play IDs representing active seeded data that can be destroyed.</returns>
|
||||
List<string> GetAllPlayIds();
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bit.SeederApi.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for executing query operations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public interface IQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a query with the given query name and arguments.
|
||||
/// Queries are read-only and do not track entities or create seed IDs.
|
||||
/// </summary>
|
||||
/// <param name="queryName">The name of the query (e.g., "EmergencyAccessInviteQuery")</param>
|
||||
/// <param name="arguments">Optional JSON arguments to pass to the query's Execute method</param>
|
||||
/// <returns>The result of the query execution</returns>
|
||||
/// <exception cref="SceneNotFoundException">Thrown when the query is not found</exception>
|
||||
/// <exception cref="SceneExecutionException">Thrown when there's an error executing the query</exception>
|
||||
object ExecuteQuery(string queryName, JsonElement? arguments);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Bit.SeederApi.Models.Response;
|
||||
|
||||
namespace Bit.SeederApi.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for executing and managing scene operations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public interface ISceneService
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes a scene with the given template name and arguments.
|
||||
/// </summary>
|
||||
/// <param name="templateName">The name of the scene template (e.g., "SingleUserScene")</param>
|
||||
/// <param name="arguments">Optional JSON arguments to pass to the scene's Seed method</param>
|
||||
/// <returns>A tuple containing the result and optional seed ID for tracked entities</returns>
|
||||
/// <exception cref="SceneNotFoundException">Thrown when the scene template is not found</exception>
|
||||
/// <exception cref="SceneExecutionException">Thrown when there's an error executing the scene</exception>
|
||||
Task<SceneResponseModel> ExecuteSceneAsync(string templateName, JsonElement? arguments);
|
||||
|
||||
/// <summary>
|
||||
/// Destroys data created by a scene using the seeded data ID.
|
||||
/// </summary>
|
||||
/// <param name="playId">The ID of the seeded data to destroy</param>
|
||||
/// <returns>The result of the destroy operation</returns>
|
||||
/// <exception cref="SceneExecutionException">Thrown when there's an error destroying the seeded data</exception>
|
||||
Task<object?> DestroySceneAsync(string playId);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves all play IDs for currently tracked seeded data.
|
||||
/// </summary>
|
||||
/// <returns>A list of play IDs representing active seeded data that can be destroyed.</returns>
|
||||
List<string> GetAllPlayIds();
|
||||
|
||||
/// <summary>
|
||||
/// Destroys multiple scenes by their play IDs.
|
||||
/// </summary>
|
||||
/// <param name="playIds">The list of play IDs to destroy</param>
|
||||
/// <exception cref="AggregateException">Thrown when one or more scenes fail to destroy</exception>
|
||||
Task DestroyScenesAsync(IEnumerable<string> playIds);
|
||||
}
|
||||
@@ -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<SceneService> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
IUserRepository userRepository,
|
||||
IPlayDataRepository playDataRepository,
|
||||
IOrganizationRepository organizationRepository)
|
||||
: ISceneService
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public List<string> GetAllPlayIds()
|
||||
{
|
||||
return databaseContext.PlayData
|
||||
.Select(pd => pd.PlayId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<SceneResponseModel> ExecuteSceneAsync(string templateName, JsonElement? arguments)
|
||||
{
|
||||
var result = await ExecuteSceneMethodAsync(templateName, arguments, "Seed");
|
||||
|
||||
return SceneResponseModel.FromSceneResult(result);
|
||||
}
|
||||
|
||||
public async Task<object?> 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<string> playIds)
|
||||
{
|
||||
var exceptions = new List<Exception>();
|
||||
|
||||
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<SceneResult<object?>> ExecuteSceneMethodAsync(string templateName, JsonElement? arguments, string methodName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scene = serviceProvider.GetKeyedService<IScene>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<RustSDK.RustSdkService>();
|
||||
services.AddScoped<UserSeeder>();
|
||||
services.AddScoped<ISceneService, SceneService>();
|
||||
services.AddScoped<IQueryService, QueryService>();
|
||||
|
||||
services.AddSeederApiServices();
|
||||
|
||||
services.AddScoped<MangleId>(_ => new MangleId());
|
||||
services.AddScenes();
|
||||
services.AddQueries();
|
||||
|
||||
Reference in New Issue
Block a user