Files
Umbraco-CMS/src/Umbraco.Cms.DevelopmentMode.Backoffice/DependencyInjection/UmbracoBuilderExtensions.cs
Mole 859505e751 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>
2025-09-23 11:58:09 +02:00

128 lines
8.2 KiB
C#

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;
}
}