Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 82 additions & 9 deletions src/ScissorHands.Web/ScissorHandsApplication.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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<string> args, Type mainLayout, Type indexView, Type postView, Type pageView, Type notFoundView)
{
Expand Down Expand Up @@ -211,17 +218,83 @@ private async Task<IScissorHandsApplication> 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()
Expand Down