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

Extract logic into service

This commit is contained in:
Hinton
2025-10-07 12:35:33 -07:00
parent fa46919409
commit d0d6bfb237
5 changed files with 160 additions and 75 deletions

View File

@@ -1,7 +1,6 @@
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.SeederApi.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text.Json; using System.Text.Json;
namespace Bit.SeederApi.Controllers; namespace Bit.SeederApi.Controllers;
@@ -17,12 +16,12 @@ public class SeedRequestModel
public class SeedController : Controller public class SeedController : Controller
{ {
private readonly ILogger<SeedController> _logger; private readonly ILogger<SeedController> _logger;
private readonly DatabaseContext _databaseContext; private readonly IRecipeService _recipeService;
public SeedController(ILogger<SeedController> logger, DatabaseContext databaseContext) public SeedController(ILogger<SeedController> logger, IRecipeService recipeService)
{ {
_logger = logger; _logger = logger;
_databaseContext = databaseContext; _recipeService = recipeService;
} }
[HttpPost("/seed")] [HttpPost("/seed")]
@@ -32,87 +31,35 @@ public class SeedController : Controller
try try
{ {
// Find the recipe class var result = _recipeService.ExecuteRecipe(request.Template, request.Arguments);
var recipeTypeName = $"Bit.Seeder.Recipes.{request.Template}";
var recipeType = Assembly.Load("Seeder")
.GetTypes()
.FirstOrDefault(t => t.FullName == recipeTypeName);
if (recipeType == null)
{
return NotFound(new { Error = $"Recipe '{request.Template}' not found" });
}
// Instantiate the recipe with DatabaseContext
var recipeInstance = Activator.CreateInstance(recipeType, _databaseContext);
if (recipeInstance == null)
{
return StatusCode(500, new { Error = "Failed to instantiate recipe" });
}
// Find the Seed method
var seedMethod = recipeType.GetMethod("Seed");
if (seedMethod == null)
{
return StatusCode(500, new { Error = $"Seed method not found in recipe '{request.Template}'" });
}
// Parse arguments and match to method parameters
var parameters = seedMethod.GetParameters();
var arguments = new object?[parameters.Length];
if (request.Arguments == null && parameters.Length > 0)
{
return BadRequest(new { Error = "Arguments are required for this recipe" });
}
for (int i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var parameterName = parameter.Name!;
if (request.Arguments?.TryGetProperty(parameterName, out JsonElement value) == true)
{
try
{
arguments[i] = JsonSerializer.Deserialize(value.GetRawText(), parameter.ParameterType);
}
catch (JsonException ex)
{
return BadRequest(new
{
Error = $"Failed to deserialize parameter '{parameterName}'",
Details = ex.Message
});
}
}
else if (!parameter.HasDefaultValue)
{
return BadRequest(new { Error = $"Missing required parameter: {parameterName}" });
}
else
{
arguments[i] = parameter.DefaultValue;
}
}
// Invoke the Seed method
var result = seedMethod.Invoke(recipeInstance, arguments);
return Ok(new return Ok(new
{ {
Message = "Seed completed successfully", Message = "Seed completed successfully",
Template = request.Template, request.Template,
Result = result Result = result
}); });
} }
catch (RecipeNotFoundException ex)
{
return NotFound(new { Error = ex.Message });
}
catch (RecipeExecutionException ex)
{
_logger.LogError(ex, "Error executing recipe: {Template}", request.Template);
return BadRequest(new
{
Error = ex.Message,
Details = ex.InnerException?.Message
});
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error seeding with template: {Template}", request.Template); _logger.LogError(ex, "Unexpected error seeding with template: {Template}", request.Template);
return StatusCode(500, new return StatusCode(500, new
{ {
Error = "An error occurred while seeding", Error = "An unexpected error occurred while seeding",
Details = ex.InnerException?.Message ?? ex.Message Details = ex.Message
}); });
} }
} }

View File

@@ -1,3 +1,4 @@
using Bit.SeederApi.Services;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -14,6 +15,9 @@ builder.Services.AddCustomDataProtectionServices(builder.Environment, globalSett
// Repositories // Repositories
builder.Services.AddDatabaseRepositories(globalSettings); builder.Services.AddDatabaseRepositories(globalSettings);
// Recipe Service
builder.Services.AddScoped<IRecipeService, RecipeService>();
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.

View File

@@ -0,0 +1,16 @@
using System.Text.Json;
namespace Bit.SeederApi.Services;
public interface IRecipeService
{
/// <summary>
/// Executes a recipe 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>
/// <returns>The result returned by the recipe's Seed method</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>
object? ExecuteRecipe(string templateName, JsonElement? arguments);
}

View File

@@ -0,0 +1,13 @@
namespace Bit.SeederApi.Services;
public class RecipeNotFoundException : Exception
{
public RecipeNotFoundException(string message) : base(message) { }
}
public class RecipeExecutionException : Exception
{
public RecipeExecutionException(string message) : base(message) { }
public RecipeExecutionException(string message, Exception innerException)
: base(message, innerException) { }
}

View File

@@ -0,0 +1,105 @@
using Bit.Infrastructure.EntityFramework.Repositories;
using System.Reflection;
using System.Text.Json;
namespace Bit.SeederApi.Services;
public class RecipeService : IRecipeService
{
private readonly DatabaseContext _databaseContext;
private readonly ILogger<RecipeService> _logger;
public RecipeService(DatabaseContext databaseContext, ILogger<RecipeService> logger)
{
_databaseContext = databaseContext;
_logger = logger;
}
public object? ExecuteRecipe(string templateName, JsonElement? arguments)
{
try
{
// Find the recipe class
var recipeTypeName = $"Bit.Seeder.Recipes.{templateName}";
var recipeType = Assembly.Load("Seeder")
.GetTypes()
.FirstOrDefault(t => t.FullName == recipeTypeName);
if (recipeType == null)
{
throw new RecipeNotFoundException($"Recipe '{templateName}' not found");
}
// Instantiate the recipe with DatabaseContext
var recipeInstance = Activator.CreateInstance(recipeType, _databaseContext);
if (recipeInstance == null)
{
throw new RecipeExecutionException("Failed to instantiate recipe");
}
// Find the Seed method
var seedMethod = recipeType.GetMethod("Seed");
if (seedMethod == null)
{
throw new RecipeExecutionException($"Seed method not found in recipe '{templateName}'");
}
// Parse arguments and match to method parameters
var parameters = seedMethod.GetParameters();
var methodArguments = new object?[parameters.Length];
if (arguments == null && parameters.Length > 0)
{
throw new RecipeExecutionException("Arguments are required for this recipe");
}
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
var parameterName = parameter.Name!;
if (arguments?.TryGetProperty(parameterName, out JsonElement value) == true)
{
try
{
methodArguments[i] = JsonSerializer.Deserialize(value.GetRawText(), parameter.ParameterType);
}
catch (JsonException ex)
{
throw new RecipeExecutionException(
$"Failed to deserialize parameter '{parameterName}': {ex.Message}", ex);
}
}
else if (!parameter.HasDefaultValue)
{
throw new RecipeExecutionException($"Missing required parameter: {parameterName}");
}
else
{
methodArguments[i] = parameter.DefaultValue;
}
}
// Invoke the Seed method
var result = seedMethod.Invoke(recipeInstance, methodArguments);
_logger.LogInformation("Successfully executed recipe: {TemplateName}", templateName);
return result;
}
catch (RecipeNotFoundException)
{
throw;
}
catch (RecipeExecutionException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error executing recipe: {TemplateName}", templateName);
throw new RecipeExecutionException(
$"An unexpected error occurred while executing recipe '{templateName}'",
ex.InnerException ?? ex);
}
}
}