From 7f1e7114818490ae3e8c9e64030be19b6cc236b8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 14 Jan 2021 23:14:35 +1100 Subject: [PATCH] committing changes, notes, etc... so far --- .../ILivePublishedModelFactory.cs | 11 +- .../PublishedModelFactoryExtensions.cs | 29 +-- src/Umbraco.Core/Runtime/MainDom.cs | 3 +- .../UmbracoBuilderExtensions.cs | 169 ++++++++++++++++++ ...lsBuilderRazorRuntimeCompilationOptions.cs | 82 +++++++++ .../PureLiveModelFactory.cs | 144 ++++++++++----- .../RoslynCompiler.cs | 20 ++- .../Umbraco.ModelsBuilder.Embedded.csproj | 1 + .../UmbracoServices.cs | 3 +- .../UmbracoBuilderExtensions.cs | 4 +- 10 files changed, 369 insertions(+), 97 deletions(-) create mode 100644 src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderRazorRuntimeCompilationOptions.cs diff --git a/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs index 913a2311a4..091893fb72 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.Models.PublishedContent +namespace Umbraco.Core.Models.PublishedContent { /// @@ -11,15 +11,6 @@ /// object SyncRoot { get; } - /// - /// Refreshes the factory. - /// - /// - /// This will typically re-compiled models/classes into a new DLL that are used to populate the cache. - /// This is called prior to refreshing the cache. - /// - void Refresh(); - /// /// Tells the factory that it should build a new generation of models /// diff --git a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs index a877d6a7f5..db18bfd8a9 100644 --- a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs +++ b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using Umbraco.Core.Models.PublishedContent; @@ -13,48 +13,25 @@ namespace Umbraco.Core /// /// Returns true if the current is an implementation of /// - /// - /// public static bool IsLiveFactory(this IPublishedModelFactory factory) => factory is ILivePublishedModelFactory; /// /// Returns true if the current is an implementation of and is enabled /// - /// - /// public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory) { if (factory is ILivePublishedModelFactory liveFactory) + { return liveFactory.Enabled; + } // if it's not ILivePublishedModelFactory we can't determine if it's enabled or not so return true return true; } - [Obsolete("This method is no longer used or necessary and will be removed from future")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static void WithSafeLiveFactory(this IPublishedModelFactory factory, Action action) - { - if (factory is ILivePublishedModelFactory liveFactory) - { - lock (liveFactory.SyncRoot) - { - //Call refresh on the live factory to re-compile the models - liveFactory.Refresh(); - action(); - } - } - else - { - action(); - } - } - /// /// Sets a flag to reset the ModelsBuilder models if the is /// - /// - /// /// /// This does not recompile the pure live models, only sets a flag to tell models builder to recompile when they are requested. /// diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index f1f6ee3afc..c378f4a58b 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -54,6 +54,7 @@ namespace Umbraco.Core.Runtime #endregion + /// public bool Acquire(IApplicationShutdownRegistry hostingEnvironment) { _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs index 85f15942dc..bb0b966195 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,6 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Core.Composing; @@ -22,6 +26,7 @@ namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection /// public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder) { + builder.AddRazorProjectEngine(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddUnique(); @@ -77,5 +82,169 @@ namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection builder.Services.AddSingleton(); return builder; } + + private static IUmbracoBuilder AddRazorProjectEngine(this IUmbracoBuilder builder) + { + // TODO: This is super nasty, but we can at least tinker with this for now + // this pre-builds the container just so we can extract the default RazorProjectEngine + // in order to extract out all features and re-add them to ours + + // Since we cannot construct the razor engine like netcore does: + // https://github.com/dotnet/aspnetcore/blob/336e05577cd8bec2000ffcada926189199e4cef0/src/Mvc/Mvc.Razor.RuntimeCompilation/src/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs#L86 + // because many things are internal we need to resort to this which is to get the default RazorProjectEngine + // that is nornally created and use that to create our custom one while ensuring all of the razor features + // that we can't really add ourselves are there. + // Pretty much all methods, even thnigs like SetCSharpLanguageVersion are actually adding razor features. + + //var internalServicesBuilder = new ServiceCollection(); + //internalServicesBuilder.AddControllersWithViews().AddRazorRuntimeCompilation(); + //var internalServices = internalServicesBuilder.BuildServiceProvider(); + //var defaultRazorProjectEngine = internalServices.GetRequiredService(); + + ServiceProvider internalServices = builder.Services.BuildServiceProvider(); + RazorProjectEngine defaultRazorProjectEngine = internalServices.GetRequiredService(); + + builder.Services.AddSingleton(s => + { + RazorProjectFileSystem fileSystem = s.GetRequiredService(); + + // Create the project engine + var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder => + { + // replace all features with the defaults + builder.Features.Clear(); + + foreach (IRazorEngineFeature f in defaultRazorProjectEngine.EngineFeatures) + { + builder.Features.Add(f); + } + + foreach (IRazorProjectEngineFeature f in defaultRazorProjectEngine.ProjectFeatures) + { + builder.Features.Add(f); + } + + // The razor engine only supports one instance of IMetadataReferenceFeature + // so we need to jump through some hoops to allow multiple by using a wrapper. + // so get the current ones, remove them from the list, create a wrapper of them and + // our custom one and then add it back. + var metadataReferenceFeatures = builder.Features.OfType().ToList(); + foreach (IMetadataReferenceFeature m in metadataReferenceFeatures) + { + builder.Features.Remove(m); + } + + // add our custom one to the list + metadataReferenceFeatures.Add(new PureLiveMetadataReferenceFeature(s.GetRequiredService())); + + // now add them to our wrapper and back into the features + builder.Features.Add(new MetadataReferenceFeatureWrapper(metadataReferenceFeatures)); + + //RazorExtensions.Register(builder); + + //// Roslyn + TagHelpers infrastructure + //// TODO: These are internal... + //var referenceManager = s.GetRequiredService(); + //builder.Features.Add(new LazyMetadataReferenceFeature(referenceManager)); + + //builder.Features.Add(new CompilationTagHelperFeature()); + + //// TagHelperDescriptorProviders (actually do tag helper discovery) + //builder.Features.Add(new DefaultTagHelperDescriptorProvider()); + //builder.Features.Add(new ViewComponentTagHelperDescriptorProvider()); + //builder.SetCSharpLanguageVersion(csharpCompiler.ParseOptions.LanguageVersion); + }); + + return projectEngine; + }); + + return builder; + } + } + + /// + /// Wraps multiple + /// + /// + /// + /// This is required because the razor engine only supports a single IMetadataReferenceFeature but their APIs don't state this, + /// this is purely down to them doing this 'First' call: https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Razor/Microsoft.CodeAnalysis.Razor/src/CompilationTagHelperFeature.cs#L37 + /// So in order to have multiple, we need to have a wrapper. + /// + /// + public class MetadataReferenceFeatureWrapper : IMetadataReferenceFeature + { + private readonly IReadOnlyList _metadataReferenceFeatures; + private RazorEngine _engine; + + /// + /// Initializes a new instance of the class. + /// + public MetadataReferenceFeatureWrapper(IEnumerable metadataReferenceFeatures) + => _metadataReferenceFeatures = metadataReferenceFeatures.ToList(); + + /// + public IReadOnlyList References + => _metadataReferenceFeatures.SelectMany(x => x.References).ToList(); + + /// + public RazorEngine Engine + { + get => _engine; + set + { + _engine = value; + foreach (IMetadataReferenceFeature feature in _metadataReferenceFeatures) + { + feature.Engine = value; + } + } + } + } + + /// + /// A custom that will dynamically resolve a reference for razor based on the current PureLive assembly. + /// + /// + /// + /// The default implementation of IMetadataReferenceFeature is https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/LazyMetadataReferenceFeature.cs + /// which uses a ReferenceManager https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs + /// to resolve it's references. This is done using ApplicationParts which would be nice and simple to use if we could, but the Razor engine ONLY works + /// with application part assemblies that have physical files with physical paths. We don't want to load in our PureLive assemblies on physical paths because + /// those files will be locked. Instead we load them in via bytes but this is not supported and we'll get an exception if we add them to application parts. + /// The other problem with LazyMetadataReferenceFeature is that it doesn't support dynamic assemblies, it will just check what in application parts once and + /// that's it which will not work for us in Pure Live. + /// + /// + internal class PureLiveMetadataReferenceFeature : IMetadataReferenceFeature + { + // TODO: Even though I was hoping this would work and this does allow you to return a metadata reference dynamically at runtime, it doesn't make any + // difference because the CSharpCompiler for razor only loads in it's references one time based on the initial reference checks: + // https://github.com/dotnet/aspnetcore/blob/100ab02ea0214d49535fa56f33a77acd61fe039c/src/Mvc/Mvc.Razor.RuntimeCompilation/src/CSharpCompiler.cs#L84 + // Since ReferenceManager resolves them once lazily and that's it. + + private readonly PureLiveModelFactory _pureLiveModelFactory; + + public PureLiveMetadataReferenceFeature(PureLiveModelFactory pureLiveModelFactory) => _pureLiveModelFactory = pureLiveModelFactory; + + /// + public IReadOnlyList References + { + get + { + // TODO: This won't really work based on how the CSharp compiler works + //if (_pureLiveModelFactory.CurrentModelsMetadataReference != null) + //{ + // //var reference = MetadataReference.CreateFromStream(null); + // //reference. + // return new[] { _pureLiveModelFactory.CurrentModelsMetadataReference }; + //} + + return Array.Empty(); + } + } + + /// + public RazorEngine Engine { get; set; } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderRazorRuntimeCompilationOptions.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderRazorRuntimeCompilationOptions.cs new file mode 100644 index 0000000000..1e7eb7bf54 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderRazorRuntimeCompilationOptions.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; + +namespace Umbraco.ModelsBuilder.Embedded +{ + internal class NonRecursivePhysicalFileProvider : PhysicalFileProvider, IFileProvider + { + private static readonly char[] s_pathSeparators = new char[2] + { + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + }; + + public NonRecursivePhysicalFileProvider(string root) + : base(root) + { + } + + IDirectoryContents IFileProvider.GetDirectoryContents(string subpath) => IsRoot(subpath) ? GetDirectoryContents(subpath) : null; + + IFileInfo IFileProvider.GetFileInfo(string subpath) => IsRoot(subpath) ? GetFileInfo(subpath) : null; + + IChangeToken IFileProvider.Watch(string filter) => IsRoot(filter) ? Watch(filter) : NullChangeToken.Singleton; + + private bool IsRoot(string path) => !s_pathSeparators.Any(x => path.Contains(x)); + } + + public class ModelsBuilderRazorRuntimeCompilationOptions : IConfigureOptions + { + private readonly ModelsBuilderSettings _config; + private readonly IHostingEnvironment _hostingEnvironment; + + /// + /// Initializes a new instance of the class. + /// + public ModelsBuilderRazorRuntimeCompilationOptions( + IOptions config, + IHostingEnvironment hostingEnvironment) + { + _config = config.Value; + _hostingEnvironment = hostingEnvironment; + } + + /// + public void Configure(MvcRazorRuntimeCompilationOptions options) + { + //RazorProjectEngine.Create() + + // TODO: Not sure this is going to be possible :/ + // See https://stackoverflow.com/questions/58685966/adding-assemblies-types-to-be-made-available-to-razor-page-at-runtime + // See https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/MvcRazorRuntimeCompilationOptions.cs + // See https://github.com/dotnet/aspnetcore/blob/b795ac3546eb3e2f47a01a64feb3020794ca33bb/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs + // See https://github.com/dotnet/aspnetcore/blob/114f0f6d1ef1d777fb93d90c87ac506027c55ea0/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RuntimeViewCompiler.cs#L26 + + // This is where the RazorProjectEngine gets created + // https://github.com/dotnet/aspnetcore/blob/336e05577cd8bec2000ffcada926189199e4cef0/src/Mvc/Mvc.Razor.RuntimeCompilation/src/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs#L86 + // In theory, it seems like + + //MetadataReference ref; + //var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + + // From what I can tell, we can specify a file provider here for all razor files + // that need to be watched which will recompiled when they are changed. + + // TODO: Should be constants and or shared with our RazorViewEngineOptions classes + options.FileProviders.Add(new NonRecursivePhysicalFileProvider(_hostingEnvironment.MapPathContentRoot("~/Views"))); + options.FileProviders.Add(new PhysicalFileProvider(_hostingEnvironment.MapPathContentRoot("~/Views/Partials"))); + options.FileProviders.Add(new PhysicalFileProvider(_hostingEnvironment.MapPathContentRoot("~/Views/MacroPartials"))); + options.FileProviders.Add(new PhysicalFileProvider(_hostingEnvironment.MapPathContentRoot("~/App_Plugins"))); + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index dddef525ff..6433accaef 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -5,9 +5,13 @@ using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; +using System.Reflection.PortableExecutable; +using System.Runtime.Loader; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -34,7 +38,7 @@ namespace Umbraco.ModelsBuilder.Embedded private int _skipver; private readonly int _debugLevel; private RoslynCompiler _roslynCompiler; - private UmbracoAssemblyLoadContext _currentAssemblyLoadContext; + //private UmbracoAssemblyLoadContext _currentAssemblyLoadContext; private readonly Lazy _umbracoServices; // fixme: this is because of circular refs :( private static readonly Regex s_assemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); private static readonly string[] s_ourFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" }; @@ -43,6 +47,7 @@ namespace Umbraco.ModelsBuilder.Embedded private readonly IApplicationShutdownRegistry _hostingLifetime; private readonly ModelsGenerationError _errors; private readonly IPublishedValueFallback _publishedValueFallback; + private readonly ApplicationPartManager _applicationPartManager; private static readonly Regex s_usingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); private static readonly Regex s_aattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); @@ -53,7 +58,8 @@ namespace Umbraco.ModelsBuilder.Embedded IOptions config, IHostingEnvironment hostingEnvironment, IApplicationShutdownRegistry hostingLifetime, - IPublishedValueFallback publishedValueFallback) + IPublishedValueFallback publishedValueFallback, + ApplicationPartManager applicationPartManager) { _umbracoServices = umbracoServices; _profilingLogger = profilingLogger; @@ -62,6 +68,7 @@ namespace Umbraco.ModelsBuilder.Embedded _hostingEnvironment = hostingEnvironment; _hostingLifetime = hostingLifetime; _publishedValueFallback = publishedValueFallback; + _applicationPartManager = applicationPartManager; _errors = new ModelsGenerationError(config, _hostingEnvironment); _ver = 1; // zero is for when we had no version _skipver = -1; // nothing to skip @@ -90,10 +97,22 @@ namespace Umbraco.ModelsBuilder.Embedded private UmbracoServices UmbracoServices => _umbracoServices.Value; + /// + /// Gets the currently loaded pure live models assembly + /// + /// + /// Can be null + /// + public Assembly CurrentModelsAssembly { get; private set; } + + public MetadataReference CurrentModelsMetadataReference { get; private set; } + /// public object SyncRoot { get; } = new object(); - // gets the RoslynCompiler + /// + /// Gets the RoslynCompiler + /// private RoslynCompiler RoslynCompiler { get @@ -111,13 +130,6 @@ namespace Umbraco.ModelsBuilder.Embedded /// public bool Enabled => _config.Enable; - /// - public void Refresh() - { - ResetModels(); - EnsureModels(); - } - public IPublishedElement CreateModel(IPublishedElement element) { // get models, rebuilding them if needed @@ -263,6 +275,9 @@ namespace Umbraco.ModelsBuilder.Embedded var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); + // TODO: Remove the old application part + var parts = _applicationPartManager.ApplicationParts; + if (File.Exists(dllPathFile)) { File.Delete(dllPathFile); @@ -307,15 +322,8 @@ namespace Umbraco.ModelsBuilder.Embedded } } - var roslynLocked = false; try { - // TODO: DO NOT LOCK ON RoslynCompiler - - // always take the BuildManager lock *before* taking the _locker lock - // to avoid possible deadlock situations (see notes above) - Monitor.Enter(RoslynCompiler, ref roslynLocked); - _locker.EnterUpgradeableReadLock(); if (_hasModels) @@ -328,17 +336,18 @@ namespace Umbraco.ModelsBuilder.Embedded // 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.DebugDuration("Get models.", "Got models.")) { try { - Assembly assembly = GetModelsAssembly(_pendingRebuild); + // TODO: We may have to copy this to a temp place? + // string assemblyName = Path.GetRandomFileName(); + + Assembly assembly = GetModelsAssembly(_pendingRebuild, out MetadataReference metadataReference); + + CurrentModelsAssembly = assembly; + CurrentModelsMetadataReference = metadataReference; - // the one below can be used to simulate an issue with BuildManager, ie it will register - // the models with the factory but NOT with the BuildManager, which will not recompile views. - // this is for U4-8043 which is an obvious issue but I cannot replicate - // _modelsAssembly = _modelsAssembly ?? assembly; IEnumerable types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); _infos = RegisterModels(types); _errors.Clear(); @@ -353,6 +362,7 @@ namespace Umbraco.ModelsBuilder.Embedded } finally { + CurrentModelsAssembly = null; _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; } } @@ -374,38 +384,71 @@ namespace Umbraco.ModelsBuilder.Embedded { _locker.ExitUpgradeableReadLock(); } - - if (roslynLocked) - { - Monitor.Exit(RoslynCompiler); - } } } - private Assembly ReloadAssembly(string pathToAssembly) + private static MetadataReference CreateMetadataReference(string path) { - // If there's a current AssemblyLoadContext, unload it before creating a new one. - if (!(_currentAssemblyLoadContext is null)) + using (FileStream stream = File.OpenRead(path)) { - _currentAssemblyLoadContext.Unload(); - GC.Collect(); - GC.WaitForPendingFinalizers(); - } + var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata); + var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata); - // 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(); - - // Use filestream to load in the new assembly, otherwise it'll be locked - // See https://www.strathweb.com/2019/01/collectible-assemblies-in-net-core-3-0/ for more info - using (var fs = new FileStream(pathToAssembly, FileMode.Open, FileAccess.Read)) - { - return _currentAssemblyLoadContext.LoadFromStream(fs); + return assemblyMetadata.GetReference(filePath: path); } } - private Assembly GetModelsAssembly(bool forceRebuild) + private Assembly ReloadAssembly(string pathToAssembly, out MetadataReference metadataReference) { + // TODO: We should look into removing the old application part + + //// If there's a current AssemblyLoadContext, unload it before creating a new one. + //if (!(_currentAssemblyLoadContext is null)) + //{ + // _currentAssemblyLoadContext.Unload(); + // GC.Collect(); + // GC.WaitForPendingFinalizers(); + //} + + //// 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(); + + // Need to work on a temp file so that it's not locked + var tempFile = Path.GetTempFileName(); + File.Copy(pathToAssembly, tempFile, true); + // Load it in + Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(tempFile); + // Create a metadata ref, TODO: This is actually not required and doesn't really work so we can remove that + metadataReference = CreateMetadataReference(tempFile); + + // Add the assembly to the application parts - this is required because this is how + // the razor ReferenceManager resolves what to load, see + // https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs#L53 + var partFactory = ApplicationPartFactory.GetApplicationPartFactory(assembly); + foreach (ApplicationPart applicationPart in partFactory.GetApplicationParts(assembly)) + { + _applicationPartManager.ApplicationParts.Add(applicationPart); + } + + return assembly; + + //// Use filestream to load in the new assembly, otherwise it'll be locked + //// See https://www.strathweb.com/2019/01/collectible-assemblies-in-net-core-3-0/ for more info + //using (var fs = new FileStream(pathToAssembly, FileMode.Open, FileAccess.Read)) + //{ + // Assembly assembly = _currentAssemblyLoadContext.LoadFromStream(fs); + // //fs.Position = 0; + // //metadataReference = MetadataReference.CreateFromStream(fs, filePath: null); + // //metadataReference = CreateMetadataReference(fs, pathToAssembly); + // return assembly; + //} + } + + private Assembly GetModelsAssembly(bool forceRebuild, out MetadataReference metadataReference) + { + metadataReference = null; + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) { @@ -458,7 +501,7 @@ namespace Umbraco.ModelsBuilder.Embedded if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete")) { - assembly = ReloadAssembly(dllPath); + assembly = ReloadAssembly(dllPath, out MetadataReference mdr); ModelsBuilderAssemblyAttribute attr = assembly.GetCustomAttribute(); if (attr != null && attr.PureLive && attr.SourceHash == currentHash) @@ -470,6 +513,7 @@ namespace Umbraco.ModelsBuilder.Embedded _skipver = assembly.GetName().Version.Revision; _logger.LogDebug("Loading cached models (dll)."); + metadataReference = mdr; return assembly; } @@ -504,7 +548,7 @@ namespace Umbraco.ModelsBuilder.Embedded { var assemblyPath = GetOutputAssemblyPath(currentHash); RoslynCompiler.CompileToFile(projFile, assemblyPath); - assembly = ReloadAssembly(assemblyPath); + assembly = ReloadAssembly(assemblyPath, out metadataReference); File.WriteAllText(dllPathFile, assembly.Location); File.WriteAllText(modelsHashFile, currentHash); TryDeleteUnusedAssemblies(dllPathFile); @@ -547,7 +591,7 @@ namespace Umbraco.ModelsBuilder.Embedded { var assemblyPath = GetOutputAssemblyPath(currentHash); RoslynCompiler.CompileToFile(projFile, assemblyPath); - assembly = ReloadAssembly(assemblyPath); + assembly = ReloadAssembly(assemblyPath, out metadataReference); File.WriteAllText(dllPathFile, assemblyPath); File.WriteAllText(modelsHashFile, currentHash); TryDeleteUnusedAssemblies(dllPathFile); @@ -778,12 +822,16 @@ namespace Umbraco.ModelsBuilder.Embedded // always ignore our own file changes if (s_ourFiles.Contains(changed)) + { return; + } _logger.LogInformation("Detected files changes."); lock (SyncRoot) // don't reset while being locked + { ResetModels(); + } } public void Stop(bool immediate) diff --git a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs index 7f1443b156..d735770774 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs @@ -1,11 +1,11 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; namespace Umbraco.ModelsBuilder.Embedded { @@ -28,7 +28,7 @@ namespace Umbraco.ModelsBuilder.Embedded // Making it kind of a waste to convert the Assembly types into MetadataReference // every time GetCompiledAssembly is called, so that's why I do it in the ctor _refs = new List(); - foreach(var assembly in referenceAssemblies.Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)).Distinct()) + foreach (var assembly in referenceAssemblies.Where(x => !x.IsDynamic && !string.IsNullOrWhiteSpace(x.Location)).Distinct()) { _refs.Add(MetadataReference.CreateFromFile(assembly.Location)); }; @@ -54,13 +54,15 @@ namespace Umbraco.ModelsBuilder.Embedded var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); - var compilation = CSharpCompilation.Create("ModelsGeneratedAssembly", + var compilation = CSharpCompilation.Create( + "ModelsGeneratedAssembly", new[] { syntaxTree }, references: _refs, - options: new CSharpCompilationOptions(_outputKind, - optimizationLevel: OptimizationLevel.Release, - // Not entirely certain that assemblyIdentityComparer is nececary? - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); + options: new CSharpCompilationOptions( + _outputKind, + optimizationLevel: OptimizationLevel.Release, + // Not entirely certain that assemblyIdentityComparer is nececary? + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); compilation.Emit(savePath); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj index f2b264dcb0..1fe149eb11 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj +++ b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs index 8763da86a6..76803abe1f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/UmbracoServices.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; @@ -11,6 +11,7 @@ using Umbraco.ModelsBuilder.Embedded.Building; namespace Umbraco.ModelsBuilder.Embedded { + public sealed class UmbracoServices { private readonly IContentTypeService _contentTypeService; diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index ca2f9e6161..ca6c2da6e8 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -28,8 +28,8 @@ namespace Umbraco.Web.Website.DependencyInjection .Add(builder.TypeLoader.GetSurfaceControllers()); // Configure MVC startup options for custom view locations - builder.Services.AddTransient, RenderRazorViewEngineOptionsSetup>(); - builder.Services.AddTransient, PluginRazorViewEngineOptionsSetup>(); + builder.Services.ConfigureOptions(); + builder.Services.ConfigureOptions(); // Wraps all existing view engines in a ProfilerViewEngine builder.Services.AddTransient, ProfilingViewEngineWrapperMvcViewOptionsSetup>();