1
0
mirror of https://github.com/bitwarden/server synced 2025-12-15 15:53:59 +00:00

Refactor recipies into scenes

This commit is contained in:
Hinton
2025-10-17 11:47:19 -04:00
parent f6fe7a9316
commit fd41332e4c
8 changed files with 120 additions and 108 deletions

View File

@@ -39,7 +39,7 @@ public class SeedController(ILogger<SeedController> logger, IRecipeService recip
}
catch (RecipeExecutionException ex)
{
logger.LogError(ex, "Error executing recipe: {Template}", request.Template);
logger.LogError(ex, "Error executing scene: {Template}", request.Template);
return BadRequest(new
{
Error = ex.Message,

View File

@@ -0,0 +1,29 @@
using System.Reflection;
using Bit.Seeder;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Bit.SeederApi.Extensions;
public static class ServiceCollectionExtensions
{
/// <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.
/// </summary>
public static IServiceCollection AddScenes(this IServiceCollection services)
{
var seederAssembly = Assembly.Load("Seeder");
var sceneTypes = seederAssembly.GetTypes()
.Where(t => t is { IsClass: true, IsAbstract: false } &&
t.GetInterfaces().Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition().Name == "IScene`1"));
foreach (var sceneType in sceneTypes)
{
services.TryAddScoped(sceneType);
services.TryAddKeyedScoped(typeof(IScene), sceneType.Name, (sp, key) => sp.GetRequiredService(sceneType));
}
return services;
}
}

View File

@@ -1,4 +1,5 @@
using Bit.Seeder;
using Bit.SeederApi.Extensions;
using Bit.SeederApi.Services;
using Bit.SharedWeb.Utilities;
@@ -20,6 +21,7 @@ builder.Services.AddSingleton<Bit.RustSDK.RustSdkService>();
builder.Services.AddScoped<Bit.Seeder.Factories.UserSeeder>();
builder.Services.AddScoped<IRecipeService, RecipeService>();
builder.Services.AddScoped<MangleId>(_ => new MangleId());
builder.Services.AddScenes();
var app = builder.Build();

View File

@@ -6,17 +6,17 @@ namespace Bit.SeederApi.Services;
public interface IRecipeService
{
/// <summary>
/// Executes a recipe with the given template name and arguments.
/// Executes a scene with the given template name and arguments.
/// </summary>
/// <param name="templateName">The name of the recipe template (e.g., "OrganizationWithUsersRecipe")</param>
/// <param name="arguments">Optional JSON arguments to pass to the recipe's Seed method</param>
/// <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="RecipeNotFoundException">Thrown when the recipe template is not found</exception>
/// <exception cref="RecipeExecutionException">Thrown when there's an error executing the recipe</exception>
/// <exception cref="RecipeNotFoundException">Thrown when the scene template is not found</exception>
/// <exception cref="RecipeExecutionException">Thrown when there's an error executing the scene</exception>
(object? Result, Guid? SeedId) ExecuteRecipe(string templateName, JsonElement? arguments);
/// <summary>
/// Destroys data created by a recipe using the seeded data ID.
/// Destroys data created by a scene using the seeded data ID.
/// </summary>
/// <param name="seedId">The ID of the seeded data to destroy</param>
/// <returns>The result of the destroy operation</returns>

View File

@@ -1,5 +1,4 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories;
@@ -15,6 +14,12 @@ public class RecipeService(
IOrganizationRepository organizationRepository)
: IRecipeService
{
private static readonly JsonSerializerOptions _jsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public List<SeededData> GetAllSeededData()
{
return databaseContext.SeededData.ToList();
@@ -45,7 +50,7 @@ public class RecipeService(
databaseContext.Add(seededData);
databaseContext.SaveChanges();
logger.LogInformation("Saved seeded data with ID {SeedId} for recipe {RecipeName}",
logger.LogInformation("Saved seeded data with ID {SeedId} for scene {RecipeName}",
seededData.Id, templateName);
return (Result: recipeResult.Result, SeedId: seededData.Id);
@@ -99,118 +104,70 @@ public class RecipeService(
databaseContext.Remove(seededData);
databaseContext.SaveChanges();
logger.LogInformation("Successfully destroyed seeded data with ID {SeedId} for recipe {RecipeName}",
logger.LogInformation("Successfully destroyed seeded data with ID {SeedId} for scene {RecipeName}",
seedId, seededData.RecipeName);
return new { SeedId = seedId, RecipeName = seededData.RecipeName };
}
private object? ExecuteRecipeMethod(string templateName, JsonElement? arguments, string methodName)
private RecipeResult? ExecuteRecipeMethod(string templateName, JsonElement? arguments, string methodName)
{
try
{
var recipeType = LoadRecipeType(templateName);
var method = GetRecipeMethod(recipeType, templateName, methodName);
var recipeInstance = CreateRecipeInstance(recipeType);
var scene = serviceProvider.GetKeyedService<IScene>(templateName)
?? throw new RecipeNotFoundException(templateName);
var methodArguments = ParseMethodArguments(method, arguments);
var result = method.Invoke(recipeInstance, methodArguments);
var requestType = scene.GetRequestType();
logger.LogInformation("Successfully executed {MethodName} on recipe: {TemplateName}", methodName, templateName);
return result;
}
catch (Exception ex) when (ex is not RecipeNotFoundException and not RecipeExecutionException)
{
logger.LogError(ex, "Unexpected error executing {MethodName} on recipe: {TemplateName}", methodName, templateName);
throw new RecipeExecutionException(
$"An unexpected error occurred while executing {methodName} on recipe '{templateName}'",
ex.InnerException ?? ex);
}
}
private static Type LoadRecipeType(string templateName)
{
var recipeTypeName = $"Bit.Seeder.Recipes.{templateName}";
var recipeType = Assembly.Load("Seeder")
.GetTypes()
.FirstOrDefault(t => t.FullName == recipeTypeName);
return recipeType ?? throw new RecipeNotFoundException(templateName);
}
private static MethodInfo GetRecipeMethod(Type recipeType, string templateName, string methodName)
{
var method = recipeType.GetMethod(methodName);
return method ?? throw new RecipeExecutionException($"{methodName} method not found in recipe '{templateName}'");
}
private object CreateRecipeInstance(Type recipeType)
{
var constructors = recipeType.GetConstructors();
if (constructors.Length == 0)
{
throw new RecipeExecutionException($"No public constructors found for recipe type '{recipeType.Name}'");
}
var constructor = constructors[0];
var parameters = constructor.GetParameters();
var constructorArgs = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var service = serviceProvider.GetService(parameter.ParameterType);
if (service == null)
// Deserialize the arguments into the request model
object? requestModel;
if (arguments == null)
{
throw new RecipeExecutionException(
$"Unable to resolve service of type '{parameter.ParameterType.Name}' for recipe constructor");
// Try to create an instance with default values
try
{
requestModel = Activator.CreateInstance(requestType);
if (requestModel == null)
{
throw new RecipeExecutionException(
$"Arguments are required for scene '{templateName}'");
}
}
catch
{
throw new RecipeExecutionException(
$"Arguments are required for scene '{templateName}'");
}
}
constructorArgs[i] = service;
}
return Activator.CreateInstance(recipeType, constructorArgs)!;
}
private static object?[] ParseMethodArguments(MethodInfo seedMethod, JsonElement? arguments)
{
var parameters = seedMethod.GetParameters();
if (arguments == null && parameters.Length > 0)
{
throw new RecipeExecutionException("Arguments are required for this recipe");
}
var methodArguments = new object?[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var parameterName = parameter.Name!;
if (arguments?.TryGetProperty(parameterName, out var value) == true)
else
{
try
{
methodArguments[i] = JsonSerializer.Deserialize(value.GetRawText(), parameter.ParameterType);
requestModel = JsonSerializer.Deserialize(arguments.Value.GetRawText(), requestType, _jsonOptions);
if (requestModel == null)
{
throw new RecipeExecutionException(
$"Failed to deserialize request model for scene '{templateName}'");
}
}
catch (JsonException ex)
{
throw new RecipeExecutionException(
$"Failed to deserialize parameter '{parameterName}': {ex.Message}", ex);
$"Failed to deserialize request model for scene '{templateName}': {ex.Message}", ex);
}
}
else if (!parameter.HasDefaultValue)
{
throw new RecipeExecutionException($"Missing required parameter: {parameterName}");
}
else
{
methodArguments[i] = parameter.DefaultValue;
}
}
return methodArguments;
var result = scene.Seed(requestModel);
logger.LogInformation("Successfully executed {MethodName} on scene: {TemplateName}", methodName, templateName);
return result;
}
catch (Exception ex) when (ex is not RecipeNotFoundException and not RecipeExecutionException)
{
logger.LogError(ex, "Unexpected error executing {MethodName} on scene: {TemplateName}", methodName, templateName);
throw new RecipeExecutionException(
$"An unexpected error occurred while executing {methodName} on scene '{templateName}'",
ex.InnerException ?? ex);
}
}
}