diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
index 0c01dad0fb..c9a3d6feaa 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading;
-using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
-using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
@@ -130,6 +127,8 @@ namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection
private static IUmbracoBuilder AddPureLiveRazorEngine(this IUmbracoBuilder builder)
{
+ // See notes in RefreshingRazorViewEngine for information on what this is doing.
+
// copy the current collection, we need to use this later to rebuild a container
// to re-create the razor compiler provider
var initialCollection = new ServiceCollection
@@ -151,116 +150,4 @@ namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection
return builder;
}
}
-
- ///
- /// Custom that wraps aspnetcore's default implementation
- ///
- ///
- /// This is used so that when new PureLive models are built, the entire razor stack is re-constructed so all razor
- /// caches and assembly references, etc... are cleared.
- ///
- internal class RefreshingRazorViewEngine : IRazorViewEngine
- {
- private IRazorViewEngine _current;
- private readonly PureLiveModelFactory _pureLiveModelFactory;
- private readonly Func _defaultRazorViewEngineFactory;
- private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// A factory method used to re-construct the default aspnetcore
- ///
- /// The
- public RefreshingRazorViewEngine(Func defaultRazorViewEngineFactory, PureLiveModelFactory pureLiveModelFactory)
- {
- _pureLiveModelFactory = pureLiveModelFactory;
- _defaultRazorViewEngineFactory = defaultRazorViewEngineFactory;
- _current = _defaultRazorViewEngineFactory();
- _pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged;
- }
-
- ///
- /// When the pure live models change, re-construct the razor stack
- ///
- private void PureLiveModelFactory_ModelsChanged(object sender, EventArgs e)
- {
- _locker.EnterWriteLock();
- try
- {
- _current = _defaultRazorViewEngineFactory();
- }
- finally
- {
- _locker.ExitWriteLock();
- }
- }
-
- public RazorPageResult FindPage(ActionContext context, string pageName)
- {
- _locker.EnterReadLock();
- try
- {
- return _current.FindPage(context, pageName);
- }
- finally
- {
- _locker.ExitReadLock();
- }
- }
-
- public string GetAbsolutePath(string executingFilePath, string pagePath)
- {
- _locker.EnterReadLock();
- try
- {
- return _current.GetAbsolutePath(executingFilePath, pagePath);
- }
- finally
- {
- _locker.ExitReadLock();
- }
- }
-
- public RazorPageResult GetPage(string executingFilePath, string pagePath)
- {
- _locker.EnterReadLock();
- try
- {
- return _current.GetPage(executingFilePath, pagePath);
- }
- finally
- {
- _locker.ExitReadLock();
- }
- }
-
- public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
- {
- _locker.EnterReadLock();
- try
- {
- return _current.FindView(context, viewName, isMainPage);
-
- }
- finally
- {
- _locker.ExitReadLock();
- }
- }
-
- public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
- {
- _locker.EnterReadLock();
- try
- {
- return _current.GetView(executingFilePath, viewPath, isMainPage);
- }
- finally
- {
- _locker.ExitReadLock();
- }
- }
- }
}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs b/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs
new file mode 100644
index 0000000000..ad82d1d7b3
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Threading;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Razor;
+using Microsoft.AspNetCore.Mvc.ViewEngines;
+
+/*
+ * OVERVIEW:
+ *
+ * The CSharpCompiler is responsible for the actual compilation of razor at runtime.
+ * It creates a CSharpCompilation instance to do the compilation. This is where DLL references
+ * are applied. However, the way this works is not flexible for dynamic assemblies since the references
+ * are only discovered and loaded once before the first compilation occurs. This is done here:
+ * https://github.com/dotnet/aspnetcore/blob/114f0f6d1ef1d777fb93d90c87ac506027c55ea0/src/Mvc/Mvc.Razor.RuntimeCompilation/src/CSharpCompiler.cs#L79
+ * The CSharpCompiler is internal and cannot be replaced or extended, however it's references come from:
+ * RazorReferenceManager. Unfortunately this is also internal and cannot be replaced, though it can be extended
+ * using MvcRazorRuntimeCompilationOptions, except this is the place where references are only loaded once which
+ * is done with a LazyInitializer. See https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs#L35.
+ *
+ * The way that RazorReferenceManager works is by resolving references from the ApplicationPartsManager - either by
+ * an application part that is specifically an ICompilationReferencesProvider or an AssemblyPart. So to fulfill this
+ * requirement, we add the MB assembly to the assembly parts manager within the PureLiveModelFactory when the assembly
+ * is (re)generated. But due to the above restrictions, when re-generating, this will have no effect since the references
+ * have already been resolved with the LazyInitializer in the RazorReferenceManager.
+ *
+ * The services that can be replaced are: IViewCompilerProvider (default is the internal RuntimeViewCompilerProvider) and
+ * IViewCompiler (default is the internal RuntimeViewCompiler). There is one specific public extension point that I was
+ * hoping would solve all of the problems which was IMetadataReferenceFeature (implemented by LazyMetadataReferenceFeature
+ * which uses RazorReferencesManager) which is a razor feature that you can add
+ * to the RazorProjectEngine. It is used to resolve roslyn references and by default is backed by RazorReferencesManager.
+ * Unfortunately, this service is not used by the CSharpCompiler, it seems to only be used by some tag helper compilations.
+ *
+ * There are caches at several levels, all of which are not publicly accessible APIs (apart from RazorViewEngine.ViewLookupCache
+ * which is possible to clear by casting and then calling cache.Compact(100); but that doesn't get us far enough).
+ *
+ * For this to work, several caches must be cleared:
+ * - RazorViewEngine.ViewLookupCache
+ * - RazorReferencesManager._compilationReferences
+ * - RazorPageActivator._activationInfo (though this one may be optional)
+ * - RuntimeViewCompiler._cache
+ *
+ * What are our options?
+ *
+ * a) We can copy a ton of code into our application: CSharpCompiler, RuntimeViewCompilerProvider, RuntimeViewCompiler and
+ * RazorReferenceManager (probably more depending on the extent of Internal references).
+ * b) We can use reflection to try to access all of the above resources and try to forcefully clear caches and reset initialization flags.
+ * c) We hack these replace-able services with our own implementations that wrap the default services. To do this
+ * requires re-resolving the original services from a pre-built DI container. In effect this re-creates these
+ * services from scratch which means there is no caches.
+ *
+ * ... Option C works, we will use that but need to verify how this affects memory since ideally the old services will be GC'd.
+ *
+ * Option C, how its done:
+ * - Before we add our custom razor services to the container, we make a copy of the services collection which is the snapshot of registered services
+ * with razor defaults before ours are added.
+ * - We replace the default implementation of IRazorViewEngine with our own. This is a wrapping service that wraps the default RazorViewEngine instance.
+ * The ctor for this service takes in a Factory method to re-construct the default RazorViewEngine and all of it's dependency graph.
+ * - When the PureLive models change, the Factory is invoked and the default razor services are all re-created, thus clearing their caches and the newly
+ * created instance is wrapped. The RazorViewEngine is the only service that needs to be replaced and wrapped for this to work because it's dependency
+ * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature.
+ */
+
+namespace Umbraco.ModelsBuilder.Embedded
+{
+ ///
+ /// Custom that wraps aspnetcore's default implementation
+ ///
+ ///
+ /// This is used so that when new PureLive models are built, the entire razor stack is re-constructed so all razor
+ /// caches and assembly references, etc... are cleared.
+ ///
+ internal class RefreshingRazorViewEngine : IRazorViewEngine
+ {
+ private IRazorViewEngine _current;
+ private readonly PureLiveModelFactory _pureLiveModelFactory;
+ private readonly Func _defaultRazorViewEngineFactory;
+ private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A factory method used to re-construct the default aspnetcore
+ ///
+ /// The
+ public RefreshingRazorViewEngine(Func defaultRazorViewEngineFactory, PureLiveModelFactory pureLiveModelFactory)
+ {
+ _pureLiveModelFactory = pureLiveModelFactory;
+ _defaultRazorViewEngineFactory = defaultRazorViewEngineFactory;
+ _current = _defaultRazorViewEngineFactory();
+ _pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged;
+ }
+
+ ///
+ /// When the pure live models change, re-construct the razor stack
+ ///
+ private void PureLiveModelFactory_ModelsChanged(object sender, EventArgs e)
+ {
+ _locker.EnterWriteLock();
+ try
+ {
+ _current = _defaultRazorViewEngineFactory();
+ }
+ finally
+ {
+ _locker.ExitWriteLock();
+ }
+ }
+
+ public RazorPageResult FindPage(ActionContext context, string pageName)
+ {
+ _locker.EnterReadLock();
+ try
+ {
+ return _current.FindPage(context, pageName);
+ }
+ finally
+ {
+ _locker.ExitReadLock();
+ }
+ }
+
+ public string GetAbsolutePath(string executingFilePath, string pagePath)
+ {
+ _locker.EnterReadLock();
+ try
+ {
+ return _current.GetAbsolutePath(executingFilePath, pagePath);
+ }
+ finally
+ {
+ _locker.ExitReadLock();
+ }
+ }
+
+ public RazorPageResult GetPage(string executingFilePath, string pagePath)
+ {
+ _locker.EnterReadLock();
+ try
+ {
+ return _current.GetPage(executingFilePath, pagePath);
+ }
+ finally
+ {
+ _locker.ExitReadLock();
+ }
+ }
+
+ public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage)
+ {
+ _locker.EnterReadLock();
+ try
+ {
+ return _current.FindView(context, viewName, isMainPage);
+
+ }
+ finally
+ {
+ _locker.ExitReadLock();
+ }
+ }
+
+ public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage)
+ {
+ _locker.EnterReadLock();
+ try
+ {
+ return _current.GetView(executingFilePath, viewPath, isMainPage);
+ }
+ finally
+ {
+ _locker.ExitReadLock();
+ }
+ }
+ }
+}