diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
index bace0f96c4..18f79e5602 100644
--- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs
@@ -21,12 +21,19 @@ namespace Umbraco.Core.Configuration.Models
/// If this is false then absolutely nothing happens.
/// Default value is false which means that unless we have this setting, nothing happens.
///
- public bool Enable { get; set; } = false;
+ // TODO: This setting makes no sense at all, this basically just disables haveing models be able to reset dynamically
+ // and configure some dashboards but the models are all still built and active!
+ // Can this be truly disabled or not?
+ // Then there's other ways to disable things - EnableFactory also causes odd flags but again, how can this be disabled?
+ // The other ways that flags change are ModelsMode.
+ // TODO: Make these make sense and test what is possible
+ // Confirmed A) Enabled = false, ModelsMode = Nothing, EnabledFagtory = false == EXPLODES, null refs because these things are needed unless you replace nucache.
+ public bool Enable { get; set; } = true;
///
/// Gets or sets a value for the models mode.
///
- public ModelsMode ModelsMode { get; set; } = ModelsMode.Nothing;
+ public ModelsMode ModelsMode { get; set; } = ModelsMode.PureLive;
///
/// Gets or sets a value for models namespace.
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
index c615559920..6b413ad4a1 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
@@ -1,4 +1,4 @@
-using System.Text;
+using System.Text;
using Microsoft.Extensions.Options;
using Umbraco.Configuration;
using Umbraco.Core.Configuration;
@@ -41,9 +41,13 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice
sb.Append("");
sb.Append("- The models factory is ");
+
+ // TODO: Test this - if models factory is entirely disabled will umbraco work at all?
+ // if not, is there a point to this?
sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive
? "enabled"
: "not enabled. Umbraco will not use models");
+
sb.Append(".
");
sb.Append(_config.ModelsMode != ModelsMode.Nothing
diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
index bb0b966195..e35c6b9c67 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -1,18 +1,34 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
-using Microsoft.AspNetCore.Mvc.Razor.Extensions;
+using System.Reflection;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Razor;
+using Microsoft.AspNetCore.Mvc.Razor.Compilation;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.DependencyInjection;
+using Umbraco.Core.Events;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.ModelsBuilder.Embedded.Building;
+using Umbraco.ModelsBuilder.Embedded.DependencyInjection;
+using Umbraco.Web.WebAssets;
+
+// This is the insanity that allows you to customize the RazorProjectEngineBuilder
+[assembly: ProvideRazorExtensionInitializer("ModelsBuilderPureLive", typeof(ModelsBuilderRazorProjectBuilderExtension))]
namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection
{
@@ -28,7 +44,10 @@ namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection
{
builder.AddRazorProjectEngine();
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ // TODO: I feel like we could just do builder.AddNotificationHandler() and it
+ // would automatically just register for all implemented INotificationHandler{T}?
+ builder.AddNotificationHandler();
+ builder.AddNotificationHandler();
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
@@ -92,76 +111,275 @@ namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection
// 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.
+ // that is nornally created and use that to create/wrap our custom one.
- //var internalServicesBuilder = new ServiceCollection();
- //internalServicesBuilder.AddControllersWithViews().AddRazorRuntimeCompilation();
- //var internalServices = internalServicesBuilder.BuildServiceProvider();
- //var defaultRazorProjectEngine = internalServices.GetRequiredService();
+ ServiceProvider initialServices = builder.Services.BuildServiceProvider();
+ RazorProjectEngine defaultRazorProjectEngine = initialServices.GetRequiredService();
- ServiceProvider internalServices = builder.Services.BuildServiceProvider();
- RazorProjectEngine defaultRazorProjectEngine = internalServices.GetRequiredService();
-
- builder.Services.AddSingleton(s =>
+ // copy the current collection, we need to use this later to rebuild a container
+ // to re-create the razor compiler provider
+ var initialCollection = new ServiceCollection
{
- RazorProjectFileSystem fileSystem = s.GetRequiredService();
+ builder.Services
+ };
- // Create the project engine
- var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder =>
- {
- // replace all features with the defaults
- builder.Features.Clear();
+ builder.Services.AddSingleton(
+ s => new RefreshingRazorProjectEngine(defaultRazorProjectEngine, s, s.GetRequiredService()));
- foreach (IRazorEngineFeature f in defaultRazorProjectEngine.EngineFeatures)
- {
- builder.Features.Add(f);
- }
+ builder.Services.AddSingleton(
+ s => new RefreshingRuntimeViewCompilerProvider(
+ () =>
+ {
+ // re-create the original container so that a brand new IViewCompilerProvider
+ // is produced, if we don't re-create the container then it will just return the same instance.
+ ServiceProvider recreatedServices = initialCollection.BuildServiceProvider();
+ return recreatedServices.GetRequiredService();
+ }, s.GetRequiredService()));
- foreach (IRazorProjectEngineFeature f in defaultRazorProjectEngine.ProjectFeatures)
- {
- builder.Features.Add(f);
- }
+ builder.Services.AddSingleton(
+ s => new RefreshingRazorPageActivator(
+ () =>
+ {
+ // re-create the original container so that a brand new IRazorPageActivator
+ // is produced, if we don't re-create the container then it will just return the same instance.
+ ServiceProvider recreatedServices = initialCollection.BuildServiceProvider();
+ return recreatedServices.GetRequiredService();
+ }, s.GetRequiredService()));
- // 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;
- });
+ builder.Services.AddSingleton(
+ s => new RefreshingRazorViewEngine(
+ () =>
+ {
+ // re-create the original container so that a brand new IRazorPageActivator
+ // is produced, if we don't re-create the container then it will just return the same instance.
+ ServiceProvider recreatedServices = initialCollection.BuildServiceProvider();
+ return recreatedServices.GetRequiredService();
+ }, s.GetRequiredService()));
return builder;
}
}
+ internal class ModelsBuilderRazorProjectBuilderExtension : RazorExtensionInitializer
+ {
+ public override void Initialize(RazorProjectEngineBuilder builder)
+ {
+ // Finally, after jumping through many hoops, we can customize the builder.
+
+ // Get our extension that launched this so we can access services
+ ModelsBuilderAssemblyExtension mbExt = builder.Configuration.Extensions.OfType().First();
+
+ // Now... customize
+
+ // TODO: BUT This is called before all of the default options are done, argh! so you can't replace anything here anyways
+ }
+ }
+
+ // We need a custom assembly extension so we can pass state into the initializer :/
+ internal class ModelsBuilderAssemblyExtension : AssemblyExtension
+ {
+ public ModelsBuilderAssemblyExtension(IServiceProvider serviceProvider, string extensionName, Assembly assembly)
+ : base(extensionName, assembly) => ServiceProvider = serviceProvider;
+
+ public IServiceProvider ServiceProvider { get; }
+ }
+
+ // The default razor page activator keeps an internal cache of activations, this allows clearning that cache
+ // TODO: Find out if we really need to clear this cache or not? Or if just clearing the view engine cache is enough?
+ internal class RefreshingRazorPageActivator : IRazorPageActivator
+ {
+ private readonly Func _defaultRazorPageActivatorFactory;
+ private readonly PureLiveModelFactory _pureLiveModelFactory;
+ private IRazorPageActivator _current;
+
+ public RefreshingRazorPageActivator(
+ Func defaultRazorPageActivatorFactory,
+ PureLiveModelFactory pureLiveModelFactory)
+ {
+ _pureLiveModelFactory = pureLiveModelFactory;
+ _defaultRazorPageActivatorFactory = defaultRazorPageActivatorFactory;
+ _current = _defaultRazorPageActivatorFactory();
+ _pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged;
+ }
+
+ // TODO: Do we need to lock?
+ private void PureLiveModelFactory_ModelsChanged(object sender, EventArgs e) => _current = _defaultRazorPageActivatorFactory();
+
+ public void Activate(IRazorPage page, ViewContext context) => _current.Activate(page, context);
+ }
+
+ // We need to have a refreshing razor view engine - the default keeps an in memory cache of views and it cannot be cleared because
+ // the cache key instance is internal and would require manually tracking all keys since it cannot be iterated.
+ // So like other 'Refreshing' intances, we just create a brand new one and let the old one die therefore clearing the cache.
+ internal class RefreshingRazorViewEngine : IRazorViewEngine
+ {
+ private IRazorViewEngine _current;
+ private readonly PureLiveModelFactory _pureLiveModelFactory;
+ private readonly Func _defaultRazorViewEngineFactory;
+
+ public RefreshingRazorViewEngine(Func defaultRazorViewEngineFactory, PureLiveModelFactory pureLiveModelFactory)
+ {
+ _pureLiveModelFactory = pureLiveModelFactory;
+ _defaultRazorViewEngineFactory = defaultRazorViewEngineFactory;
+ _current = _defaultRazorViewEngineFactory();
+ _pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged;
+ }
+
+ // TODO: Do we need to lock?
+ private void PureLiveModelFactory_ModelsChanged(object sender, EventArgs e) => _current = _defaultRazorViewEngineFactory();
+
+ public RazorPageResult FindPage(ActionContext context, string pageName) => _current.FindPage(context, pageName);
+
+ public string GetAbsolutePath(string executingFilePath, string pagePath) => _current.GetAbsolutePath(executingFilePath, pagePath);
+
+ public RazorPageResult GetPage(string executingFilePath, string pagePath) => _current.GetPage(executingFilePath, pagePath);
+
+ public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) => _current.FindView(context, viewName, isMainPage);
+
+ public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage) => _current.GetView(executingFilePath, viewPath, isMainPage);
+ }
+
+ // The default view compiler creates the compiler once and only once. That compiler will have a stale list of references
+ // to build against so it needs to be re-created. The only way to do that due to internals is to wrap it and re-create
+ // the default instance therefore resetting the compiler references.
+ // TODO: Find out if we really need to clear this cache or not? Or if just clearing the view engine cache is enough?
+ internal class RefreshingRuntimeViewCompilerProvider : IViewCompilerProvider
+ {
+ private IViewCompilerProvider _current;
+ private readonly Func _defaultViewCompilerProviderFactory;
+ private readonly PureLiveModelFactory _pureLiveModelFactory;
+
+ public RefreshingRuntimeViewCompilerProvider(
+ Func defaultViewCompilerProviderFactory,
+ PureLiveModelFactory pureLiveModelFactory)
+ {
+ _defaultViewCompilerProviderFactory = defaultViewCompilerProviderFactory;
+ _pureLiveModelFactory = pureLiveModelFactory;
+ _current = _defaultViewCompilerProviderFactory();
+ _pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged;
+ }
+
+ // TODO: Do we need to lock?
+ private void PureLiveModelFactory_ModelsChanged(object sender, EventArgs e) => _current = _defaultViewCompilerProviderFactory();
+
+ public IViewCompiler GetCompiler() => _current.GetCompiler();
+ }
+
+ // TODO: Need to review this to see if this service is actually one we need to clear or not?
+ // Does it hold cache? etc... I originally said "so that all of the underlying services are cleared"
+ // but I'm not sure now if that's needed since we need the above which do hold caches.
+ // The other problem is that because we are re-creating the above services from the default service collection,
+ // the reference they will have for their RazorProjectEngine will not be this one, it will be the default one...
+ // though I guess we can change that based on the ordering and resolving of the containers.
+ // I'm just not entirely convinced we need this anymore?
+ // ... Actually, it might be all related to this IMetadataReferenceFeature thing since that sort of needs to be refreshed.
+ // guess we need to do some testing. But we need to ensure that there's not a bunch of different razor project engines being
+ // referenced everywhere.
+ internal class RefreshingRazorProjectEngine : RazorProjectEngine
+ {
+ private RazorProjectEngine _current;
+ private readonly RazorProjectEngine _defaultRazorProjectEngine;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly PureLiveModelFactory _pureLiveModelFactory;
+ private readonly MethodInfo _createCodeDocumentCore;
+ private readonly MethodInfo _createCodeDocumentDesignTimeCore;
+ private readonly MethodInfo _processCore;
+
+ public RefreshingRazorProjectEngine(
+ RazorProjectEngine defaultRazorProjectEngine,
+ IServiceProvider serviceProvider,
+ PureLiveModelFactory pureLiveModelFactory)
+ {
+ _defaultRazorProjectEngine = defaultRazorProjectEngine;
+ _serviceProvider = serviceProvider;
+ _pureLiveModelFactory = pureLiveModelFactory;
+ _current = CreateNew();
+ Type engineType = _current.GetType();
+ _createCodeDocumentCore = engineType.GetMethod(nameof(CreateCodeDocumentCore), BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(RazorProjectItem) }, null);
+ _createCodeDocumentDesignTimeCore = engineType.GetMethod(nameof(CreateCodeDocumentDesignTimeCore), BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(RazorProjectItem) }, null);
+ _processCore = engineType.GetMethod(nameof(ProcessCore), BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(RazorCodeDocument) }, null);
+
+ _pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged;
+ }
+
+ public override RazorConfiguration Configuration => _current.Configuration;
+
+ public override RazorProjectFileSystem FileSystem => _current.FileSystem;
+
+ public override RazorEngine Engine => _current.Engine;
+
+ public override IReadOnlyList ProjectFeatures => _current.ProjectFeatures;
+
+ // TODO: Do we need to lock?
+ private void PureLiveModelFactory_ModelsChanged(object sender, EventArgs e) => _current = CreateNew();
+
+ private RazorConfiguration GetConfiguration()
+ {
+ RazorConfiguration defaultConfig = RazorConfiguration.Default;
+
+ // TODO: It turns out you can add logic into the RazorProjectEngineBuilder by adding custom razor extensions to the configuration.
+ var mbConfig = RazorConfiguration.Create(
+ defaultConfig.LanguageVersion,
+ "ModelsBuilderPureLiveConfig",
+ new[] { new ModelsBuilderAssemblyExtension(_serviceProvider, "ModelsBuilderPureLive", typeof(RefreshingRazorProjectEngine).Assembly) });
+
+ return mbConfig;
+ }
+
+ private RazorProjectEngine CreateNew()
+ {
+ // get the default
+ RazorProjectFileSystem fileSystem = _serviceProvider.GetRequiredService();
+
+ // Create the project engine
+ var projectEngine = RazorProjectEngine.Create(GetConfiguration(), fileSystem, builder =>
+ {
+ // TODO: All this hacking is because RazorExtensionInitializer doesn't give us access to the service provider
+
+ // 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(_pureLiveModelFactory));
+
+ // now add them to our wrapper and back into the features
+ builder.Features.Add(new MetadataReferenceFeatureWrapper(metadataReferenceFeatures));
+ });
+
+ return projectEngine;
+ }
+
+ protected override RazorCodeDocument CreateCodeDocumentCore(RazorProjectItem projectItem)
+ => (RazorCodeDocument)_createCodeDocumentCore.Invoke(_current, new[] { projectItem });
+
+ protected override RazorCodeDocument CreateCodeDocumentDesignTimeCore(RazorProjectItem projectItem)
+ => (RazorCodeDocument)_createCodeDocumentDesignTimeCore.Invoke(_current, new[] { projectItem });
+
+ protected override void ProcessCore(RazorCodeDocument codeDocument)
+ => _processCore.Invoke(_current, new[] { codeDocument });
+
+ }
+
///
/// Wraps multiple
///
diff --git a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs
index 8883069ca7..2e969eae17 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs
@@ -33,6 +33,7 @@ namespace Umbraco.ModelsBuilder.Embedded
private readonly OutOfDateModelsStatus _outOfDateModels;
private readonly LinkGenerator _linkGenerator;
private readonly IUmbracoRequestLifetime _umbracoRequestLifetime;
+ private readonly ContentModelBinder _modelBinder;
public ModelsBuilderNotificationHandler(
IOptions config,
@@ -40,7 +41,8 @@ namespace Umbraco.ModelsBuilder.Embedded
LiveModelsProvider liveModelsProvider,
OutOfDateModelsStatus outOfDateModels,
LinkGenerator linkGenerator,
- IUmbracoRequestLifetime umbracoRequestLifetime)
+ IUmbracoRequestLifetime umbracoRequestLifetime,
+ ContentModelBinder modelBinder)
{
_config = config.Value;
_shortStringHelper = shortStringHelper;
@@ -49,6 +51,7 @@ namespace Umbraco.ModelsBuilder.Embedded
_shortStringHelper = shortStringHelper;
_linkGenerator = linkGenerator;
_umbracoRequestLifetime = umbracoRequestLifetime;
+ _modelBinder = modelBinder;
}
///
@@ -60,7 +63,7 @@ namespace Umbraco.ModelsBuilder.Embedded
// note: UmbracoApiController instances are automatically registered
_umbracoRequestLifetime.RequestEnd += (sender, context) => _liveModelsProvider.AppEndRequest(context);
- ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException;
+ _modelBinder.ModelBindingException += ContentModelBinder_ModelBindingException;
if (_config.Enable)
{
diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs
index 6433accaef..5088cf3d9f 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs
@@ -38,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" };
@@ -93,8 +93,12 @@ namespace Umbraco.ModelsBuilder.Embedded
// get it here, this need to be fast
_debugLevel = _config.DebugLevel;
+
+ AssemblyLoadContext.Default.Resolving += OnResolvingDefaultAssemblyLoadContext;
}
+ public event EventHandler ModelsChanged;
+
private UmbracoServices UmbracoServices => _umbracoServices.Value;
///
@@ -130,6 +134,16 @@ namespace Umbraco.ModelsBuilder.Embedded
///
public bool Enabled => _config.Enable;
+ ///
+ /// Handle the event when a reference cannot be resolved from the default context and return our custom MB assembly reference if we have one
+ ///
+ ///
+ /// 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.
+ ///
+ private Assembly OnResolvingDefaultAssemblyLoadContext(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
+ => _currentAssemblyLoadContext?.LoadFromAssemblyName(assemblyName);
+
public IPublishedElement CreateModel(IPublishedElement element)
{
// get models, rebuilding them if needed
@@ -188,7 +202,7 @@ namespace Umbraco.ModelsBuilder.Embedded
///
public void Reset()
{
- if (_config.Enable)
+ if (Enabled)
{
ResetModels();
}
@@ -275,9 +289,6 @@ 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);
@@ -340,14 +351,19 @@ namespace Umbraco.ModelsBuilder.Embedded
{
try
{
- // TODO: We may have to copy this to a temp place?
- // string assemblyName = Path.GetRandomFileName();
-
Assembly assembly = GetModelsAssembly(_pendingRebuild, out MetadataReference metadataReference);
+ bool hasAssembly = CurrentModelsAssembly != null;
CurrentModelsAssembly = assembly;
CurrentModelsMetadataReference = metadataReference;
+ // raise the event that they've changed.
+ // don't raise on the initial call, only when it's actually changed.
+ if (hasAssembly)
+ {
+ ModelsChanged?.Invoke(this, new EventArgs());
+ }
+
IEnumerable types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits());
_infos = RegisterModels(types);
_errors.Clear();
@@ -391,37 +407,64 @@ namespace Umbraco.ModelsBuilder.Embedded
{
using (FileStream stream = File.OpenRead(path))
{
- var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata);
- var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata);
-
- return assemblyMetadata.GetReference(filePath: path);
+ return CreateMetadataReference(stream, path);
}
}
+ private static MetadataReference CreateMetadataReference(Stream stream, string path)
+ {
+ var moduleMetadata = ModuleMetadata.CreateFromStream(stream, PEStreamOptions.PrefetchMetadata);
+ var assemblyMetadata = AssemblyMetadata.Create(moduleMetadata);
+ return assemblyMetadata.GetReference(filePath: path);
+ }
+
+ // This is NOT thread safe but it is only called from within a lock
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();
- //// If there's a current AssemblyLoadContext, unload it before creating a new one.
- //if (!(_currentAssemblyLoadContext is null))
- //{
- // _currentAssemblyLoadContext.Unload();
- // GC.Collect();
- // GC.WaitForPendingFinalizers();
- //}
+ // we need to remove the current part too
+ ApplicationPart currentPart = _applicationPartManager.ApplicationParts.FirstOrDefault(x => x.Name == RoslynCompiler.GeneratedAssemblyName);
+ if (currentPart != null)
+ {
+ _applicationPartManager.ApplicationParts.Remove(currentPart);
+ }
+ }
- //// 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();
+ // 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
+ // 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
+ // TODO: Not sure if the process always has access to a temp path?
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
+ Assembly assembly = _currentAssemblyLoadContext.LoadFromAssemblyPath(tempFile);
+
+ // Create a metadata ref
metadataReference = CreateMetadataReference(tempFile);
+ //// 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 = _currentAssemblyLoadContext.LoadFromStream(fs);
+
+ // // reset stream so it can be used for the reference (the call to CreateMetadataReference will close the stream)
+ // fs.Position = 0;
+ // metadataReference = CreateMetadataReference(fs, pathToAssembly);
+ //}
+
// 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
@@ -432,19 +475,9 @@ namespace Umbraco.ModelsBuilder.Embedded
}
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;
- //}
}
+ // This is NOT thread safe but it is only called from within a lock
private Assembly GetModelsAssembly(bool forceRebuild, out MetadataReference metadataReference)
{
metadataReference = null;
diff --git a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs
index d735770774..1321078f98 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs
@@ -11,14 +11,19 @@ namespace Umbraco.ModelsBuilder.Embedded
{
public class RoslynCompiler
{
+ public const string GeneratedAssemblyName = "ModelsGeneratedAssembly";
+
private OutputKind _outputKind;
private CSharpParseOptions _parseOptions;
private List _refs;
///
- /// Roslyn compiler which can be used to compile a c# file to a Dll assembly
+ /// Initializes a new instance of the class.
///
/// Referenced assemblies used in the source file
+ ///
+ /// Roslyn compiler which can be used to compile a c# file to a Dll assembly
+ ///
public RoslynCompiler(IEnumerable referenceAssemblies)
{
_outputKind = OutputKind.DynamicallyLinkedLibrary;
@@ -55,7 +60,7 @@ namespace Umbraco.ModelsBuilder.Embedded
var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions);
var compilation = CSharpCompilation.Create(
- "ModelsGeneratedAssembly",
+ GeneratedAssemblyName,
new[] { syntaxTree },
references: _refs,
options: new CSharpCompilationOptions(
diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs b/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs
index b7feeaaaeb..5064095cdd 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Loader;
@@ -6,8 +6,9 @@ using System.Text;
namespace Umbraco.ModelsBuilder.Embedded
{
- class UmbracoAssemblyLoadContext : AssemblyLoadContext
+ internal class UmbracoAssemblyLoadContext : AssemblyLoadContext
{
+ private AssemblyDependencyResolver _resolver;
///
/// Collectible AssemblyLoadContext used to load in the compiled generated models.
@@ -15,11 +16,17 @@ namespace Umbraco.ModelsBuilder.Embedded
///
public UmbracoAssemblyLoadContext() : base(isCollectible: true)
{
-
+ //_resolver = new AssemblyDependencyResolver(pluginPath);
}
protected override Assembly Load(AssemblyName assemblyName)
{
+ //string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
+ //if (assemblyPath != null)
+ //{
+ // return LoadFromAssemblyPath(assemblyPath);
+ //}
+
return null;
}
}
diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs
index 5068e52b49..6cc0416dbb 100644
--- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs
+++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs
@@ -509,7 +509,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (draftChanged || publishedChanged)
{
- CurrentPublishedSnapshot.Resync();
+ CurrentPublishedSnapshot?.Resync();
}
}
@@ -609,7 +609,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (anythingChanged)
{
- CurrentPublishedSnapshot.Resync();
+ CurrentPublishedSnapshot?.Resync();
}
}
@@ -727,7 +727,6 @@ namespace Umbraco.Web.PublishedCache.NuCache
// we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which
// they require.
- // These can be run side by side in parallel.
using (_contentStore.GetScopedWriteLock(_scopeProvider))
{
NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _);
@@ -739,7 +738,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
- CurrentPublishedSnapshot.Resync();
+ CurrentPublishedSnapshot?.Resync();
}
private void Notify(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action, List, List, List> action)
@@ -831,7 +830,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
}
- CurrentPublishedSnapshot.Resync();
+ CurrentPublishedSnapshot?.Resync();
}
public void Notify(DomainCacheRefresher.JsonPayload[] payloads)
@@ -1070,7 +1069,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) =>
{
- ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync();
+ svc.CurrentPublishedSnapshot?.Resync();
}, int.MaxValue);
}
diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs
index 3afc8978b6..3a0b929d52 100644
--- a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs
+++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs
@@ -2,6 +2,7 @@ using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
@@ -11,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.PublishedContent;
@@ -42,8 +44,6 @@ namespace Umbraco.Web.Common.AspNetCore
private IIOHelper IOHelper => Context.RequestServices.GetRequiredService();
- private ContentModelBinder ContentModelBinder => new ContentModelBinder();
-
///
/// Gets the
///
@@ -57,7 +57,7 @@ namespace Umbraco.Web.Common.AspNetCore
{
// Here we do the magic model swap
ViewContext ctx = value;
- ctx.ViewData = BindViewData(ctx.ViewData);
+ ctx.ViewData = BindViewData(ctx.HttpContext, ctx.ViewData);
base.ViewContext = ctx;
}
}
@@ -127,7 +127,7 @@ namespace Umbraco.Web.Common.AspNetCore
/// or . This will use the to bind the models
/// to the correct output type.
///
- protected ViewDataDictionary BindViewData(ViewDataDictionary viewData)
+ protected ViewDataDictionary BindViewData(HttpContext context, ViewDataDictionary viewData)
{
// check if it's already the correct type and continue if it is
if (viewData is ViewDataDictionary vdd)
@@ -153,8 +153,9 @@ namespace Umbraco.Web.Common.AspNetCore
viewData = MapViewDataDictionary(viewData, typeof(TModel));
// bind the model
+ var contentModelBinder = context.RequestServices.GetRequiredService();
var bindingContext = new DefaultModelBindingContext();
- ContentModelBinder.BindModel(bindingContext, viewDataModel, typeof(TModel));
+ contentModelBinder.BindModel(bindingContext, viewDataModel, typeof(TModel));
viewData.Model = bindingContext.Result.Model;
diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
index 30a5b4d53e..0751eb582c 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -269,6 +269,8 @@ namespace Umbraco.Web.Common.DependencyInjection
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddSingleton();
+
builder.AddHttpClients();
// TODO: Does this belong in web components??
diff --git a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs
index 7bdf1b13af..47ca49f014 100644
--- a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs
+++ b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs
@@ -14,6 +14,11 @@ namespace Umbraco.Web.Common.ModelBinders
///
public class ContentModelBinder : IModelBinder
{
+ ///
+ /// Occurs on model binding exceptions.
+ ///
+ public event EventHandler ModelBindingException; // TODO: This cannot use IEventAggregator currently because it cannot be async
+
///
public Task BindModelAsync(ModelBindingContext bindingContext)
{
@@ -193,10 +198,5 @@ namespace Umbraco.Web.Common.ModelBinders
///
public bool Restart { get; set; }
}
-
- ///
- /// Occurs on model binding exceptions.
- ///
- public static event EventHandler ModelBindingException;
}
}
diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs
index 46f7b2d7ae..22c3f14945 100644
--- a/src/Umbraco.Web.UI.NetCore/Startup.cs
+++ b/src/Umbraco.Web.UI.NetCore/Startup.cs
@@ -48,6 +48,14 @@ namespace Umbraco.Web.UI.NetCore
.AddBackOffice()
.AddWebsite()
.AddComposers()
+ // TODO: This call and AddDistributedCache are interesting ones. They are both required for back office and front-end to render
+ // but we don't want to force people to call so many of these ext by default and want to keep all of this relatively simple.
+ // but we still need to allow the flexibility for people to use their own ModelsBuilder. In that case people can call a different
+ // AddModelsBuilderCommunity (or whatever) after our normal calls to replace our services.
+ // So either we call AddModelsBuilder within AddBackOffice AND AddWebsite just like we do with AddDistributedCache or we
+ // have a top level method to add common things required for backoffice/frontend like .AddCommon()
+ // or we allow passing in options to these methods to configure what happens within them.
+ .AddModelsBuilder()
.Build();
#pragma warning restore IDE0022 // Use expression body for methods