diff --git a/src/Samples/AspNetCore/Startup.cs b/src/Samples/AspNetCore/Startup.cs
index 4c569954b1..097957df9d 100644
--- a/src/Samples/AspNetCore/Startup.cs
+++ b/src/Samples/AspNetCore/Startup.cs
@@ -94,6 +94,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
}
private string GetApplicationPath(IWebHostEnvironment env)
- => Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common");
+ {
+ var common = Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common");
+ if (Directory.Exists(common))
+ {
+ return common;
+ }
+ if (File.Exists(Path.Combine(env.ContentRootPath, "Views/Default.dothtml")))
+ {
+ return env.ContentRootPath;
+ }
+ throw new DirectoryNotFoundException("Cannot find the 'Common' directory nor the 'Views' directory in the application root.");
+ }
}
}
diff --git a/src/Samples/AspNetCoreLatest/Startup.cs b/src/Samples/AspNetCoreLatest/Startup.cs
index cb24824f6f..4342847d57 100644
--- a/src/Samples/AspNetCoreLatest/Startup.cs
+++ b/src/Samples/AspNetCoreLatest/Startup.cs
@@ -105,6 +105,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
}
private string GetApplicationPath(IWebHostEnvironment env)
- => Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common");
+ {
+ if (File.Exists(Path.Combine(env.ContentRootPath, "Views/Default.dothtml")))
+ {
+ return env.ContentRootPath;
+ }
+ var common = Path.Combine(Path.GetDirectoryName(env.ContentRootPath), "Common");
+ if (File.Exists(Path.Combine(common, "Views/Default.dothtml")))
+ {
+ return common;
+ }
+ throw new DirectoryNotFoundException("Cannot find the 'Common' directory nor the 'Views' directory in the application root.");
+ }
}
}
diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj
index 216f55da77..41bcf2a7ef 100644
--- a/src/Samples/Common/DotVVM.Samples.Common.csproj
+++ b/src/Samples/Common/DotVVM.Samples.Common.csproj
@@ -19,167 +19,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/Samples/Common/DotvvmStartup.cs b/src/Samples/Common/DotvvmStartup.cs
index fc32374ecc..9172d44334 100644
--- a/src/Samples/Common/DotvvmStartup.cs
+++ b/src/Samples/Common/DotvvmStartup.cs
@@ -31,6 +31,7 @@
using DotVVM.Samples.Common.ViewModels.FeatureSamples.BindingVariables;
using DotVVM.Samples.Common.Views.ControlSamples.TemplateHost;
using DotVVM.Framework.ResourceManagement;
+using DotVVM.Samples.Common.Presenters;
using DotVVM.Samples.Common.ViewModels.FeatureSamples.CustomPrimitiveTypes;
namespace DotVVM.Samples.BasicSamples
@@ -251,6 +252,8 @@ private static void AddRoutes(DotvvmConfiguration config)
config.RouteTable.Add("FeatureSamples_PostBack_PostBackHandlers_Localized", "FeatureSamples/PostBack/PostBackHandlers_Localized", "Views/FeatureSamples/PostBack/ConfirmPostBackHandler.dothtml", presenterFactory: LocalizablePresenter.BasedOnQuery("lang"));
config.RouteTable.Add("Errors_UndefinedRouteLinkParameters-PageDetail", "Erros/UndefinedRouteLinkParameters/{Id}", "Views/Errors/UndefinedRouteLinkParameters.dothtml", new { Id = 0 });
+
+ config.RouteTable.Add("DumpExtensionsMethods", "dump-extension-methods", _ => new DumpExtensionMethodsPresenter());
}
private static void AddControls(DotvvmConfiguration config)
diff --git a/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs
new file mode 100644
index 0000000000..6b418fb287
--- /dev/null
+++ b/src/Samples/Common/Presenters/DumpExtensionMethodsPresenter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using DotVVM.Framework.Compilation;
+using DotVVM.Framework.Hosting;
+using DotVVM.Framework.Utils;
+using Microsoft.Extensions.DependencyInjection;
+using Newtonsoft.Json;
+
+namespace DotVVM.Samples.Common.Presenters
+{
+ public class DumpExtensionMethodsPresenter : IDotvvmPresenter
+ {
+ public async Task ProcessRequest(IDotvvmRequestContext context)
+ {
+ var cache = context.Configuration.ServiceProvider.GetService();
+
+ var contents = typeof(ExtensionMethodsCache)
+ .GetField("methodsCache", BindingFlags.Instance | BindingFlags.NonPublic)!
+ .GetValue(cache) as ConcurrentDictionary>;
+
+ var dump = contents.SelectMany(p => p.Value.Select(m => new {
+ Namespace = p.Key,
+ m.Name,
+ m.DeclaringType!.FullName,
+ Params = m.GetParameters().Select(p => new {
+ p.Name,
+ Type = p.ParameterType!.FullName
+ }),
+ m.IsGenericMethodDefinition,
+ GenericParameters = m.IsGenericMethodDefinition ? m.GetGenericArguments().Select(a => new {
+ a.Name
+ }) : null
+ }))
+ .OrderBy(m => m.Namespace).ThenBy(m => m.Name);
+
+ await context.HttpContext.Response.WriteAsync("ExtensionMethodsCache dump: " + JsonConvert.SerializeObject(dump, Formatting.Indented));
+ }
+ }
+}
diff --git a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs
index 164bf1fa71..3f9807656a 100644
--- a/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs
+++ b/src/Samples/Tests/Tests/Feature/StaticCommandTests.cs
@@ -1,6 +1,10 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Net;
+using System.Net.Http;
using System.Threading;
+using System.Threading.Tasks;
using DotVVM.Samples.Tests.Base;
using DotVVM.Testing.Abstractions;
using OpenQA.Selenium;
@@ -814,7 +818,7 @@ public void Feature_List_Translation_Remove_Range()
Assert.Equal(new List { "1", "2", "8", "9", "10" }, column);
});
}
-
+
[Fact]
public void Feature_List_Translation_Remove_Reverse()
{
@@ -828,6 +832,27 @@ public void Feature_List_Translation_Remove_Reverse()
});
}
+ [Fact]
+ public async Task Feature_ExtensionMethodsNotResolvedOnStartup()
+ {
+ var client = new HttpClient();
+
+ // try to visit the page
+ var pageResponse = await client.GetAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/" + SamplesRouteUrls.FeatureSamples_JavascriptTranslation_ListMethodTranslations);
+ TestOutput.WriteLine($"Page response: {(int)pageResponse.StatusCode}");
+ var wasError = pageResponse.StatusCode != HttpStatusCode.OK;
+
+ // dump extension methods on the output
+ var json = await client.GetStringAsync(TestSuiteRunner.Configuration.BaseUrls[0].TrimEnd('/') + "/dump-extension-methods");
+ TestOutput.WriteLine(json);
+
+ if (wasError)
+ {
+ // fail the test on error
+ throw new Exception("Extension methods were not resolved on application startup.");
+ }
+ }
+
protected IElementWrapperCollection GetSortedRow(IBrowserWrapper browser, string btn)
{
var orderByBtn = browser.First($"//input[@value='{btn}']", By.XPath);
diff --git a/src/Tools/AppStartupInstabilityTester.py b/src/Tools/AppStartupInstabilityTester.py
new file mode 100644
index 0000000000..80e6f77f32
--- /dev/null
+++ b/src/Tools/AppStartupInstabilityTester.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+
+import subprocess, requests, os, time, argparse
+
+parser = argparse.ArgumentParser(description="Repeatedly starts the server and every time checks if some pages are working, use to find startup-time race condition bugs")
+parser.add_argument("--port", type=int, default=16017, help="Port to run the server on")
+parser.add_argument("--working-directory", type=str, default=".", help="Working directory to run the server in")
+parser.add_argument("--server-path", type=str, default="bin/Debug/net8.0/DotVVM.Samples.BasicSamples.AspNetCoreLatest", help="Path to the server executable")
+parser.add_argument("--environment", type=str, default="Development", help="Asp.Net Core environment (Development, Production)")
+args = parser.parse_args()
+
+port = args.port
+
+def server_start() -> subprocess.Popen:
+ """Starts the server and returns the process object"""
+ server = subprocess.Popen([
+ args.server_path, "--environment", args.environment, "--urls", f"http://localhost:{port}"],
+ cwd=args.working_directory,
+ )
+ return server
+
+def req(path):
+ try:
+ response = requests.get(f"http://localhost:{port}{path}")
+ return response.status_code
+ except requests.exceptions.ConnectionError:
+ return None
+
+iteration = 0
+while True:
+ iteration += 1
+ print(f"Starting iteration {iteration}")
+ server = server_start()
+ time.sleep(0.1)
+ while req("/") is None:
+ time.sleep(0.1)
+
+ probes = [
+ req("/"),
+ req("/FeatureSamples/LambdaExpressions/StaticCommands"),
+ req("/FeatureSamples/LambdaExpressions/ClientSideFiltering"),
+ req("/FeatureSamples/LambdaExpressions/LambdaExpressions")
+ ]
+ if set(probes) != {200}:
+ print(f"Iteration {iteration} failed: {probes}")
+ time.sleep(100000000)
+
+ server.terminate()
+ server.wait()