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:
@@ -240,11 +240,6 @@ public static partial class UmbracoBuilderExtensions
|
||||
// this will directly affect developers who need to call that themselves.
|
||||
IMvcBuilder mvcBuilder = builder.Services.AddControllersWithViews();
|
||||
|
||||
if (builder.Config.GetRuntimeMode() != RuntimeMode.Production)
|
||||
{
|
||||
mvcBuilder.AddRazorRuntimeCompilation();
|
||||
}
|
||||
|
||||
mvcBuilding?.Invoke(mvcBuilder);
|
||||
|
||||
return builder;
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Umbraco.Cms.Web.Common.Filters;
|
||||
/// In which case it returns a redirect to the same page after 1 sec if not in debug mode.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only enabled when using <see cref="ModelsMode.InMemoryAuto" /> mode
|
||||
/// This is only enabled when using InMemoryAuto mode.
|
||||
/// </remarks>
|
||||
public sealed class ModelBindingExceptionAttribute : TypeFilterAttribute
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Serilog;
|
||||
using Serilog.Extensions.Hosting;
|
||||
using Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Logging;
|
||||
|
||||
|
||||
@@ -1,92 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
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.Infrastructure.ModelsBuilder;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder.Options;
|
||||
using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators;
|
||||
using Umbraco.Cms.Web.Common.ModelsBuilder;
|
||||
using Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
@@ -109,14 +30,6 @@ public static class UmbracoBuilderDependencyInjectionExtensions
|
||||
|
||||
builder.Services.Add(umbServices);
|
||||
|
||||
if (builder.Config.GetRuntimeMode() == RuntimeMode.BackofficeDevelopment)
|
||||
{
|
||||
// Configure services to allow InMemoryAuto mode
|
||||
builder.AddInMemoryModelsRazorEngine();
|
||||
|
||||
builder.AddNotificationHandler<ModelBindingErrorNotification, ModelsBuilderNotificationHandler>();
|
||||
}
|
||||
|
||||
if (builder.Config.GetRuntimeMode() != RuntimeMode.Production)
|
||||
{
|
||||
// Configure service to allow models generation
|
||||
@@ -144,31 +57,10 @@ public static class UmbracoBuilderDependencyInjectionExtensions
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigurePropertySettingsOptions>();
|
||||
|
||||
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() is ModelsMode.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());
|
||||
builder.AddNotificationHandler<UmbracoApplicationStartedNotification, RazorRuntimeCompilationValidator>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Razor.Hosting;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
/*
|
||||
* This is a clone of Microsofts implementation.
|
||||
* https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/ChecksumValidator.cs
|
||||
* The purpose of this class is to check if compiled views has changed and needs to be recompiled.
|
||||
*/
|
||||
internal static class ChecksumValidator
|
||||
{
|
||||
public static bool IsRecompilationSupported(RazorCompiledItem item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
// A Razor item only supports recompilation if its primary source file has a checksum.
|
||||
//
|
||||
// Other files (view imports) may or may not have existed at the time of compilation,
|
||||
// so we may not have checksums for them.
|
||||
IReadOnlyList<IRazorSourceChecksumMetadata> checksums = item.GetChecksumMetadata();
|
||||
return checksums.Any(c => string.Equals(item.Identifier, c.Identifier, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// Validates that we can use an existing precompiled view by comparing checksums with files on
|
||||
// disk.
|
||||
public static bool IsItemValid(RazorProjectFileSystem fileSystem, RazorCompiledItem item)
|
||||
{
|
||||
if (fileSystem == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fileSystem));
|
||||
}
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
IReadOnlyList<IRazorSourceChecksumMetadata> checksums = item.GetChecksumMetadata();
|
||||
|
||||
// The checksum that matches 'Item.Identity' in this list is significant. That represents the main file.
|
||||
//
|
||||
// We don't really care about the validation unless the main file exists. This is because we expect
|
||||
// most sites to have some _ViewImports in common location. That means that in the case you're
|
||||
// using views from a 3rd party library, you'll always have **some** conflicts.
|
||||
//
|
||||
// The presence of the main file with the same content is a very strong signal that you're in a
|
||||
// development scenario.
|
||||
IRazorSourceChecksumMetadata? primaryChecksum = checksums
|
||||
.FirstOrDefault(c => string.Equals(item.Identifier, c.Identifier, StringComparison.OrdinalIgnoreCase));
|
||||
if (primaryChecksum == null)
|
||||
{
|
||||
// No primary checksum, assume valid.
|
||||
return true;
|
||||
}
|
||||
|
||||
RazorProjectItem projectItem = fileSystem.GetItem(primaryChecksum.Identifier, fileKind: null);
|
||||
if (!projectItem.Exists)
|
||||
{
|
||||
// Main file doesn't exist - assume valid.
|
||||
return true;
|
||||
}
|
||||
|
||||
var sourceDocument = RazorSourceDocument.ReadFrom(projectItem);
|
||||
if (!string.Equals(sourceDocument.GetChecksumAlgorithm(), primaryChecksum.ChecksumAlgorithm) ||
|
||||
!ChecksumsEqual(primaryChecksum.Checksum, sourceDocument.GetChecksum()))
|
||||
{
|
||||
// Main file exists, but checksums not equal.
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < checksums.Count; i++)
|
||||
{
|
||||
IRazorSourceChecksumMetadata checksum = checksums[i];
|
||||
if (string.Equals(item.Identifier, checksum.Identifier, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Ignore primary checksum on this pass.
|
||||
continue;
|
||||
}
|
||||
|
||||
RazorProjectItem importItem = fileSystem.GetItem(checksum.Identifier, fileKind: null);
|
||||
if (!importItem.Exists)
|
||||
{
|
||||
// Import file doesn't exist - assume invalid.
|
||||
return false;
|
||||
}
|
||||
|
||||
sourceDocument = RazorSourceDocument.ReadFrom(importItem);
|
||||
if (!string.Equals(sourceDocument.GetChecksumAlgorithm(), checksum.ChecksumAlgorithm) ||
|
||||
!ChecksumsEqual(checksum.Checksum, sourceDocument.GetChecksum()))
|
||||
{
|
||||
// Import file exists, but checksums not equal.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ChecksumsEqual(string checksum, byte[] bytes)
|
||||
{
|
||||
if (bytes.Length * 2 != checksum.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
var text = bytes[i].ToString("x2", CultureInfo.InvariantCulture);
|
||||
if (checksum[i * 2] != text[0] || checksum[(i * 2) + 1] != text[1])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNetCore.Razor.Hosting;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Emit;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
internal sealed class CollectibleRuntimeViewCompiler : IViewCompiler
|
||||
{
|
||||
private readonly Lock _cacheLock = new();
|
||||
private readonly Dictionary<string, CompiledViewDescriptor> _precompiledViews;
|
||||
private readonly ConcurrentDictionary<string, string> _normalizedPathCache;
|
||||
private readonly IFileProvider _fileProvider;
|
||||
private readonly RazorProjectEngine _projectEngine;
|
||||
private IMemoryCache _cache;
|
||||
private readonly ILogger<CollectibleRuntimeViewCompiler> _logger;
|
||||
private readonly UmbracoRazorReferenceManager _referenceManager;
|
||||
private readonly CompilationOptionsProvider _compilationOptionsProvider;
|
||||
private readonly InMemoryAssemblyLoadContextManager _loadContextManager;
|
||||
|
||||
public CollectibleRuntimeViewCompiler(
|
||||
IFileProvider fileProvider,
|
||||
RazorProjectEngine projectEngine,
|
||||
IList<CompiledViewDescriptor> precompiledViews,
|
||||
ILogger<CollectibleRuntimeViewCompiler> logger,
|
||||
UmbracoRazorReferenceManager referenceManager,
|
||||
CompilationOptionsProvider compilationOptionsProvider,
|
||||
InMemoryAssemblyLoadContextManager loadContextManager)
|
||||
{
|
||||
if (precompiledViews == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(precompiledViews));
|
||||
}
|
||||
|
||||
_fileProvider = fileProvider ?? throw new ArgumentNullException(nameof(fileProvider));
|
||||
_projectEngine = projectEngine ?? throw new ArgumentNullException(nameof(projectEngine));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_referenceManager = referenceManager;
|
||||
_compilationOptionsProvider = compilationOptionsProvider;
|
||||
_loadContextManager = loadContextManager;
|
||||
|
||||
_normalizedPathCache = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
|
||||
|
||||
// This is our L0 cache, and is a durable store. Views migrate into the cache as they are requested
|
||||
// from either the set of known precompiled views, or by being compiled.
|
||||
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||
|
||||
// We need to validate that the all of the precompiled views are unique by path (case-insensitive).
|
||||
// We do this because there's no good way to canonicalize paths on windows, and it will create
|
||||
// problems when deploying to linux. Rather than deal with these issues, we just don't support
|
||||
// views that differ only by case.
|
||||
_precompiledViews = new Dictionary<string, CompiledViewDescriptor>(
|
||||
precompiledViews.Count,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (CompiledViewDescriptor precompiledView in precompiledViews)
|
||||
{
|
||||
_logger.LogDebug("Initializing Razor view compiler with compiled view: '{ViewName}'", precompiledView.RelativePath);
|
||||
// View ordering has precedence semantics, a view with a higher precedence was
|
||||
// already added to the list.
|
||||
_precompiledViews.TryAdd(precompiledView.RelativePath, precompiledView);
|
||||
}
|
||||
|
||||
if (_precompiledViews.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("Initializing Razor view compiler with no compiled views");
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearCache()
|
||||
{
|
||||
// I'm pretty sure this is not necessary, since it should be an atomic operation,
|
||||
// but let's make sure that we don't end up resolving any views while clearing the cache.
|
||||
lock (_cacheLock)
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||
}
|
||||
}
|
||||
|
||||
public Task<CompiledViewDescriptor> CompileAsync(string relativePath)
|
||||
{
|
||||
if (relativePath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(relativePath));
|
||||
}
|
||||
|
||||
// Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already
|
||||
// normalized and a cache entry exists.
|
||||
if (_cache.TryGetValue<Task<CompiledViewDescriptor>>(relativePath, out Task<CompiledViewDescriptor>? cachedResult) && cachedResult is not null)
|
||||
{
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
var normalizedPath = GetNormalizedPath(relativePath);
|
||||
if (_cache.TryGetValue(normalizedPath, out cachedResult) && cachedResult is not null)
|
||||
{
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
// Entry does not exist. Attempt to create one.
|
||||
cachedResult = OnCacheMiss(normalizedPath);
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
private Task<CompiledViewDescriptor> OnCacheMiss(string normalizedPath)
|
||||
{
|
||||
ViewCompilerWorkItem item;
|
||||
TaskCompletionSource<CompiledViewDescriptor> taskSource;
|
||||
MemoryCacheEntryOptions cacheEntryOptions;
|
||||
|
||||
// Safe races cannot be allowed when compiling Razor pages. To ensure only one compilation request succeeds
|
||||
// per file, we'll lock the creation of a cache entry. Creating the cache entry should be very quick. The
|
||||
// actual work for compiling files happens outside the critical section.
|
||||
lock (_cacheLock)
|
||||
{
|
||||
// Double-checked locking to handle a possible race.
|
||||
if (_cache.TryGetValue<Task<CompiledViewDescriptor>>(normalizedPath, out Task<CompiledViewDescriptor>? result) && result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (_precompiledViews.TryGetValue(normalizedPath, out CompiledViewDescriptor? precompiledView))
|
||||
{
|
||||
_logger.LogTrace("Located compiled view for view at path '{Path}'", normalizedPath);
|
||||
item = CreatePrecompiledWorkItem(normalizedPath, precompiledView);
|
||||
}
|
||||
else
|
||||
{
|
||||
item = CreateRuntimeCompilationWorkItem(normalizedPath);
|
||||
}
|
||||
|
||||
// At this point, we've decided what to do - but we should create the cache entry and
|
||||
// release the lock first.
|
||||
cacheEntryOptions = new MemoryCacheEntryOptions();
|
||||
|
||||
Debug.Assert(item.ExpirationTokens != null);
|
||||
for (var i = 0; i < item.ExpirationTokens.Count; i++)
|
||||
{
|
||||
cacheEntryOptions.ExpirationTokens.Add(item.ExpirationTokens[i]);
|
||||
}
|
||||
|
||||
taskSource = new TaskCompletionSource<CompiledViewDescriptor>(creationOptions: TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (item.SupportsCompilation)
|
||||
{
|
||||
// We'll compile in just a sec, be patient.
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we can't compile, we should have already created the descriptor
|
||||
Debug.Assert(item.Descriptor != null);
|
||||
taskSource.SetResult(item.Descriptor);
|
||||
}
|
||||
|
||||
_cache.Set(normalizedPath, taskSource.Task, cacheEntryOptions);
|
||||
}
|
||||
|
||||
// Now the lock has been released so we can do more expensive processing.
|
||||
if (item.SupportsCompilation)
|
||||
{
|
||||
Debug.Assert(taskSource != null);
|
||||
|
||||
if (item.Descriptor?.Item != null &&
|
||||
ChecksumValidator.IsItemValid(_projectEngine.FileSystem, item.Descriptor.Item))
|
||||
{
|
||||
// If the item has checksums to validate, we should also have a precompiled view.
|
||||
Debug.Assert(item.Descriptor != null);
|
||||
|
||||
taskSource.SetResult(item.Descriptor);
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Invalidating compiled view at path '{Path}' with a file since the checksum did not match", item.NormalizedPath);
|
||||
try
|
||||
{
|
||||
CompiledViewDescriptor descriptor = CompileAndEmit(normalizedPath);
|
||||
descriptor.ExpirationTokens = cacheEntryOptions.ExpirationTokens;
|
||||
taskSource.SetResult(descriptor);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
taskSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
private ViewCompilerWorkItem CreatePrecompiledWorkItem(string normalizedPath, CompiledViewDescriptor precompiledView)
|
||||
{
|
||||
// We have a precompiled view - but we're not sure that we can use it yet.
|
||||
//
|
||||
// We need to determine first if we have enough information to 'recompile' this view. If that's the case
|
||||
// we'll create change tokens for all of the files.
|
||||
//
|
||||
// Then we'll attempt to validate if any of those files have different content than the original sources
|
||||
// based on checksums.
|
||||
if (precompiledView.Item == null || !ChecksumValidator.IsRecompilationSupported(precompiledView.Item))
|
||||
{
|
||||
return new ViewCompilerWorkItem()
|
||||
{
|
||||
// If we don't have a checksum for the primary source file we can't recompile.
|
||||
SupportsCompilation = false,
|
||||
|
||||
ExpirationTokens = Array.Empty<IChangeToken>(), // Never expire because we can't recompile.
|
||||
Descriptor = precompiledView, // This will be used as-is.
|
||||
};
|
||||
}
|
||||
|
||||
var item = new ViewCompilerWorkItem()
|
||||
{
|
||||
SupportsCompilation = true,
|
||||
|
||||
Descriptor = precompiledView, // This might be used, if the checksums match.
|
||||
|
||||
// Used to validate and recompile
|
||||
NormalizedPath = normalizedPath,
|
||||
|
||||
ExpirationTokens = GetExpirationTokens(precompiledView),
|
||||
};
|
||||
|
||||
// We also need to create a new descriptor, because the original one doesn't have expiration tokens on
|
||||
// it. These will be used by the view location cache, which is like an L1 cache for views (this class is
|
||||
// the L2 cache).
|
||||
item.Descriptor = new CompiledViewDescriptor()
|
||||
{
|
||||
ExpirationTokens = item.ExpirationTokens,
|
||||
Item = precompiledView.Item,
|
||||
RelativePath = precompiledView.RelativePath,
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private ViewCompilerWorkItem CreateRuntimeCompilationWorkItem(string normalizedPath)
|
||||
{
|
||||
IList<IChangeToken> expirationTokens = new List<IChangeToken>
|
||||
{
|
||||
_fileProvider.Watch(normalizedPath),
|
||||
};
|
||||
|
||||
RazorProjectItem projectItem = _projectEngine.FileSystem.GetItem(normalizedPath, fileKind: null);
|
||||
if (!projectItem.Exists)
|
||||
{
|
||||
_logger.LogTrace("Could not find a file for view at path '{Path}'", normalizedPath);
|
||||
// If the file doesn't exist, we can't do compilation right now - we still want to cache
|
||||
// the fact that we tried. This will allow us to re-trigger compilation if the view file
|
||||
// is added.
|
||||
return new ViewCompilerWorkItem()
|
||||
{
|
||||
// We don't have enough information to compile
|
||||
SupportsCompilation = false,
|
||||
|
||||
Descriptor = new CompiledViewDescriptor()
|
||||
{
|
||||
RelativePath = normalizedPath,
|
||||
ExpirationTokens = expirationTokens,
|
||||
},
|
||||
|
||||
// We can try again if the file gets created.
|
||||
ExpirationTokens = expirationTokens,
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogTrace("Found file at path '{Path}'", normalizedPath);
|
||||
|
||||
GetChangeTokensFromImports(expirationTokens, projectItem);
|
||||
|
||||
return new ViewCompilerWorkItem()
|
||||
{
|
||||
SupportsCompilation = true,
|
||||
|
||||
NormalizedPath = normalizedPath,
|
||||
ExpirationTokens = expirationTokens,
|
||||
};
|
||||
}
|
||||
|
||||
private IList<IChangeToken> GetExpirationTokens(CompiledViewDescriptor precompiledView)
|
||||
{
|
||||
IReadOnlyList<IRazorSourceChecksumMetadata> checksums = precompiledView.Item.GetChecksumMetadata();
|
||||
var expirationTokens = new List<IChangeToken>(checksums.Count);
|
||||
|
||||
for (var i = 0; i < checksums.Count; i++)
|
||||
{
|
||||
// We rely on Razor to provide the right set of checksums. Trust the compiler, it has to do a good job,
|
||||
// so it probably will.
|
||||
expirationTokens.Add(_fileProvider.Watch(checksums[i].Identifier));
|
||||
}
|
||||
|
||||
return expirationTokens;
|
||||
}
|
||||
|
||||
private void GetChangeTokensFromImports(IList<IChangeToken> expirationTokens, RazorProjectItem projectItem)
|
||||
{
|
||||
// OK this means we can do compilation. For now let's just identify the other files we need to watch
|
||||
// so we can create the cache entry. Compilation will happen after we release the lock.
|
||||
IImportProjectFeature[] importFeature = _projectEngine.ProjectFeatures.OfType<IImportProjectFeature>().ToArray();
|
||||
foreach (IImportProjectFeature feature in importFeature)
|
||||
{
|
||||
foreach (RazorProjectItem? file in feature.GetImports(projectItem))
|
||||
{
|
||||
if (file.FilePath != null)
|
||||
{
|
||||
expirationTokens.Add(_fileProvider.Watch(file.FilePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CompiledViewDescriptor CompileAndEmit(string relativePath)
|
||||
{
|
||||
RazorProjectItem projectItem = _projectEngine.FileSystem.GetItem(relativePath, fileKind: null);
|
||||
RazorCodeDocument codeDocument = _projectEngine.Process(projectItem);
|
||||
RazorCSharpDocument cSharpDocument = codeDocument.GetCSharpDocument();
|
||||
|
||||
if (cSharpDocument.Diagnostics.Count > 0)
|
||||
{
|
||||
throw CompilationExceptionFactory.Create(
|
||||
codeDocument,
|
||||
cSharpDocument.Diagnostics);
|
||||
}
|
||||
|
||||
Assembly assembly = CompileAndEmit(codeDocument, cSharpDocument.GeneratedCode);
|
||||
|
||||
// Anything we compile from source will use Razor 2.1 and so should have the new metadata.
|
||||
var loader = new RazorCompiledItemLoader();
|
||||
RazorCompiledItem item = loader.LoadItems(assembly).Single();
|
||||
return new CompiledViewDescriptor(item);
|
||||
}
|
||||
|
||||
internal Assembly CompileAndEmit(RazorCodeDocument codeDocument, string generatedCode)
|
||||
{
|
||||
var startTimestamp = _logger.IsEnabled(LogLevel.Debug) ? Stopwatch.GetTimestamp() : 0;
|
||||
|
||||
var assemblyName = Path.GetRandomFileName();
|
||||
CSharpCompilation compilation = CreateCompilation(generatedCode, assemblyName);
|
||||
|
||||
EmitOptions emitOptions = _compilationOptionsProvider.EmitOptions;
|
||||
var emitPdbFile = _compilationOptionsProvider.EmitPdb && emitOptions.DebugInformationFormat != DebugInformationFormat.Embedded;
|
||||
|
||||
|
||||
using (var assemblyStream = new MemoryStream())
|
||||
using (MemoryStream? pdbStream = emitPdbFile ? new MemoryStream() : null)
|
||||
{
|
||||
EmitResult result = compilation.Emit(
|
||||
assemblyStream,
|
||||
pdbStream,
|
||||
options: _compilationOptionsProvider.EmitOptions);
|
||||
|
||||
if (result.Success is false)
|
||||
{
|
||||
UmbracoCompilationException compilationException = CompilationExceptionFactory.Create(
|
||||
codeDocument,
|
||||
generatedCode,
|
||||
assemblyName,
|
||||
result.Diagnostics);
|
||||
|
||||
LogCompilationFailure(compilationException);
|
||||
|
||||
throw compilationException;
|
||||
}
|
||||
|
||||
assemblyStream.Seek(0, SeekOrigin.Begin);
|
||||
pdbStream?.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
Assembly assembly = _loadContextManager.LoadCollectibleAssemblyFromStream(assemblyStream, pdbStream);
|
||||
|
||||
return assembly;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogCompilationFailure(UmbracoCompilationException compilationException)
|
||||
{
|
||||
IEnumerable<string>? messages = compilationException.CompilationFailures?
|
||||
.WhereNotNull()
|
||||
.SelectMany(x => x.Messages!)
|
||||
.WhereNotNull()
|
||||
.Select(x => x.FormattedMessage)
|
||||
.WhereNotNull();
|
||||
|
||||
foreach (var message in messages ?? Enumerable.Empty<string>())
|
||||
{
|
||||
_logger.LogError(compilationException, "Compilation error occured with message: {ErrorMessage}", message);
|
||||
}
|
||||
}
|
||||
|
||||
private CSharpCompilation CreateCompilation(string compilationContent, string assemblyName)
|
||||
{
|
||||
IReadOnlyList<MetadataReference> refs = _referenceManager.CompilationReferences;
|
||||
|
||||
var sourceText = SourceText.From(compilationContent, Encoding.UTF8);
|
||||
SyntaxTree syntaxTree = SyntaxFactory
|
||||
.ParseSyntaxTree(sourceText, _compilationOptionsProvider.ParseOptions)
|
||||
.WithFilePath(assemblyName);
|
||||
|
||||
CSharpCompilation compilation = CSharpCompilation
|
||||
.Create(assemblyName)
|
||||
.AddSyntaxTrees(syntaxTree)
|
||||
.AddReferences(refs)
|
||||
.WithOptions(_compilationOptionsProvider.CSharpCompilationOptions);
|
||||
|
||||
// We'll add the reference to the InMemory assembly directly, this means we don't have to hack around with assembly parts.
|
||||
// We might be asked to compile views before the InMemory models assembly is created tho (if you replace the no-nodes for instance)
|
||||
// In this case we'll just skip the InMemory models assembly reference
|
||||
if (_loadContextManager.ModelsAssemblyLocation is null)
|
||||
{
|
||||
_logger.LogInformation("No InMemory models assembly available, skipping reference");
|
||||
return compilation;
|
||||
}
|
||||
|
||||
PortableExecutableReference inMemoryAutoReference = MetadataReference.CreateFromFile(_loadContextManager.ModelsAssemblyLocation);
|
||||
compilation = compilation.AddReferences(inMemoryAutoReference);
|
||||
return compilation;
|
||||
}
|
||||
|
||||
private string GetNormalizedPath(string relativePath)
|
||||
{
|
||||
Debug.Assert(relativePath != null);
|
||||
if (relativePath.Length == 0)
|
||||
{
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
if (!_normalizedPathCache.TryGetValue(relativePath, out var normalizedPath))
|
||||
{
|
||||
normalizedPath = NormalizePath(relativePath);
|
||||
_normalizedPathCache[relativePath] = normalizedPath;
|
||||
}
|
||||
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
// Taken from: https://github.com/dotnet/aspnetcore/blob/a450cb69b5e4549f5515cdb057a68771f56cefd7/src/Mvc/Mvc.Razor/src/ViewPath.cs
|
||||
// This normalizes the relative path to the view, ensuring that it matches with what we have as keys in _precompiledViews
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
var addLeadingSlash = path[0] != '\\' && path[0] != '/';
|
||||
var transformSlashes = path.IndexOf('\\') != -1;
|
||||
|
||||
if (!addLeadingSlash && !transformSlashes)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var length = path.Length;
|
||||
if (addLeadingSlash)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
|
||||
return string.Create(length, (path, addLeadingSlash), (span, tuple) =>
|
||||
{
|
||||
(string pathValue, bool addLeadingSlashValue) = tuple;
|
||||
var spanIndex = 0;
|
||||
|
||||
if (addLeadingSlashValue)
|
||||
{
|
||||
span[spanIndex++] = '/';
|
||||
}
|
||||
|
||||
foreach (var ch in pathValue)
|
||||
{
|
||||
span[spanIndex++] = ch == '\\' ? '/' : ch;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class ViewCompilerWorkItem
|
||||
{
|
||||
public bool SupportsCompilation { get; set; } = default!;
|
||||
|
||||
public string NormalizedPath { get; set; } = default!;
|
||||
|
||||
public IList<IChangeToken> ExpirationTokens { get; set; } = default!;
|
||||
|
||||
public CompiledViewDescriptor Descriptor { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
/*
|
||||
* This is a partial clone of the frameworks CompilationFailedExceptionFactory, a few things has been simplified to fit our needs.
|
||||
* https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/CompilationFailedExceptionFactory.cs
|
||||
*/
|
||||
internal static class CompilationExceptionFactory
|
||||
{
|
||||
public static UmbracoCompilationException Create(
|
||||
RazorCodeDocument codeDocument,
|
||||
IEnumerable<RazorDiagnostic> diagnostics)
|
||||
{
|
||||
// If a SourceLocation does not specify a file path, assume it is produced from parsing the current file.
|
||||
var messageGroups = diagnostics.GroupBy(
|
||||
razorError => razorError.Span.FilePath ?? codeDocument.Source.FilePath,
|
||||
StringComparer.Ordinal);
|
||||
|
||||
var failures = new List<CompilationFailure>();
|
||||
foreach (var group in messageGroups)
|
||||
{
|
||||
var filePath = group.Key;
|
||||
var fileContent = ReadContent(codeDocument, filePath);
|
||||
var compilationFailure = new CompilationFailure(
|
||||
filePath,
|
||||
fileContent,
|
||||
compiledContent: string.Empty,
|
||||
messages: group.Select(parserError => CreateDiagnosticMessage(parserError, filePath)));
|
||||
failures.Add(compilationFailure);
|
||||
}
|
||||
|
||||
return new UmbracoCompilationException{CompilationFailures = failures};
|
||||
}
|
||||
|
||||
public static UmbracoCompilationException Create(
|
||||
RazorCodeDocument codeDocument,
|
||||
string compilationContent,
|
||||
string assemblyName,
|
||||
IEnumerable<Diagnostic> diagnostics)
|
||||
{
|
||||
var diagnosticGroups = diagnostics
|
||||
.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.GroupBy(diagnostic => GetFilePath(codeDocument, diagnostic), StringComparer.Ordinal);
|
||||
|
||||
var failures = new List<CompilationFailure>();
|
||||
foreach (var group in diagnosticGroups)
|
||||
{
|
||||
var sourceFilePath = group.Key;
|
||||
string sourceFileContent;
|
||||
if (string.Equals(assemblyName, sourceFilePath, StringComparison.Ordinal))
|
||||
{
|
||||
// The error is in the generated code and does not have a mapping line pragma
|
||||
sourceFileContent = compilationContent;
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceFileContent = ReadContent(codeDocument, sourceFilePath!);
|
||||
}
|
||||
|
||||
var compilationFailure = new CompilationFailure(
|
||||
sourceFilePath,
|
||||
sourceFileContent,
|
||||
compilationContent,
|
||||
group.Select(GetDiagnosticMessage));
|
||||
|
||||
failures.Add(compilationFailure);
|
||||
}
|
||||
|
||||
return new UmbracoCompilationException{ CompilationFailures = failures};
|
||||
}
|
||||
|
||||
private static string ReadContent(RazorCodeDocument codeDocument, string filePath)
|
||||
{
|
||||
RazorSourceDocument? sourceDocument;
|
||||
if (string.IsNullOrEmpty(filePath) || string.Equals(codeDocument.Source.FilePath, filePath, StringComparison.Ordinal))
|
||||
{
|
||||
sourceDocument = codeDocument.Source;
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceDocument = codeDocument.Imports.FirstOrDefault(f => string.Equals(f.FilePath, filePath, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
if (sourceDocument != null)
|
||||
{
|
||||
var contentChars = new char[sourceDocument.Length];
|
||||
sourceDocument.CopyTo(0, contentChars, 0, sourceDocument.Length);
|
||||
return new string(contentChars);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static DiagnosticMessage GetDiagnosticMessage(Diagnostic diagnostic)
|
||||
{
|
||||
var mappedLineSpan = diagnostic.Location.GetMappedLineSpan();
|
||||
return new DiagnosticMessage(
|
||||
diagnostic.GetMessage(CultureInfo.CurrentCulture),
|
||||
CSharpDiagnosticFormatter.Instance.Format(diagnostic, CultureInfo.CurrentCulture),
|
||||
mappedLineSpan.Path,
|
||||
mappedLineSpan.StartLinePosition.Line + 1,
|
||||
mappedLineSpan.StartLinePosition.Character + 1,
|
||||
mappedLineSpan.EndLinePosition.Line + 1,
|
||||
mappedLineSpan.EndLinePosition.Character + 1);
|
||||
}
|
||||
|
||||
private static string GetFilePath(RazorCodeDocument codeDocument, Diagnostic diagnostic)
|
||||
{
|
||||
if (diagnostic.Location == Location.None)
|
||||
{
|
||||
return codeDocument.Source.FilePath;
|
||||
}
|
||||
|
||||
return diagnostic.Location.GetMappedLineSpan().Path;
|
||||
}
|
||||
|
||||
private static DiagnosticMessage CreateDiagnosticMessage(
|
||||
RazorDiagnostic razorDiagnostic,
|
||||
string filePath)
|
||||
{
|
||||
var sourceSpan = razorDiagnostic.Span;
|
||||
var message = razorDiagnostic.GetMessage(CultureInfo.CurrentCulture);
|
||||
return new DiagnosticMessage(
|
||||
message: message,
|
||||
formattedMessage: razorDiagnostic.ToString(),
|
||||
filePath: filePath,
|
||||
startLine: sourceSpan.LineIndex + 1,
|
||||
startColumn: sourceSpan.CharacterIndex,
|
||||
endLine: sourceSpan.LineIndex + 1,
|
||||
endColumn: sourceSpan.CharacterIndex + sourceSpan.Length);
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Emit;
|
||||
using Microsoft.Extensions.DependencyModel;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using DependencyContextCompilationOptions = Microsoft.Extensions.DependencyModel.CompilationOptions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
/*
|
||||
* This is a partial Clone'n'Own of microsofts CSharpCompiler, this is just the parts relevant for getting the CompilationOptions
|
||||
* Essentially, what this does is that it looks at the compilation options of the Dotnet project and "copies" that
|
||||
* https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/CSharpCompiler.cs
|
||||
*/
|
||||
internal class CompilationOptionsProvider
|
||||
{
|
||||
private readonly IWebHostEnvironment _hostingEnvironment;
|
||||
private CSharpParseOptions? _parseOptions;
|
||||
private CSharpCompilationOptions? _compilationOptions;
|
||||
private bool _emitPdb;
|
||||
private EmitOptions? _emitOptions;
|
||||
private bool _optionsInitialized;
|
||||
|
||||
public CompilationOptionsProvider(IWebHostEnvironment hostingEnvironment) =>
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
|
||||
public virtual CSharpParseOptions ParseOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureOptions();
|
||||
return _parseOptions;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual CSharpCompilationOptions CSharpCompilationOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureOptions();
|
||||
return _compilationOptions;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool EmitPdb
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureOptions();
|
||||
return _emitPdb;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual EmitOptions EmitOptions
|
||||
{
|
||||
get
|
||||
{
|
||||
EnsureOptions();
|
||||
return _emitOptions;
|
||||
}
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(_emitOptions), nameof(_parseOptions), nameof(_compilationOptions))]
|
||||
private void EnsureOptions()
|
||||
{
|
||||
if (!_optionsInitialized)
|
||||
{
|
||||
var dependencyContextOptions = GetDependencyContextCompilationOptions();
|
||||
_parseOptions = GetParseOptions(_hostingEnvironment, dependencyContextOptions);
|
||||
_compilationOptions = GetCompilationOptions(_hostingEnvironment, dependencyContextOptions);
|
||||
_emitOptions = GetEmitOptions(dependencyContextOptions);
|
||||
|
||||
_optionsInitialized = true;
|
||||
}
|
||||
|
||||
Debug.Assert(_parseOptions is not null);
|
||||
Debug.Assert(_compilationOptions is not null);
|
||||
Debug.Assert(_emitOptions is not null);
|
||||
}
|
||||
|
||||
private DependencyContextCompilationOptions GetDependencyContextCompilationOptions()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_hostingEnvironment.ApplicationName))
|
||||
{
|
||||
var applicationAssembly = Assembly.Load(new AssemblyName(_hostingEnvironment.ApplicationName));
|
||||
var dependencyContext = DependencyContext.Load(applicationAssembly);
|
||||
if (dependencyContext?.CompilationOptions != null)
|
||||
{
|
||||
return dependencyContext.CompilationOptions;
|
||||
}
|
||||
}
|
||||
|
||||
return DependencyContextCompilationOptions.Default;
|
||||
}
|
||||
|
||||
private EmitOptions GetEmitOptions(DependencyContextCompilationOptions dependencyContextOptions)
|
||||
{
|
||||
// Assume we're always producing pdbs unless DebugType = none
|
||||
_emitPdb = true;
|
||||
DebugInformationFormat debugInformationFormat;
|
||||
if (string.IsNullOrEmpty(dependencyContextOptions.DebugType))
|
||||
{
|
||||
debugInformationFormat = DebugInformationFormat.PortablePdb;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Based on https://github.com/dotnet/roslyn/blob/1d28ff9ba248b332de3c84d23194a1d7bde07e4d/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs#L624-L640
|
||||
switch (dependencyContextOptions.DebugType.ToLowerInvariant())
|
||||
{
|
||||
case "none":
|
||||
// There isn't a way to represent none in DebugInformationFormat.
|
||||
// We'll set EmitPdb to false and let callers handle it by setting a null pdb-stream.
|
||||
_emitPdb = false;
|
||||
return new EmitOptions();
|
||||
case "portable":
|
||||
debugInformationFormat = DebugInformationFormat.PortablePdb;
|
||||
break;
|
||||
case "embedded":
|
||||
// Roslyn does not expose enough public APIs to produce a binary with embedded pdbs.
|
||||
// We'll produce PortablePdb instead to continue providing a reasonable user experience.
|
||||
debugInformationFormat = DebugInformationFormat.PortablePdb;
|
||||
break;
|
||||
case "full":
|
||||
case "pdbonly":
|
||||
debugInformationFormat = DebugInformationFormat.PortablePdb;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
var emitOptions = new EmitOptions(debugInformationFormat: debugInformationFormat);
|
||||
return emitOptions;
|
||||
}
|
||||
|
||||
private static CSharpCompilationOptions GetCompilationOptions(
|
||||
IWebHostEnvironment hostingEnvironment,
|
||||
DependencyContextCompilationOptions dependencyContextOptions)
|
||||
{
|
||||
var csharpCompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
|
||||
|
||||
// Disable 1702 until roslyn turns this off by default
|
||||
csharpCompilationOptions = csharpCompilationOptions.WithSpecificDiagnosticOptions(
|
||||
new Dictionary<string, ReportDiagnostic>
|
||||
{
|
||||
{"CS1701", ReportDiagnostic.Suppress}, // Binding redirects
|
||||
{"CS1702", ReportDiagnostic.Suppress},
|
||||
{"CS1705", ReportDiagnostic.Suppress}
|
||||
});
|
||||
|
||||
if (dependencyContextOptions.AllowUnsafe.HasValue)
|
||||
{
|
||||
csharpCompilationOptions = csharpCompilationOptions.WithAllowUnsafe(
|
||||
dependencyContextOptions.AllowUnsafe.Value);
|
||||
}
|
||||
|
||||
OptimizationLevel optimizationLevel;
|
||||
if (dependencyContextOptions.Optimize.HasValue)
|
||||
{
|
||||
optimizationLevel = dependencyContextOptions.Optimize.Value ?
|
||||
OptimizationLevel.Release :
|
||||
OptimizationLevel.Debug;
|
||||
}
|
||||
else
|
||||
{
|
||||
optimizationLevel = hostingEnvironment.IsDevelopment() ?
|
||||
OptimizationLevel.Debug :
|
||||
OptimizationLevel.Release;
|
||||
}
|
||||
csharpCompilationOptions = csharpCompilationOptions.WithOptimizationLevel(optimizationLevel);
|
||||
|
||||
if (dependencyContextOptions.WarningsAsErrors.HasValue)
|
||||
{
|
||||
var reportDiagnostic = dependencyContextOptions.WarningsAsErrors.Value ?
|
||||
ReportDiagnostic.Error :
|
||||
ReportDiagnostic.Default;
|
||||
csharpCompilationOptions = csharpCompilationOptions.WithGeneralDiagnosticOption(reportDiagnostic);
|
||||
}
|
||||
|
||||
return csharpCompilationOptions;
|
||||
}
|
||||
|
||||
private static CSharpParseOptions GetParseOptions(
|
||||
IWebHostEnvironment hostingEnvironment,
|
||||
DependencyContextCompilationOptions dependencyContextOptions)
|
||||
{
|
||||
var configurationSymbol = hostingEnvironment.IsDevelopment() ? "DEBUG" : "RELEASE";
|
||||
var defines = dependencyContextOptions.Defines.Concat(new[] { configurationSymbol }).Where(define => define != null);
|
||||
|
||||
var parseOptions = new CSharpParseOptions(preprocessorSymbols: (IEnumerable<string>)defines);
|
||||
|
||||
parseOptions = parseOptions.WithLanguageVersion(LanguageVersion.Latest);
|
||||
|
||||
return parseOptions;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
internal sealed class InMemoryAssemblyLoadContextManager
|
||||
{
|
||||
private UmbracoAssemblyLoadContext? _currentAssemblyLoadContext;
|
||||
|
||||
public InMemoryAssemblyLoadContextManager() =>
|
||||
AssemblyLoadContext.Default.Resolving += OnResolvingDefaultAssemblyLoadContext;
|
||||
|
||||
private string? _modelsAssemblyLocation;
|
||||
|
||||
public string? ModelsAssemblyLocation => _modelsAssemblyLocation;
|
||||
|
||||
/// <summary>
|
||||
/// Handle the event when a reference cannot be resolved from the default context and return our custom MB assembly reference if we have one
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is required because the razor engine will only try to load things from the default context, it doesn't know anything
|
||||
/// about our context so we need to proxy.
|
||||
/// </remarks>
|
||||
private Assembly? OnResolvingDefaultAssemblyLoadContext(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
|
||||
=> assemblyName.Name == RoslynCompiler.GeneratedAssemblyName
|
||||
? _currentAssemblyLoadContext?.LoadFromAssemblyName(assemblyName)
|
||||
: null;
|
||||
|
||||
internal void RenewAssemblyLoadContext()
|
||||
{
|
||||
// If there's a current AssemblyLoadContext, unload it before creating a new one.
|
||||
_currentAssemblyLoadContext?.Unload();
|
||||
|
||||
// We must create a new assembly load context
|
||||
// as long as theres a reference to the assembly load context we can't delete the assembly it loaded
|
||||
_currentAssemblyLoadContext = new UmbracoAssemblyLoadContext();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an assembly into the collectible assembly used by the factory
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is essentially just a wrapper around the <see cref="UmbracoAssemblyLoadContext"/>,
|
||||
/// because we don't want to allow other clases to take a reference on the AssemblyLoadContext
|
||||
/// </remarks>
|
||||
/// <returns>The loaded assembly</returns>
|
||||
public Assembly LoadCollectibleAssemblyFromStream(Stream assembly, Stream? assemblySymbols)
|
||||
{
|
||||
_currentAssemblyLoadContext ??= new UmbracoAssemblyLoadContext();
|
||||
return _currentAssemblyLoadContext.LoadFromStream(assembly, assemblySymbols);
|
||||
}
|
||||
|
||||
public Assembly LoadCollectibleAssemblyFromPath(string path)
|
||||
{
|
||||
_currentAssemblyLoadContext ??= new UmbracoAssemblyLoadContext();
|
||||
return _currentAssemblyLoadContext.LoadFromAssemblyPath(path);
|
||||
}
|
||||
|
||||
public Assembly LoadModelsAssembly(string path)
|
||||
{
|
||||
Assembly assembly = LoadCollectibleAssemblyFromPath(path);
|
||||
_modelsAssemblyLocation = assembly.Location;
|
||||
return assembly;
|
||||
}
|
||||
}
|
||||
@@ -1,877 +0,0 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Extensions;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder;
|
||||
using Umbraco.Cms.Infrastructure.ModelsBuilder.Building;
|
||||
using Umbraco.Extensions;
|
||||
using File = System.IO.File;
|
||||
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto
|
||||
{
|
||||
internal sealed partial class InMemoryModelFactory : IAutoPublishedModelFactory, IRegisteredObject, IDisposable
|
||||
{
|
||||
private static readonly Regex s_usingRegex = GetUsingRegex();
|
||||
|
||||
[GeneratedRegex("^using(.*);", RegexOptions.Multiline | RegexOptions.Compiled)]
|
||||
private static partial Regex GetUsingRegex();
|
||||
|
||||
private static readonly Regex s_aattrRegex = GetAssemblyRegex();
|
||||
|
||||
[GeneratedRegex("^\\[assembly:(.*)\\]", RegexOptions.Multiline | RegexOptions.Compiled)]
|
||||
private static partial Regex GetAssemblyRegex();
|
||||
|
||||
private static readonly Regex s_assemblyVersionRegex = GetAssemblyVersionRegex();
|
||||
|
||||
[GeneratedRegex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled)]
|
||||
private static partial Regex GetAssemblyVersionRegex();
|
||||
|
||||
private static readonly FrozenSet<string> s_ourFiles = FrozenSet.Create("models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled");
|
||||
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
|
||||
private readonly IProfilingLogger _profilingLogger;
|
||||
private readonly ILogger<InMemoryModelFactory> _logger;
|
||||
private readonly FileSystemWatcher? _watcher;
|
||||
private readonly Lazy<UmbracoServices> _umbracoServices; // TODO: this is because of circular refs :(
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
private readonly IApplicationShutdownRegistry _hostingLifetime;
|
||||
private readonly ModelsGenerationError _errors;
|
||||
private readonly IPublishedValueFallback _publishedValueFallback;
|
||||
private readonly InMemoryAssemblyLoadContextManager _loadContextManager;
|
||||
private readonly RuntimeCompilationCacheBuster _runtimeCompilationCacheBuster;
|
||||
private readonly Lazy<string> _pureLiveDirectory = null!;
|
||||
private readonly int _debugLevel;
|
||||
private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
|
||||
private volatile bool _hasModels; // volatile 'cos reading outside lock
|
||||
private bool _pendingRebuild;
|
||||
private int _ver;
|
||||
private int? _skipver;
|
||||
private RoslynCompiler? _roslynCompiler;
|
||||
private ModelsBuilderSettings _config;
|
||||
private bool _disposedValue;
|
||||
|
||||
public InMemoryModelFactory(
|
||||
Lazy<UmbracoServices> umbracoServices,
|
||||
IProfilingLogger profilingLogger,
|
||||
ILogger<InMemoryModelFactory> logger,
|
||||
IOptionsMonitor<ModelsBuilderSettings> config,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHostEnvironment hostEnvironment,
|
||||
IApplicationShutdownRegistry hostingLifetime,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
InMemoryAssemblyLoadContextManager loadContextManager,
|
||||
RuntimeCompilationCacheBuster runtimeCompilationCacheBuster)
|
||||
{
|
||||
_umbracoServices = umbracoServices;
|
||||
_profilingLogger = profilingLogger;
|
||||
_logger = logger;
|
||||
_config = config.CurrentValue;
|
||||
_hostEnvironment = hostEnvironment;
|
||||
_hostingLifetime = hostingLifetime;
|
||||
_publishedValueFallback = publishedValueFallback;
|
||||
_loadContextManager = loadContextManager;
|
||||
_runtimeCompilationCacheBuster = runtimeCompilationCacheBuster;
|
||||
_errors = new ModelsGenerationError(config, _hostEnvironment);
|
||||
_ver = 1; // zero is for when we had no version
|
||||
_skipver = -1; // nothing to skip
|
||||
|
||||
if (!hostingEnvironment.IsHosted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
config.OnChange(x => _config = x);
|
||||
_pureLiveDirectory = new Lazy<string>(PureLiveDirectoryAbsolute);
|
||||
|
||||
if (!Directory.Exists(_pureLiveDirectory.Value))
|
||||
{
|
||||
Directory.CreateDirectory(_pureLiveDirectory.Value);
|
||||
}
|
||||
|
||||
// BEWARE! if the watcher is not properly released then for some reason the
|
||||
// BuildManager will start confusing types - using a 'registered object' here
|
||||
// though we should probably plug into Umbraco's MainDom - which is internal
|
||||
_hostingLifetime.RegisterObject(this);
|
||||
_watcher = new FileSystemWatcher(_pureLiveDirectory.Value);
|
||||
_watcher.Changed += WatcherOnChanged;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
|
||||
// get it here, this need to be fast
|
||||
_debugLevel = _config.DebugLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently loaded Live models assembly
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Can be null
|
||||
/// </remarks>
|
||||
public Assembly? CurrentModelsAssembly { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object SyncRoot { get; } = new();
|
||||
|
||||
private UmbracoServices UmbracoServices => _umbracoServices.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the RoslynCompiler
|
||||
/// </summary>
|
||||
private RoslynCompiler RoslynCompiler
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_roslynCompiler != null)
|
||||
{
|
||||
return _roslynCompiler;
|
||||
}
|
||||
|
||||
_roslynCompiler = new RoslynCompiler();
|
||||
return _roslynCompiler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Enabled => _config.ModelsMode == ModelsMode.InMemoryAuto;
|
||||
|
||||
public IPublishedElement CreateModel(IPublishedElement element)
|
||||
{
|
||||
// get models, rebuilding them if needed
|
||||
Dictionary<string, ModelInfo>? infos = EnsureModels().ModelInfos;
|
||||
if (infos == null)
|
||||
{
|
||||
return element;
|
||||
}
|
||||
|
||||
// be case-insensitive
|
||||
var contentTypeAlias = element.ContentType.Alias;
|
||||
|
||||
// lookup model constructor (else null)
|
||||
infos.TryGetValue(contentTypeAlias, out var info);
|
||||
|
||||
// create model
|
||||
return info is null || info.Ctor is null ? element : info.Ctor(element, _publishedValueFallback);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetModelType(string? alias)
|
||||
{
|
||||
Infos infos = EnsureModels();
|
||||
|
||||
// fail fast
|
||||
if (alias is null ||
|
||||
infos.ModelInfos is null ||
|
||||
!infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo) ||
|
||||
modelInfo.ModelType is null)
|
||||
{
|
||||
return typeof(IPublishedElement);
|
||||
}
|
||||
|
||||
return modelInfo.ModelType;
|
||||
}
|
||||
|
||||
// this runs only once the factory is ready
|
||||
// NOT when building models
|
||||
public Type MapModelType(Type type)
|
||||
{
|
||||
Infos infos = EnsureModels();
|
||||
return ModelType.Map(type, infos.ModelTypeMap);
|
||||
}
|
||||
|
||||
// this runs only once the factory is ready
|
||||
// NOT when building models
|
||||
public IList CreateModelList(string? alias)
|
||||
{
|
||||
Infos infos = EnsureModels();
|
||||
|
||||
// fail fast
|
||||
if (alias is null || infos.ModelInfos is null || !infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo))
|
||||
{
|
||||
return new List<IPublishedElement>();
|
||||
}
|
||||
|
||||
Func<IList>? ctor = modelInfo.ListCtor;
|
||||
if (ctor != null)
|
||||
{
|
||||
return ctor();
|
||||
}
|
||||
|
||||
if (modelInfo.ModelType is null)
|
||||
{
|
||||
return new List<IPublishedElement>();
|
||||
}
|
||||
|
||||
Type listType = typeof(List<>).MakeGenericType(modelInfo.ModelType);
|
||||
ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor<Func<IList>>(declaring: listType);
|
||||
|
||||
return ctor is null ? new List<IPublishedElement>() : ctor();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Reset()
|
||||
{
|
||||
if (Enabled)
|
||||
{
|
||||
ResetModels();
|
||||
}
|
||||
}
|
||||
|
||||
// tells the factory that it should build a new generation of models
|
||||
private void ResetModels()
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Resetting models.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_locker.EnterWriteLock();
|
||||
|
||||
_hasModels = false;
|
||||
_pendingRebuild = true;
|
||||
|
||||
if (!Directory.Exists(_pureLiveDirectory.Value))
|
||||
{
|
||||
Directory.CreateDirectory(_pureLiveDirectory.Value);
|
||||
}
|
||||
|
||||
// clear stuff
|
||||
var modelsHashFile = Path.Combine(_pureLiveDirectory.Value, "models.hash");
|
||||
var dllPathFile = Path.Combine(_pureLiveDirectory.Value, "all.dll.path");
|
||||
|
||||
if (File.Exists(dllPathFile))
|
||||
{
|
||||
File.Delete(dllPathFile);
|
||||
}
|
||||
|
||||
if (File.Exists(modelsHashFile))
|
||||
{
|
||||
File.Delete(modelsHashFile);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsWriteLockHeld)
|
||||
{
|
||||
_locker.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the factory is running with the lastest generation of models
|
||||
internal Infos EnsureModels()
|
||||
{
|
||||
if (_debugLevel > 0)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Ensuring models.");
|
||||
}
|
||||
}
|
||||
|
||||
// don't use an upgradeable lock here because only 1 thread at a time could enter it
|
||||
try
|
||||
{
|
||||
_locker.EnterReadLock();
|
||||
if (_hasModels)
|
||||
{
|
||||
return _infos;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsReadLockHeld)
|
||||
{
|
||||
_locker.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_locker.EnterUpgradeableReadLock();
|
||||
|
||||
if (_hasModels)
|
||||
{
|
||||
return _infos;
|
||||
}
|
||||
|
||||
_locker.EnterWriteLock();
|
||||
|
||||
// we don't have models,
|
||||
// either they haven't been loaded from the cache yet
|
||||
// or they have been reseted and are pending a rebuild
|
||||
using (!_profilingLogger.IsEnabled(Core.Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration<InMemoryModelFactory>("Get models.", "Got models."))
|
||||
{
|
||||
try
|
||||
{
|
||||
Assembly assembly = GetModelsAssembly(_pendingRebuild);
|
||||
|
||||
CurrentModelsAssembly = assembly;
|
||||
|
||||
/*
|
||||
* We used to use an event here, and a RefreshingRazorViewEngine to bust the caches,
|
||||
* this worked by essentially completely recreating the entire ViewEngine/ViewCompiler every time we generate models.
|
||||
* There was this note about first load:
|
||||
* NOTE: That on first load, if there is content, this will execute before the razor view engine
|
||||
* has loaded which means it hasn't yet bound to this event so there's no need to worry about if
|
||||
* it will be eagerly re-generated unnecessarily on first render. BUT we should be aware that if we
|
||||
* change this to use the event aggregator that will no longer be the case.
|
||||
*
|
||||
* Now we have our own ViewCompiler, and clear the caches more directly, however what the comment mentioned
|
||||
* is not really a big problem since this will execute before the razor view engine has loaded,
|
||||
* which means the cache will be empty already.
|
||||
*/
|
||||
_runtimeCompilationCacheBuster.BustCache();
|
||||
|
||||
IEnumerable<Type> types = assembly.ExportedTypes.Where(x => x.Inherits<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
|
||||
_infos = RegisterModels(types);
|
||||
_errors.Clear();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogError(e, "Failed to build models.");
|
||||
_logger.LogWarning("Running without models."); // be explicit
|
||||
_errors.Report("Failed to build InMemory models.", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CurrentModelsAssembly = null;
|
||||
_infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
|
||||
}
|
||||
}
|
||||
|
||||
// don't even try again
|
||||
_hasModels = true;
|
||||
}
|
||||
|
||||
return _infos;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_locker.IsWriteLockHeld)
|
||||
{
|
||||
_locker.ExitWriteLock();
|
||||
}
|
||||
|
||||
if (_locker.IsUpgradeableReadLockHeld)
|
||||
{
|
||||
_locker.ExitUpgradeableReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string PureLiveDirectoryAbsolute() => _hostEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempData + "/InMemoryAuto");
|
||||
|
||||
// This is NOT thread safe but it is only called from within a lock
|
||||
private Assembly ReloadAssembly(string pathToAssembly)
|
||||
{
|
||||
_loadContextManager.RenewAssemblyLoadContext();
|
||||
|
||||
// NOTE: We cannot use in-memory assemblies due to the way the razor engine works which must use
|
||||
// application parts in order to add references to it's own CSharpCompiler.
|
||||
// These parts must have real paths since that is how the references are loaded. In that
|
||||
// case we'll need to work on temp files so that the assembly isn't locked.
|
||||
|
||||
// Get a temp file path
|
||||
// NOTE: We cannot use Path.GetTempFileName(), see this issue:
|
||||
// https://github.com/dotnet/AspNetCore.Docs/issues/3589 which can cause issues, this is recommended instead
|
||||
var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
||||
File.Copy(pathToAssembly, tempFile, true);
|
||||
|
||||
// Load it in
|
||||
Assembly assembly = _loadContextManager.LoadModelsAssembly(tempFile);
|
||||
|
||||
return assembly;
|
||||
}
|
||||
|
||||
// This is NOT thread safe but it is only called from within a lock
|
||||
private Assembly GetModelsAssembly(bool forceRebuild)
|
||||
{
|
||||
if (!Directory.Exists(_pureLiveDirectory.Value))
|
||||
{
|
||||
Directory.CreateDirectory(_pureLiveDirectory.Value);
|
||||
}
|
||||
|
||||
IList<TypeModel> typeModels = UmbracoServices.GetAllTypes();
|
||||
var currentHash = TypeModelHasher.Hash(typeModels);
|
||||
var modelsHashFile = Path.Combine(_pureLiveDirectory.Value, "models.hash");
|
||||
var modelsSrcFile = Path.Combine(_pureLiveDirectory.Value, "models.generated.cs");
|
||||
var projFile = Path.Combine(_pureLiveDirectory.Value, "all.generated.cs");
|
||||
var dllPathFile = Path.Combine(_pureLiveDirectory.Value, "all.dll.path");
|
||||
|
||||
// caching the generated models speeds up booting
|
||||
// currentHash hashes both the types & the user's partials
|
||||
if (!forceRebuild)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Looking for cached models.");
|
||||
}
|
||||
if (File.Exists(modelsHashFile) && File.Exists(projFile))
|
||||
{
|
||||
var cachedHash = File.ReadAllText(modelsHashFile);
|
||||
if (currentHash != cachedHash)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Found obsolete cached models.");
|
||||
}
|
||||
forceRebuild = true;
|
||||
}
|
||||
|
||||
// else cachedHash matches currentHash, we can try to load an existing dll
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("Could not find cached models.");
|
||||
forceRebuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
Assembly assembly;
|
||||
if (!forceRebuild)
|
||||
{
|
||||
// try to load the dll directly (avoid rebuilding)
|
||||
//
|
||||
// ensure that the .dll file does not have a corresponding .dll.delete file
|
||||
// as that would mean the the .dll file is going to be deleted and should not
|
||||
// be re-used - that should not happen in theory, but better be safe
|
||||
if (File.Exists(dllPathFile))
|
||||
{
|
||||
var dllPath = File.ReadAllText(dllPathFile);
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Cached models dll at {dllPath}.",dllPath);
|
||||
}
|
||||
|
||||
if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete"))
|
||||
{
|
||||
assembly = ReloadAssembly(dllPath);
|
||||
|
||||
ModelsBuilderAssemblyAttribute? attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
|
||||
if (attr != null && attr.IsInMemory && attr.SourceHash == currentHash)
|
||||
{
|
||||
// if we were to resume at that revision, then _ver would keep increasing
|
||||
// and that is probably a bad idea - so, we'll always rebuild starting at
|
||||
// ver 1, but we remember we want to skip that one - so we never end up
|
||||
// with the "same but different" version of the assembly in memory
|
||||
_skipver = assembly.GetName().Version?.Revision;
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Loading cached models (dll).");
|
||||
}
|
||||
return assembly;
|
||||
}
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Cached models dll cannot be loaded (invalid assembly).");
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(dllPath))
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Cached models dll does not exist.");
|
||||
}
|
||||
}
|
||||
else if (File.Exists(dllPath + ".delete"))
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Cached models dll is marked for deletion.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Cached models dll cannot be loaded (why?).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// must reset the version in the file else it would keep growing
|
||||
// loading cached modules only happens when the app restarts
|
||||
var text = File.ReadAllText(projFile);
|
||||
Match match = s_assemblyVersionRegex.Match(text);
|
||||
if (match.Success)
|
||||
{
|
||||
text = text.Replace(match.Value, "AssemblyVersion(\"0.0.0." + _ver + "\")");
|
||||
File.WriteAllText(projFile, text);
|
||||
}
|
||||
|
||||
_ver++;
|
||||
try
|
||||
{
|
||||
var assemblyPath = GetOutputAssemblyPath(currentHash);
|
||||
RoslynCompiler.CompileToFile(projFile, assemblyPath);
|
||||
assembly = ReloadAssembly(assemblyPath);
|
||||
File.WriteAllText(dllPathFile, assembly.Location);
|
||||
File.WriteAllText(modelsHashFile, currentHash);
|
||||
TryDeleteUnusedAssemblies(dllPathFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
|
||||
throw;
|
||||
}
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Loading cached models (source).");
|
||||
}
|
||||
return assembly;
|
||||
}
|
||||
|
||||
// need to rebuild
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Rebuilding models.");
|
||||
}
|
||||
|
||||
// generate code, save
|
||||
var code = GenerateModelsCode(typeModels);
|
||||
|
||||
// add extra attributes,
|
||||
// IsLive=true helps identifying Assemblies that contain live models
|
||||
// AssemblyVersion is so that we have a different version for each rebuild
|
||||
var ver = _ver == _skipver ? ++_ver : _ver;
|
||||
_ver++;
|
||||
string mbAssemblyDirective = $@"[assembly:ModelsBuilderAssembly(IsInMemory = true, SourceHash = ""{currentHash}"")]
|
||||
[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]";
|
||||
code = code.Replace("//ASSATTR", mbAssemblyDirective);
|
||||
File.WriteAllText(modelsSrcFile, code);
|
||||
|
||||
// generate proj, save
|
||||
var projFiles = new Dictionary<string, string>
|
||||
{
|
||||
{ "models.generated.cs", code },
|
||||
};
|
||||
var proj = GenerateModelsProj(projFiles);
|
||||
File.WriteAllText(projFile, proj);
|
||||
|
||||
// compile and register
|
||||
try
|
||||
{
|
||||
var assemblyPath = GetOutputAssemblyPath(currentHash);
|
||||
RoslynCompiler.CompileToFile(projFile, assemblyPath);
|
||||
assembly = ReloadAssembly(assemblyPath);
|
||||
File.WriteAllText(dllPathFile, assemblyPath);
|
||||
File.WriteAllText(modelsHashFile, currentHash);
|
||||
TryDeleteUnusedAssemblies(dllPathFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile);
|
||||
throw;
|
||||
}
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Done rebuilding.");
|
||||
}
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private static void TryDeleteUnusedAssemblies(string dllPathFile)
|
||||
{
|
||||
if (File.Exists(dllPathFile))
|
||||
{
|
||||
var dllPath = File.ReadAllText(dllPathFile);
|
||||
DirectoryInfo? dirInfo = new DirectoryInfo(dllPath).Parent;
|
||||
IEnumerable<FileInfo>? files = dirInfo?.GetFiles().Where(f => f.FullName != dllPath);
|
||||
if (files is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (FileInfo file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file.FullName);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// The file is in use, we'll try again next time...
|
||||
// This shouldn't happen anymore.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetOutputAssemblyPath(string currentHash)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(Path.Combine(_pureLiveDirectory.Value, "Compiled"));
|
||||
if (!dirInfo.Exists)
|
||||
{
|
||||
Directory.CreateDirectory(dirInfo.FullName);
|
||||
}
|
||||
|
||||
return Path.Combine(dirInfo.FullName, $"generated.cs{currentHash}.dll");
|
||||
}
|
||||
|
||||
|
||||
private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile)
|
||||
{
|
||||
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Failed to compile.");
|
||||
}
|
||||
|
||||
// the dll file reference still points to the previous dll, which is obsolete
|
||||
// now and will be deleted by ASP.NET eventually, so better clear that reference.
|
||||
// also touch the proj file to force views to recompile - don't delete as it's
|
||||
// useful to have the source around for debugging.
|
||||
try
|
||||
{
|
||||
if (File.Exists(dllPathFile))
|
||||
{
|
||||
File.Delete(dllPathFile);
|
||||
}
|
||||
|
||||
if (File.Exists(modelsHashFile))
|
||||
{
|
||||
File.Delete(modelsHashFile);
|
||||
}
|
||||
|
||||
if (File.Exists(projFile))
|
||||
{
|
||||
File.SetLastWriteTime(projFile, DateTime.Now);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{ /* enough */
|
||||
}
|
||||
}
|
||||
|
||||
private static Infos RegisterModels(IEnumerable<Type> types)
|
||||
{
|
||||
Type[] ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) };
|
||||
var modelInfos = new Dictionary<string, ModelInfo>(StringComparer.InvariantCultureIgnoreCase);
|
||||
var map = new Dictionary<string, Type>();
|
||||
|
||||
foreach (Type type in types)
|
||||
{
|
||||
ConstructorInfo? constructor = null;
|
||||
Type? parameterType = null;
|
||||
|
||||
foreach (ConstructorInfo ctor in type.GetConstructors())
|
||||
{
|
||||
ParameterInfo[] parms = ctor.GetParameters();
|
||||
if (parms.Length == 2 && typeof(IPublishedElement).IsAssignableFrom(parms[0].ParameterType) && typeof(IPublishedValueFallback).IsAssignableFrom(parms[1].ParameterType))
|
||||
{
|
||||
if (constructor != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet.");
|
||||
}
|
||||
|
||||
constructor = ctor;
|
||||
parameterType = parms[0].ParameterType;
|
||||
}
|
||||
}
|
||||
|
||||
if (constructor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPropertySet.");
|
||||
}
|
||||
|
||||
PublishedModelAttribute? attribute = type.GetCustomAttribute<PublishedModelAttribute>(false);
|
||||
var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias;
|
||||
|
||||
if (modelInfos.TryGetValue(typeName, out var modelInfo))
|
||||
{
|
||||
throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType?.FullName} want to be a model type for content type with alias \"{typeName}\".");
|
||||
}
|
||||
|
||||
// TODO: use Core's ReflectionUtilities.EmitCtor !!
|
||||
// Yes .. DynamicMethod is uber slow
|
||||
// TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still?
|
||||
// See CtorInvokeBenchmarks
|
||||
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
|
||||
ILGenerator gen = meth.GetILGenerator();
|
||||
gen.Emit(OpCodes.Ldarg_0);
|
||||
gen.Emit(OpCodes.Ldarg_1);
|
||||
gen.Emit(OpCodes.Newobj, constructor);
|
||||
gen.Emit(OpCodes.Ret);
|
||||
var func = (Func<IPublishedElement, IPublishedValueFallback, IPublishedElement>)meth.CreateDelegate(typeof(Func<IPublishedElement, IPublishedValueFallback, IPublishedElement>));
|
||||
|
||||
modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type };
|
||||
map[typeName] = type;
|
||||
}
|
||||
|
||||
return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map };
|
||||
}
|
||||
|
||||
private string GenerateModelsCode(IList<TypeModel> typeModels)
|
||||
{
|
||||
if (!Directory.Exists(_pureLiveDirectory.Value))
|
||||
{
|
||||
Directory.CreateDirectory(_pureLiveDirectory.Value);
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(_pureLiveDirectory.Value, "*.generated.cs"))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
var builder = new TextBuilder(_config, typeModels);
|
||||
|
||||
var codeBuilder = new StringBuilder();
|
||||
builder.Generate(codeBuilder, builder.GetModelsToGenerate());
|
||||
var code = codeBuilder.ToString();
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
private static string GenerateModelsProj(IDictionary<string, string> files)
|
||||
{
|
||||
// ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj
|
||||
// trying to keep things simple for the time being, just write everything to one big file
|
||||
|
||||
// group all 'using' at the top of the file (else fails)
|
||||
var usings = new List<string>();
|
||||
foreach (string k in files.Keys.ToList())
|
||||
{
|
||||
files[k] = s_usingRegex.Replace(files[k], m =>
|
||||
{
|
||||
usings.Add(m.Groups[1].Value);
|
||||
return string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
// group all '[assembly:...]' at the top of the file (else fails)
|
||||
var aattrs = new List<string>();
|
||||
foreach (string k in files.Keys.ToList())
|
||||
{
|
||||
files[k] = s_aattrRegex.Replace(files[k], m =>
|
||||
{
|
||||
aattrs.Add(m.Groups[1].Value);
|
||||
return string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
var text = new StringBuilder();
|
||||
foreach (var u in usings.Distinct())
|
||||
{
|
||||
text.Append("using ");
|
||||
text.Append(u);
|
||||
text.Append(";\r\n");
|
||||
}
|
||||
|
||||
foreach (var a in aattrs)
|
||||
{
|
||||
text.Append("[assembly:");
|
||||
text.Append(a);
|
||||
text.Append("]\r\n");
|
||||
}
|
||||
|
||||
text.Append("\r\n\r\n");
|
||||
foreach (KeyValuePair<string, string> f in files)
|
||||
{
|
||||
text.Append("// FILE: ");
|
||||
text.Append(f.Key);
|
||||
text.Append("\r\n\r\n");
|
||||
text.Append(f.Value);
|
||||
text.Append("\r\n\r\n\r\n");
|
||||
}
|
||||
|
||||
text.Append("// EOF\r\n");
|
||||
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
private void WatcherOnChanged(object sender, FileSystemEventArgs args)
|
||||
{
|
||||
var changed = args.Name;
|
||||
|
||||
// don't reset when our files change because we are building!
|
||||
//
|
||||
// comment it out, and always ignore our files, because it seems that some
|
||||
// race conditions can occur on slow Cloud filesystems and then we keep
|
||||
// rebuilding
|
||||
|
||||
// if (_building && OurFiles.Contains(changed))
|
||||
// {
|
||||
// //_logger.LogInformation<InMemoryModelFactory>("Ignoring files self-changes.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// always ignore our own file changes
|
||||
if (changed != null && s_ourFiles.Contains(changed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Detected files changes.");
|
||||
|
||||
// don't reset while being locked
|
||||
lock (SyncRoot)
|
||||
{
|
||||
ResetModels();
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop(bool immediate)
|
||||
{
|
||||
Dispose();
|
||||
|
||||
_hostingLifetime.UnregisterObject(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_watcher is not null)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
_watcher.Dispose();
|
||||
}
|
||||
|
||||
_locker.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
}
|
||||
|
||||
internal sealed class Infos
|
||||
{
|
||||
public Dictionary<string, Type>? ModelTypeMap { get; set; }
|
||||
|
||||
public Dictionary<string, ModelInfo>? ModelInfos { get; set; }
|
||||
}
|
||||
|
||||
internal sealed class ModelInfo
|
||||
{
|
||||
public Type? ParameterType { get; set; }
|
||||
|
||||
public Func<IPublishedElement, IPublishedValueFallback, IPublishedElement>? Ctor { get; set; }
|
||||
|
||||
public Type? ModelType { get; set; }
|
||||
|
||||
public Func<IList>? ListCtor { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
internal sealed class RuntimeCompilationCacheBuster
|
||||
{
|
||||
private readonly IViewCompilerProvider _viewCompilerProvider;
|
||||
private readonly IRazorViewEngine _razorViewEngine;
|
||||
|
||||
public RuntimeCompilationCacheBuster(
|
||||
IViewCompilerProvider viewCompilerProvider,
|
||||
IRazorViewEngine razorViewEngine)
|
||||
{
|
||||
_viewCompilerProvider = viewCompilerProvider;
|
||||
_razorViewEngine = razorViewEngine;
|
||||
}
|
||||
|
||||
private RazorViewEngine ViewEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_razorViewEngine is RazorViewEngine typedViewEngine)
|
||||
{
|
||||
return typedViewEngine;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"Unable to resolve RazorViewEngine, this means we can't clear the cache and views won't render properly");
|
||||
}
|
||||
}
|
||||
|
||||
private CollectibleRuntimeViewCompiler ViewCompiler
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_viewCompilerProvider.GetCompiler() is CollectibleRuntimeViewCompiler collectibleCompiler)
|
||||
{
|
||||
return collectibleCompiler;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to resolve CollectibleRuntimeViewCompiler, and is unable to clear the cache");
|
||||
}
|
||||
}
|
||||
|
||||
public void BustCache()
|
||||
{
|
||||
ViewCompiler.ClearCache();
|
||||
Action<RazorViewEngine>? clearCacheMethod = ReflectionUtilities.EmitMethod<Action<RazorViewEngine>>("ClearCache");
|
||||
clearCacheMethod?.Invoke(ViewEngine);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
internal sealed class UmbracoAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoAssemblyLoadContext" /> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Collectible AssemblyLoadContext used to load in the compiled generated models.
|
||||
/// Must be a collectible assembly in order to be able to be unloaded.
|
||||
/// </remarks>
|
||||
public UmbracoAssemblyLoadContext()
|
||||
: base(true)
|
||||
{
|
||||
}
|
||||
|
||||
// we never load anything directly by assembly name. This method will never be called
|
||||
protected override Assembly? Load(AssemblyName assemblyName) => null;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Microsoft.AspNetCore.Diagnostics;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
internal sealed class UmbracoCompilationException : Exception, ICompilationException
|
||||
{
|
||||
public IEnumerable<CompilationFailure?>? CompilationFailures { get; init; }
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
using System.Reflection.PortableExecutable;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
|
||||
/*
|
||||
* This is a clone'n'own of Microsofts RazorReferenceManager, please check if there's been any updates to the file on their end
|
||||
* before trying to mock about fixing issues:
|
||||
* https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs
|
||||
*
|
||||
* This class is used by the the ViewCompiler (CollectibleRuntimeViewCompiler) to find the required references when compiling views.
|
||||
*/
|
||||
internal sealed class UmbracoRazorReferenceManager
|
||||
{
|
||||
private readonly ApplicationPartManager _partManager;
|
||||
private readonly MvcRazorRuntimeCompilationOptions _options;
|
||||
private object _compilationReferencesLock = new object();
|
||||
private bool _compilationReferencesInitialized;
|
||||
private IReadOnlyList<MetadataReference>? _compilationReferences;
|
||||
|
||||
public UmbracoRazorReferenceManager(
|
||||
ApplicationPartManager partManager,
|
||||
IOptions<MvcRazorRuntimeCompilationOptions> options)
|
||||
{
|
||||
_partManager = partManager;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<MetadataReference> CompilationReferences
|
||||
{
|
||||
get
|
||||
{
|
||||
return LazyInitializer.EnsureInitialized(
|
||||
ref _compilationReferences,
|
||||
ref _compilationReferencesInitialized,
|
||||
ref _compilationReferencesLock,
|
||||
GetCompilationReferences)!;
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<MetadataReference> GetCompilationReferences()
|
||||
{
|
||||
var referencePaths = GetReferencePaths();
|
||||
|
||||
return referencePaths
|
||||
.Select(CreateMetadataReference)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// For unit testing
|
||||
internal IEnumerable<string> GetReferencePaths()
|
||||
{
|
||||
var referencePaths = new List<string>(_options.AdditionalReferencePaths.Count);
|
||||
|
||||
foreach (var part in _partManager.ApplicationParts)
|
||||
{
|
||||
if (part is ICompilationReferencesProvider compilationReferenceProvider)
|
||||
{
|
||||
referencePaths.AddRange(compilationReferenceProvider.GetReferencePaths());
|
||||
}
|
||||
else if (part is AssemblyPart assemblyPart)
|
||||
{
|
||||
referencePaths.AddRange(assemblyPart.GetReferencePaths());
|
||||
}
|
||||
}
|
||||
|
||||
referencePaths.AddRange(_options.AdditionalReferencePaths);
|
||||
|
||||
return referencePaths;
|
||||
}
|
||||
|
||||
private static MetadataReference CreateMetadataReference(string path)
|
||||
{
|
||||
using (var stream = File.OpenRead(path))
|
||||
{
|
||||
var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata);
|
||||
var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata);
|
||||
|
||||
return assemblyMetadata.GetReference(filePath: path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Exceptions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ModelsBuilder.InMemoryAuto;
|
||||
|
||||
internal sealed class UmbracoViewCompilerProvider : IViewCompilerProvider
|
||||
{
|
||||
private readonly RazorProjectEngine _razorProjectEngine;
|
||||
private readonly UmbracoRazorReferenceManager _umbracoRazorReferenceManager;
|
||||
private readonly CompilationOptionsProvider _compilationOptionsProvider;
|
||||
private readonly InMemoryAssemblyLoadContextManager _loadContextManager;
|
||||
|
||||
private readonly ApplicationPartManager _applicationPartManager;
|
||||
|
||||
private readonly ILogger<CollectibleRuntimeViewCompiler> _logger;
|
||||
private readonly Func<IViewCompiler> _createCompiler;
|
||||
|
||||
private object _initializeLock = new object();
|
||||
private bool _initialized;
|
||||
private IViewCompiler? _compiler;
|
||||
private readonly MvcRazorRuntimeCompilationOptions _options;
|
||||
|
||||
|
||||
public UmbracoViewCompilerProvider(
|
||||
ApplicationPartManager applicationPartManager,
|
||||
RazorProjectEngine razorProjectEngine,
|
||||
ILoggerFactory loggerFactory,
|
||||
IOptions<MvcRazorRuntimeCompilationOptions> options,
|
||||
UmbracoRazorReferenceManager umbracoRazorReferenceManager,
|
||||
CompilationOptionsProvider compilationOptionsProvider,
|
||||
InMemoryAssemblyLoadContextManager loadContextManager)
|
||||
{
|
||||
_applicationPartManager = applicationPartManager;
|
||||
_razorProjectEngine = razorProjectEngine;
|
||||
_umbracoRazorReferenceManager = umbracoRazorReferenceManager;
|
||||
_compilationOptionsProvider = compilationOptionsProvider;
|
||||
_loadContextManager = loadContextManager;
|
||||
_options = options.Value;
|
||||
|
||||
_logger = loggerFactory.CreateLogger<CollectibleRuntimeViewCompiler>();
|
||||
_createCompiler = CreateCompiler;
|
||||
}
|
||||
|
||||
public IViewCompiler GetCompiler()
|
||||
{
|
||||
return LazyInitializer.EnsureInitialized(
|
||||
ref _compiler,
|
||||
ref _initialized,
|
||||
ref _initializeLock,
|
||||
_createCompiler)!;
|
||||
}
|
||||
|
||||
private IViewCompiler CreateCompiler()
|
||||
{
|
||||
var feature = new ViewsFeature();
|
||||
_applicationPartManager.PopulateFeature(feature);
|
||||
|
||||
return new CollectibleRuntimeViewCompiler(
|
||||
GetCompositeFileProvider(_options),
|
||||
_razorProjectEngine,
|
||||
feature.ViewDescriptors,
|
||||
_logger,
|
||||
_umbracoRazorReferenceManager,
|
||||
_compilationOptionsProvider,
|
||||
_loadContextManager);
|
||||
}
|
||||
|
||||
private static IFileProvider GetCompositeFileProvider(MvcRazorRuntimeCompilationOptions options)
|
||||
{
|
||||
IList<IFileProvider> fileProviders = options.FileProviders;
|
||||
if (fileProviders.Count == 0)
|
||||
{
|
||||
throw new PanicException();
|
||||
}
|
||||
else if (fileProviders.Count == 1)
|
||||
{
|
||||
return fileProviders[0];
|
||||
}
|
||||
|
||||
return new CompositeFileProvider(fileProviders);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder;
|
||||
/// </summary>
|
||||
internal sealed class ModelsBuilderNotificationHandler :
|
||||
INotificationHandler<ServerVariablesParsingNotification>,
|
||||
INotificationHandler<ModelBindingErrorNotification>,
|
||||
INotificationHandler<TemplateSavingNotification>
|
||||
{
|
||||
private readonly ModelsBuilderSettings _config;
|
||||
@@ -37,62 +36,6 @@ internal sealed class ModelsBuilderNotificationHandler :
|
||||
_defaultViewContentProvider = defaultViewContentProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles when a model binding error occurs
|
||||
/// </summary>
|
||||
public void Handle(ModelBindingErrorNotification notification)
|
||||
{
|
||||
ModelsBuilderAssemblyAttribute? sourceAttr =
|
||||
notification.SourceType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
|
||||
ModelsBuilderAssemblyAttribute? modelAttr =
|
||||
notification.ModelType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
|
||||
|
||||
// if source or model is not a ModelsBuider type...
|
||||
if (sourceAttr == null || modelAttr == null)
|
||||
{
|
||||
// if neither are ModelsBuilder types, give up entirely
|
||||
if (sourceAttr == null && modelAttr == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// else report, but better not restart (loops?)
|
||||
notification.Message.Append(" The ");
|
||||
notification.Message.Append(sourceAttr == null ? "view model" : "source");
|
||||
notification.Message.Append(" is a ModelsBuilder type, but the ");
|
||||
notification.Message.Append(sourceAttr != null ? "view model" : "source");
|
||||
notification.Message.Append(" is not. The application is in an unstable state and should be restarted.");
|
||||
return;
|
||||
}
|
||||
|
||||
// both are ModelsBuilder types
|
||||
var pureSource = sourceAttr.IsInMemory;
|
||||
var pureModel = modelAttr.IsInMemory;
|
||||
|
||||
if (sourceAttr.IsInMemory || modelAttr.IsInMemory)
|
||||
{
|
||||
if (pureSource == false || pureModel == false)
|
||||
{
|
||||
// only one is pure - report, but better not restart (loops?)
|
||||
notification.Message.Append(pureSource
|
||||
? " The content model is in memory generated, but the view model is not."
|
||||
: " The view model is in memory generated, but the content model is not.");
|
||||
notification.Message.Append(" The application is in an unstable state and should be restarted.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// both are pure - report, and if different versions, restart
|
||||
// if same version... makes no sense... and better not restart (loops?)
|
||||
Version? sourceVersion = notification.SourceType.Assembly.GetName().Version;
|
||||
Version? modelVersion = notification.ModelType.Assembly.GetName().Version;
|
||||
notification.Message.Append(" Both view and content models are in memory generated, with ");
|
||||
notification.Message.Append(sourceVersion == modelVersion
|
||||
? "same version. The application is in an unstable state and should be restarted."
|
||||
: "different versions. The application is in an unstable state and should be restarted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the <see cref="ServerVariablesParsingNotification" /> notification to add custom urls and MB mode
|
||||
/// </summary>
|
||||
@@ -135,7 +78,7 @@ internal sealed class ModelsBuilderNotificationHandler :
|
||||
/// </summary>
|
||||
public void Handle(TemplateSavingNotification notification)
|
||||
{
|
||||
if (_config.ModelsMode == ModelsMode.Nothing)
|
||||
if (_config.ModelsMode == Core.Constants.ModelsBuilder.ModelsModes.Nothing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -181,7 +124,7 @@ internal sealed class ModelsBuilderNotificationHandler :
|
||||
|
||||
private Dictionary<string, object> GetModelsBuilderSettings()
|
||||
{
|
||||
var settings = new Dictionary<string, object> { { "mode", _config.ModelsMode.ToString() } };
|
||||
var settings = new Dictionary<string, object> { { "mode", _config.ModelsMode } };
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<Description>Contains the web assembly needed to run Umbraco CMS.</Description>
|
||||
<RootNamespace>Umbraco.Cms.Web.Common</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
TODO: Fix and remove overrides:
|
||||
@@ -21,7 +21,7 @@
|
||||
-->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors),SA1117,SA1401,SA1134,ASP0019,CS0618,SYSLIB0051,IDE0040,SA1400,SA1405,CS0419,CS1574,SA1649</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
@@ -30,7 +30,6 @@
|
||||
<PackageReference Include="Asp.Versioning.Mvc" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
|
||||
<PackageReference Include="Dazinator.Extensions.FileProviders" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" />
|
||||
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user