From 1e90dbc4890e2f2e1ef85c7a78d1f2ba747a1281 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:37:39 +0000 Subject: [PATCH 1/4] Initial plan From 7100be3b6b581d35cf39d0e2de6b1460e2018ef6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:43:14 +0000 Subject: [PATCH 2/4] Add robust error handling and caching for reflection-based method invocation Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> --- .../ScissorHandsApplication.cs | 74 ++++++++++++++++--- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/src/ScissorHands.Web/ScissorHandsApplication.cs b/src/ScissorHands.Web/ScissorHandsApplication.cs index 9a53d52..4c3ca6e 100644 --- a/src/ScissorHands.Web/ScissorHandsApplication.cs +++ b/src/ScissorHands.Web/ScissorHandsApplication.cs @@ -1,3 +1,5 @@ +using System.Reflection; + using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -42,6 +44,7 @@ public sealed class ScissorHandsApplication : IScissorHandsApplication private ILogger? _logger; private SiteManifest? _site; private IStaticSiteGenerator? _generator; + private MethodInfo? _cachedBuildMethod; internal ScissorHandsApplication(WebApplication app, IEnumerable args, Type mainLayout, Type indexView, Type postView, Type pageView, Type notFoundView) { @@ -211,17 +214,70 @@ 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) + { + _cachedBuildMethod = GetBuildAsyncMethod(); + } + + var closedMethod = _cachedBuildMethod.MakeGenericMethod(_mainLayout, _indexView, _postView, _pageView, _notFoundView); + var task = (Task?)closedMethod.Invoke(_generator, [destination, preview, cancellationToken]); + + 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."); + 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); + } + } - var closedMethod = buildMethod.MakeGenericMethod(_mainLayout, _indexView, _postView, _pageView, _notFoundView); - var task = (Task?)closedMethod.Invoke(_generator, [ destination, preview, cancellationToken ]); + 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."); + } + + // Filter to the expected signature: 5 generic parameters, 3 method parameters + var matchingMethods = methods.Where(m => m.GetGenericArguments().Length == 5 && + m.GetParameters().Length == 3) + .ToList(); + + if (matchingMethods.Count == 0) + { + throw new InvalidOperationException($"No BuildAsync method with the expected signature (5 generic parameters, 3 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() From 4bba2594860b81efefae4790eef77ee217d44fb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:46:22 +0000 Subject: [PATCH 3/4] Add thread-safety and improve exception handling Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> --- src/ScissorHands.Web/ScissorHandsApplication.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/ScissorHands.Web/ScissorHandsApplication.cs b/src/ScissorHands.Web/ScissorHandsApplication.cs index 4c3ca6e..8e5fbf1 100644 --- a/src/ScissorHands.Web/ScissorHandsApplication.cs +++ b/src/ScissorHands.Web/ScissorHandsApplication.cs @@ -45,6 +45,7 @@ public sealed class ScissorHandsApplication : IScissorHandsApplication 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) { @@ -219,11 +220,17 @@ private Task BuildSiteAsync(string destination, bool preview, CancellationToken // Use cached method info if available, otherwise find and cache it if (_cachedBuildMethod == null) { - _cachedBuildMethod = GetBuildAsyncMethod(); + lock (_cacheLock) + { + if (_cachedBuildMethod == null) + { + _cachedBuildMethod = GetBuildAsyncMethod(); + } + } } var closedMethod = _cachedBuildMethod.MakeGenericMethod(_mainLayout, _indexView, _postView, _pageView, _notFoundView); - var task = (Task?)closedMethod.Invoke(_generator, [destination, preview, cancellationToken]); + var task = (Task?)closedMethod.Invoke(_generator, new object[] { destination, preview, cancellationToken }); return task ?? Task.CompletedTask; } @@ -235,6 +242,12 @@ private Task BuildSiteAsync(string destination, bool preview, CancellationToken catch (TargetInvocationException ex) { _logger?.LogError(ex.InnerException ?? ex, "BuildAsync method threw an exception during execution."); + + // Re-throw the inner exception if it exists to preserve the original exception context + if (ex.InnerException != null) + { + throw ex.InnerException; + } throw; } catch (ArgumentException ex) From b5d51fe027ebe29c4eb04ecb21ecfa7fd17bbf98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:49:01 +0000 Subject: [PATCH 4/4] Address final code review feedback - constants and stack trace preservation Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> --- src/ScissorHands.Web/ScissorHandsApplication.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ScissorHands.Web/ScissorHandsApplication.cs b/src/ScissorHands.Web/ScissorHandsApplication.cs index 8e5fbf1..9ef3cfa 100644 --- a/src/ScissorHands.Web/ScissorHandsApplication.cs +++ b/src/ScissorHands.Web/ScissorHandsApplication.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; @@ -32,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; @@ -230,7 +233,8 @@ private Task BuildSiteAsync(string destination, bool preview, CancellationToken } var closedMethod = _cachedBuildMethod.MakeGenericMethod(_mainLayout, _indexView, _postView, _pageView, _notFoundView); - var task = (Task?)closedMethod.Invoke(_generator, new object[] { destination, preview, cancellationToken }); + var parameters = new object[] { destination, preview, cancellationToken }; + var task = (Task?)closedMethod.Invoke(_generator, parameters); return task ?? Task.CompletedTask; } @@ -243,10 +247,10 @@ private Task BuildSiteAsync(string destination, bool preview, CancellationToken { _logger?.LogError(ex.InnerException ?? ex, "BuildAsync method threw an exception during execution."); - // Re-throw the inner exception if it exists to preserve the original exception context + // Re-throw the inner exception while preserving the stack trace if (ex.InnerException != null) { - throw ex.InnerException; + ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } throw; } @@ -276,13 +280,13 @@ private MethodInfo GetBuildAsyncMethod() } // Filter to the expected signature: 5 generic parameters, 3 method parameters - var matchingMethods = methods.Where(m => m.GetGenericArguments().Length == 5 && - m.GetParameters().Length == 3) + 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 (5 generic parameters, 3 method parameters) found on type {generatorType.Name}."); + 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)