diff --git a/src/ScissorHands.Web/ScissorHandsApplication.cs b/src/ScissorHands.Web/ScissorHandsApplication.cs index 9a53d52..9ef3cfa 100644 --- a/src/ScissorHands.Web/ScissorHandsApplication.cs +++ b/src/ScissorHands.Web/ScissorHandsApplication.cs @@ -1,3 +1,6 @@ +using System.Reflection; +using System.Runtime.ExceptionServices; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -30,6 +33,8 @@ public interface IScissorHandsApplication public sealed class ScissorHandsApplication : IScissorHandsApplication { private const string APP_LOGGER_NAME = "App"; + private const int EXPECTED_GENERIC_PARAMETER_COUNT = 5; + private const int EXPECTED_METHOD_PARAMETER_COUNT = 3; private readonly string[] _args; private readonly Type _mainLayout; @@ -42,6 +47,8 @@ public sealed class ScissorHandsApplication : IScissorHandsApplication private ILogger? _logger; private SiteManifest? _site; private IStaticSiteGenerator? _generator; + private MethodInfo? _cachedBuildMethod; + private readonly object _cacheLock = new object(); internal ScissorHandsApplication(WebApplication app, IEnumerable args, Type mainLayout, Type indexView, Type postView, Type pageView, Type notFoundView) { @@ -211,17 +218,83 @@ private async Task RunBuildAsync() private Task BuildSiteAsync(string destination, bool preview, CancellationToken cancellationToken) { - var buildMethod = _generator!.GetType() - .GetMethods() - .Single(m => string.Equals(m.Name, nameof(IStaticSiteGenerator.BuildAsync), StringComparison.Ordinal) && - m.IsGenericMethodDefinition && - m.GetGenericArguments().Length == 5 && - m.GetParameters().Length == 3); + try + { + // Use cached method info if available, otherwise find and cache it + if (_cachedBuildMethod == null) + { + lock (_cacheLock) + { + if (_cachedBuildMethod == null) + { + _cachedBuildMethod = GetBuildAsyncMethod(); + } + } + } + + var closedMethod = _cachedBuildMethod.MakeGenericMethod(_mainLayout, _indexView, _postView, _pageView, _notFoundView); + var parameters = new object[] { destination, preview, cancellationToken }; + var task = (Task?)closedMethod.Invoke(_generator, parameters); + + return task ?? Task.CompletedTask; + } + catch (InvalidOperationException ex) + { + _logger?.LogError(ex, "Failed to invoke BuildAsync method. The method signature may have changed or multiple overloads exist."); + throw new InvalidOperationException("Failed to invoke the BuildAsync method on the static site generator. Please ensure the IStaticSiteGenerator interface has not changed.", ex); + } + catch (TargetInvocationException ex) + { + _logger?.LogError(ex.InnerException ?? ex, "BuildAsync method threw an exception during execution."); + + // Re-throw the inner exception while preserving the stack trace + if (ex.InnerException != null) + { + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); + } + throw; + } + catch (ArgumentException ex) + { + _logger?.LogError(ex, "Failed to create generic method with the provided type arguments."); + throw new InvalidOperationException("Failed to create the generic BuildAsync method. The provided view types may not satisfy the required constraints.", ex); + } + } + + private MethodInfo GetBuildAsyncMethod() + { + if (_generator == null) + { + throw new InvalidOperationException("Static site generator has not been initialized."); + } + + var generatorType = _generator.GetType(); + var methods = generatorType.GetMethods() + .Where(m => string.Equals(m.Name, nameof(IStaticSiteGenerator.BuildAsync), StringComparison.Ordinal) && + m.IsGenericMethodDefinition) + .ToList(); + + if (methods.Count == 0) + { + throw new InvalidOperationException($"No BuildAsync method found on type {generatorType.Name}. Ensure the generator implements IStaticSiteGenerator."); + } - var closedMethod = buildMethod.MakeGenericMethod(_mainLayout, _indexView, _postView, _pageView, _notFoundView); - var task = (Task?)closedMethod.Invoke(_generator, [ destination, preview, cancellationToken ]); + // Filter to the expected signature: 5 generic parameters, 3 method parameters + var matchingMethods = methods.Where(m => m.GetGenericArguments().Length == EXPECTED_GENERIC_PARAMETER_COUNT && + m.GetParameters().Length == EXPECTED_METHOD_PARAMETER_COUNT) + .ToList(); + + if (matchingMethods.Count == 0) + { + throw new InvalidOperationException($"No BuildAsync method with the expected signature ({EXPECTED_GENERIC_PARAMETER_COUNT} generic parameters, {EXPECTED_METHOD_PARAMETER_COUNT} method parameters) found on type {generatorType.Name}."); + } + + if (matchingMethods.Count > 1) + { + throw new InvalidOperationException($"Multiple BuildAsync methods with the expected signature found on type {generatorType.Name}. Cannot determine which method to invoke."); + } - return task ?? Task.CompletedTask; + return matchingMethods[0]; } private IScissorHandsApplication LogInvalidMode()