Models builder: Move InMemoryAuto models builder and razor runtime compilation into its own package to enable hot reload (#20187)
* Move in memory models builder out of core * Move runtime validations into backoffice development project * Obsolete ModelsMode enum * Move the InMemoryModelsbuilder/RRC novel into the Backoffice development umbraco builder extension * Add runtime validator to warn if InMemoryAuto is selected but the package isn't installed * Add backoffice development to template * Remove propertyGroup * Remove oopsie * Check for modelsbuilder in notification handler instead of runtime validator * Update src/Umbraco.Cms.Api.Management/Controllers/ModelsBuilder/BuildModelsBuilderController.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove ModelsMode enum and ModelsModeExtensions * Apply suggestions from code review Co-authored-by: Kenn Jacobsen <kja@umbraco.dk> * Move project to source folder --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.DevelopmentMode.Backoffice.DependencyInjection;
|
||||
|
||||
public class BackofficeDevelopmentComposer : IComposer
|
||||
{
|
||||
public void Compose(IUmbracoBuilder builder) => builder.AddBackofficeDevelopment();
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.DevelopmentMode.Backoffice.InMemoryAuto;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.DevelopmentMode.Backoffice.DependencyInjection;
|
||||
|
||||
|
||||
/*
|
||||
* 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 InMemoryModelFactory 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. There is a known public API
|
||||
* where you can add reference paths to the runtime razor compiler via it's IOptions: MvcRazorRuntimeCompilationOptions
|
||||
* however this falls short too because those references are just loaded via the RazorReferenceManager and lazy initialized.
|
||||
*
|
||||
* 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 worked, however after a breaking change from dotnet, we cannot go with this options any longer.
|
||||
* The reason for this is that when the default RuntimeViewCompiler loads in the assembly using Assembly.Load,
|
||||
* This will not work for us since this loads the compiled views into the default AssemblyLoadContext,
|
||||
* and our compiled models are loaded in the collectible UmbracoAssemblyLoadContext, and as per the breaking change
|
||||
* you're no longer allowed reference a collectible load context from a non-collectible one
|
||||
* That is the non-collectible compiled views are not allowed to reference the collectible InMemoryAuto models.
|
||||
* https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/7.0/collectible-assemblies
|
||||
*
|
||||
* So what do we do then?
|
||||
* We've had to go with option a unfortunately, and we've cloned the above classes
|
||||
* There has had to be some modifications to the ViewCompiler (CollectibleRuntimeViewCompiler)
|
||||
* First off we've added a new class InMemoryAssemblyLoadContextManager, the role of this class is to ensure that
|
||||
* no one will take a reference to the assembly load context (you cannot unload an assembly load context if there's any references to it).
|
||||
* This means that both the InMemoryAutoFactory and the ViewCompiler uses the LoadContextManager to load their assemblies.
|
||||
* This serves another purpose being that it keeps track of the location of the models assembly.
|
||||
* This means that we no longer use the RazorReferencesManager to resolve that specific dependency, but instead add and explicit dependency to the models assembly.
|
||||
*
|
||||
* With this our assembly load context issue is solved, however the caching issue still persists now that we no longer use the RefreshingRazorViewEngine
|
||||
* To clear these caches another class the RuntimeCompilationCacheBuster has been introduced,
|
||||
* this keeps a reference to the CollectibleRuntimeViewCompiler and the RazorViewEngine and is injected into the InMemoryModelsFactory to clear the caches when rebuilding modes.
|
||||
* In order to avoid having to copy all the RazorViewEngine code the cache buster uses reflection to call the internal ClearCache method of the RazorViewEngine.
|
||||
*/
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
public static IUmbracoBuilder AddBackofficeDevelopment(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (builder.Config.GetRuntimeMode() != RuntimeMode.BackofficeDevelopment)
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
|
||||
builder.AddMvcAndRazor(mvcBuilder =>
|
||||
{
|
||||
mvcBuilder.AddRazorRuntimeCompilation();
|
||||
});
|
||||
builder.AddInMemoryModelsRazorEngine();
|
||||
builder.RuntimeModeValidators()
|
||||
.Add<InMemoryModelsBuilderModeValidator>();
|
||||
builder.AddNotificationHandler<ModelBindingErrorNotification, ModelsBuilderBindingErrorHandler>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
// See notes in RefreshingRazorViewEngine for information on what this is doing.
|
||||
private static IUmbracoBuilder AddInMemoryModelsRazorEngine(this IUmbracoBuilder builder)
|
||||
{
|
||||
// We should only add/replace these services when models builder is InMemory, otherwise we'll cause issues.
|
||||
// Since these services expect the ModelsMode to be InMemoryAuto
|
||||
if (builder.Config.GetModelsMode() == ModelsModeConstants.InMemoryAuto)
|
||||
{
|
||||
builder.Services.AddSingleton<UmbracoRazorReferenceManager>();
|
||||
builder.Services.AddSingleton<CompilationOptionsProvider>();
|
||||
builder.Services.AddSingleton<IViewCompilerProvider, UmbracoViewCompilerProvider>();
|
||||
builder.Services.AddSingleton<RuntimeCompilationCacheBuster>();
|
||||
builder.Services.AddSingleton<InMemoryAssemblyLoadContextManager>();
|
||||
|
||||
builder.Services.AddSingleton<InMemoryModelFactory>();
|
||||
// Register the factory as IPublishedModelFactory
|
||||
builder.Services.AddSingleton<IPublishedModelFactory, InMemoryModelFactory>();
|
||||
return builder;
|
||||
}
|
||||
|
||||
// This is what the community MB would replace, all of the above services are fine to be registered
|
||||
builder.Services.AddSingleton<IPublishedModelFactory>(factory => factory.CreateDefaultPublishedModelFactory());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user