From 1b44fcef0dbd08e47ab9857092a0e2fdfb540e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Fri, 26 Jan 2024 11:45:50 +0100 Subject: [PATCH 1/2] compilation page: fix controls with IDotvvmRequestContext injection --- .../DotvvmViewCompilationService.cs | 7 +- .../Testing/TestDotvvmRequestContext.cs | 10 ++ .../ViewCompilationFakeRequestContext.cs | 11 ++ .../Runtime/ViewCompilationServiceTests.cs | 141 ++++++++++++++++++ 4 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs create mode 100644 src/Tests/Runtime/ViewCompilationServiceTests.cs diff --git a/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs b/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs index a88597dbe0..c62eb4d96a 100644 --- a/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs +++ b/src/Framework/Framework/Compilation/DotvvmViewCompilationService.cs @@ -10,6 +10,8 @@ using DotVVM.Framework.Controls.Infrastructure; using Microsoft.Extensions.DependencyInjection; using DotVVM.Framework.Utils; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Testing; namespace DotVVM.Framework.Compilation { @@ -160,8 +162,9 @@ public bool BuildView(DotHtmlFileInfo file, bool forceRecompile, out DotHtmlFile var pageBuilder = controlBuilderFactory.GetControlBuilder(file.VirtualPath); - using var scopedServiceProvider = dotvvmConfiguration.ServiceProvider.CreateScope(); // dependencies that are configured as scoped cannot be resolved from root service provider - var compiledControl = pageBuilder.builder.Value.BuildControl(controlBuilderFactory, scopedServiceProvider.ServiceProvider); + using var scopedServices = dotvvmConfiguration.ServiceProvider.CreateScope(); // dependencies that are configured as scoped cannot be resolved from root service provider + scopedServices.ServiceProvider.GetRequiredService().Context = new ViewCompilationFakeRequestContext(scopedServices.ServiceProvider); + var compiledControl = pageBuilder.builder.Value.BuildControl(controlBuilderFactory, scopedServices.ServiceProvider); if (pageBuilder.descriptor.MasterPage is { FileName: {} masterPagePath }) { diff --git a/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs b/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs index 7cea6e316c..1ad27706e5 100644 --- a/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs +++ b/src/Framework/Framework/Testing/TestDotvvmRequestContext.cs @@ -11,6 +11,7 @@ using DotVVM.Framework.ResourceManagement; using DotVVM.Framework.Routing; using DotVVM.Framework.Runtime.Tracing; +using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json.Linq; namespace DotVVM.Framework.Testing @@ -53,5 +54,14 @@ public IServiceProvider Services } public CustomResponsePropertiesManager CustomResponseProperties { get; } = new CustomResponsePropertiesManager(); + + + public TestDotvvmRequestContext() { } + public TestDotvvmRequestContext(IServiceProvider services) + { + this.Services = services; + this.Configuration = services.GetService(); + this.ResourceManager = services.GetService(); + } } } diff --git a/src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs b/src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs new file mode 100644 index 0000000000..05b815a23d --- /dev/null +++ b/src/Framework/Framework/Testing/ViewCompilationFakeRequestContext.cs @@ -0,0 +1,11 @@ +using System; + +namespace DotVVM.Framework.Testing +{ + class ViewCompilationFakeRequestContext : TestDotvvmRequestContext + { + public ViewCompilationFakeRequestContext(IServiceProvider services): base(services) + { + } + } +} diff --git a/src/Tests/Runtime/ViewCompilationServiceTests.cs b/src/Tests/Runtime/ViewCompilationServiceTests.cs new file mode 100644 index 0000000000..2f52f5b9d4 --- /dev/null +++ b/src/Tests/Runtime/ViewCompilationServiceTests.cs @@ -0,0 +1,141 @@ +using System; +using System.Linq; +using DotVVM.Framework.Compilation; +using DotVVM.Framework.Configuration; +using DotVVM.Framework.Controls; +using DotVVM.Framework.Hosting; +using DotVVM.Framework.Testing; +using Microsoft.AspNetCore.Authorization.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DotVVM.Framework.Tests.Runtime +{ + [TestClass] + public class ViewCompilationServiceTests + { + static readonly FakeMarkupFileLoader fileLoader; + static readonly DotvvmConfiguration config; + static readonly DotvvmViewCompilationService service; + static ViewCompilationServiceTests() + { + fileLoader = new FakeMarkupFileLoader(); + config = DotvvmTestHelper.CreateConfiguration(s => { + s.AddSingleton(fileLoader); + }); + config.Markup.AddCodeControls("test", exampleControl: typeof(ControlWithContextInjection)); + + config.RouteTable.Add("WithContextInjection", "WithContextInjection", "WithContextInjection.dothtml", null); + fileLoader.MarkupFiles["WithContextInjection.dothtml"] = """ + @viewModel object + + """; + + config.RouteTable.Add("WithUnresolvableDependency", "WithUnresolvableDependency", "WithUnresolvableDependency.dothtml", null); + fileLoader.MarkupFiles["WithUnresolvableDependency.dothtml"] = """ + @viewModel object + + """; + + + config.RouteTable.Add("WithError", "WithError", "WithError.dothtml", null); + fileLoader.MarkupFiles["WithError.dothtml"] = """ + @viewModel object + + """; + + config.RouteTable.Add("WithMasterPage", "WithMasterPage", "WithMasterPage.dothtml", null); + fileLoader.MarkupFiles["WithMasterPage.dothtml"] = """ + @viewModel object + @masterPage MasterPage.dothtml + test + """; + fileLoader.MarkupFiles["MasterPage.dothtml"] = """ + @viewModel object + + + + + + """; + + config.RouteTable.Add("NonCompilable", "NonCompilable", null, presenterFactory: _ => throw null); + + config.Freeze(); + + service = (DotvvmViewCompilationService)config.ServiceProvider.GetRequiredService(); + } + [TestMethod] + public void RequestContextInjection() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithContextInjection"); + service.BuildView(route, out _); + Assert.IsNull(route.Exception); + Assert.AreEqual(CompilationState.CompletedSuccessfully, route.Status); + } + [TestMethod] + public void InjectionUnresolvableDependency() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithUnresolvableDependency"); + service.BuildView(route, out _); + Assert.AreEqual(CompilationState.CompilationFailed, route.Status); + Assert.AreEqual("Unable to resolve service for type 'DotVVM.Framework.Tests.Runtime.ControlWithUnresolvableDependency+ThisServiceIsntRegistered' while attempting to activate 'DotVVM.Framework.Tests.Runtime.ControlWithUnresolvableDependency'.", route.Exception); + Assert.IsNotNull(route.Exception); + } + [TestMethod] + public void ErrorInMarkup() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithError"); + service.BuildView(route, out _); + Assert.AreEqual(CompilationState.CompilationFailed, route.Status); + Assert.IsNotNull(route.Exception); + Assert.AreEqual("The control could not be resolved! Make sure that the tagPrefix is registered in DotvvmConfiguration.Markup.Controls collection!", route.Exception); + } + + [TestMethod] + public void MasterPage() + { + var route = service.GetRoutes().First(r => r.RouteName == "WithMasterPage"); + service.BuildView(route, out var masterPage); + Assert.IsNull(route.Exception); + Assert.AreEqual(CompilationState.CompletedSuccessfully, route.Status); + Assert.IsNotNull(masterPage); + Assert.AreEqual(masterPage, service.GetMasterPages().FirstOrDefault(m => m.VirtualPath == "MasterPage.dothtml")); + Assert.AreEqual(CompilationState.None, masterPage.Status); + service.BuildView(masterPage, out _); + Assert.AreEqual(CompilationState.CompletedSuccessfully, masterPage.Status); + } + + [TestMethod] + public void NonCompilable() + { + var route = service.GetRoutes().First(r => r.RouteName == "NonCompilable"); + Assert.AreEqual(CompilationState.NonCompilable, route.Status); + service.BuildView(route, out _); + Assert.AreEqual(CompilationState.NonCompilable, route.Status); + Assert.IsNull(route.Exception); + } + } + + public class ControlWithContextInjection: DotvvmControl + { + public ControlWithContextInjection(IDotvvmRequestContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + } + } + + public class ControlWithUnresolvableDependency: DotvvmControl + { + public ControlWithUnresolvableDependency(ThisServiceIsntRegistered dependency) + { + if (dependency == null) + throw new ArgumentNullException(nameof(dependency)); + } + + public class ThisServiceIsntRegistered + { + } + } +} From 39a13fd2e026024968c1c5daa968866256b04e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Standa=20Luke=C5=A1?= Date: Wed, 31 Jan 2024 12:18:25 +0100 Subject: [PATCH 2/2] ViewCompilationServiceTests: remove non-deterministic assert --- src/Tests/Runtime/ViewCompilationServiceTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tests/Runtime/ViewCompilationServiceTests.cs b/src/Tests/Runtime/ViewCompilationServiceTests.cs index 2f52f5b9d4..5e7a26b5b7 100644 --- a/src/Tests/Runtime/ViewCompilationServiceTests.cs +++ b/src/Tests/Runtime/ViewCompilationServiceTests.cs @@ -5,7 +5,6 @@ using DotVVM.Framework.Controls; using DotVVM.Framework.Hosting; using DotVVM.Framework.Testing; -using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -101,7 +100,7 @@ public void MasterPage() Assert.AreEqual(CompilationState.CompletedSuccessfully, route.Status); Assert.IsNotNull(masterPage); Assert.AreEqual(masterPage, service.GetMasterPages().FirstOrDefault(m => m.VirtualPath == "MasterPage.dothtml")); - Assert.AreEqual(CompilationState.None, masterPage.Status); + // Assert.AreEqual(CompilationState.None, masterPage.Status); // it's not deterministic, because the master page is built asynchronously after the view asks for its viewmodel type service.BuildView(masterPage, out _); Assert.AreEqual(CompilationState.CompletedSuccessfully, masterPage.Status); }