diff --git a/.gitignore b/.gitignore index 15c9fb52f2..0cffac8343 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,4 @@ src/Umbraco.Tests/TEMP/ /src/Umbraco.Web.UI.NetCore/Umbraco/Data/* /src/Umbraco.Web.UI/config/umbracoSettings.config +/src/Umbraco.Web.UI.NetCore/Umbraco/models/* diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index bace0f96c4..4f5916822e 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -14,19 +14,10 @@ namespace Umbraco.Core.Configuration.Models private static string DefaultModelsDirectory => "~/umbraco/models"; - /// - /// Gets or sets a value indicating whether the whole models experience is enabled. - /// - /// - /// 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; - /// /// 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. @@ -34,12 +25,6 @@ namespace Umbraco.Core.Configuration.Models /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. public string ModelsNamespace { get; set; } - /// - /// Gets or sets a value indicating whether we should enable the models factory. - /// - /// Default value is true because no factory is enabled by default in Umbraco. - public bool EnableFactory { get; set; } = true; - /// /// Gets or sets a value indicating whether we should flag out-of-date models. /// diff --git a/src/Umbraco.Core/Configuration/ModelsMode.cs b/src/Umbraco.Core/Configuration/ModelsMode.cs index 2483367394..c3b32b1cab 100644 --- a/src/Umbraco.Core/Configuration/ModelsMode.cs +++ b/src/Umbraco.Core/Configuration/ModelsMode.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.Configuration +namespace Umbraco.Core.Configuration { /// /// Defines the models generation modes. @@ -6,9 +6,12 @@ public enum ModelsMode { /// - /// Do not generate models. + /// Do not generate strongly typed models. /// - Nothing = 0, // default value + /// + /// This means that only IPublishedContent instances will be used. + /// + Nothing = 0, /// /// Generate models in memory. diff --git a/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs b/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs index 8d1b51cd28..6aae79ff8a 100644 --- a/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs +++ b/src/Umbraco.Core/Configuration/ModelsModeExtensions.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration; namespace Umbraco.Configuration { @@ -11,29 +11,18 @@ namespace Umbraco.Configuration /// Gets a value indicating whether the mode is LiveAnything or PureLive. /// public static bool IsLive(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.PureLive - || modelsMode == ModelsMode.LiveAppData; - } + => modelsMode == ModelsMode.PureLive || modelsMode == ModelsMode.LiveAppData; /// /// Gets a value indicating whether the mode is LiveAnything but not PureLive. /// public static bool IsLiveNotPure(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.LiveAppData; - } + => modelsMode == ModelsMode.LiveAppData; /// /// Gets a value indicating whether the mode supports explicit generation (as opposed to pure live). /// public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.AppData - || modelsMode == ModelsMode.LiveAppData; - } + => modelsMode == ModelsMode.AppData || modelsMode == ModelsMode.LiveAppData; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs index c24936b4fb..a533b0d8a2 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -1,6 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; +using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Core.Events; @@ -24,7 +26,7 @@ namespace Umbraco.Core.DependencyInjection where TNotification : INotification { // Register the handler as transient. This ensures that anything can be injected into it. - var descriptor = new ServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); + var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler), typeof(TNotificationHandler), ServiceLifetime.Transient); // TODO: Waiting on feedback here https://github.com/umbraco/Umbraco-CMS/pull/9556/files#r548365396 about whether // we perform this duplicate check or not. @@ -35,5 +37,30 @@ namespace Umbraco.Core.DependencyInjection return builder; } + + // This is required because the default implementation doesn't implement Equals or GetHashCode. + // see: https://github.com/dotnet/runtime/issues/47262 + private class UniqueServiceDescriptor : ServiceDescriptor, IEquatable + { + public UniqueServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime) + : base(serviceType, implementationType, lifetime) + { + } + + public override bool Equals(object obj) => Equals(obj as UniqueServiceDescriptor); + public bool Equals(UniqueServiceDescriptor other) => other != null && Lifetime == other.Lifetime && EqualityComparer.Default.Equals(ServiceType, other.ServiceType) && EqualityComparer.Default.Equals(ImplementationType, other.ImplementationType) && EqualityComparer.Default.Equals(ImplementationInstance, other.ImplementationInstance) && EqualityComparer>.Default.Equals(ImplementationFactory, other.ImplementationFactory); + + public override int GetHashCode() + { + int hashCode = 493849952; + hashCode = hashCode * -1521134295 + Lifetime.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ServiceType); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ImplementationType); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ImplementationInstance); + hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(ImplementationFactory); + return hashCode; + } + } + } } 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..3794bc6954 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; @@ -10,51 +10,23 @@ namespace Umbraco.Core /// public static class PublishedModelFactoryExtensions { - /// - /// 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/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs index 93a560ff7d..fbf099bcf1 100644 --- a/src/Umbraco.Core/Runtime/IMainDom.cs +++ b/src/Umbraco.Core/Runtime/IMainDom.cs @@ -1,7 +1,7 @@ -using System; +using System; using Umbraco.Core.Hosting; -// TODO: Can't change namespace due to breaking changes, change in netcore + namespace Umbraco.Core { /// @@ -24,18 +24,8 @@ namespace Umbraco.Core /// /// Tries to acquire the MainDom, returns true if successful else false /// - /// - /// bool Acquire(IApplicationShutdownRegistry hostingEnvironment); - /// - /// Registers a resource that requires the current AppDomain to be the main domain to function. - /// - /// An action to execute before the AppDomain releases the main domain status. - /// An optional weight (lower goes first). - /// A value indicating whether it was possible to register. - bool Register(Action release, int weight = 100); - /// /// Registers a resource that requires the current AppDomain to be the main domain to function. /// @@ -45,6 +35,6 @@ namespace Umbraco.Core /// A value indicating whether it was possible to register. /// If registering is successful, then the action /// is guaranteed to execute before the AppDomain releases the main domain status. - bool Register(Action install, Action release, int weight = 100); + bool Register(Action install = null, Action release = null, int weight = 100); } } diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index f1f6ee3afc..07044a9eb7 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)); @@ -65,15 +66,6 @@ namespace Umbraco.Core.Runtime }); } - /// - /// Registers a resource that requires the current AppDomain to be the main domain to function. - /// - /// An action to execute before the AppDomain releases the main domain status. - /// An optional weight (lower goes first). - /// A value indicating whether it was possible to register. - public bool Register(Action release, int weight = 100) - => Register(null, release, weight); - /// /// Registers a resource that requires the current AppDomain to be the main domain to function. /// @@ -83,11 +75,15 @@ namespace Umbraco.Core.Runtime /// A value indicating whether it was possible to register. /// If registering is successful, then the action /// is guaranteed to execute before the AppDomain releases the main domain status. - public bool Register(Action install, Action release, int weight = 100) + public bool Register(Action install = null, Action release = null, int weight = 100) { lock (_locko) { - if (_signaled) return false; + if (_signaled) + { + return false; + } + if (_isMainDom == false) { _logger.LogWarning("Register called when MainDom has not been acquired"); @@ -96,7 +92,10 @@ namespace Umbraco.Core.Runtime install?.Invoke(); if (release != null) + { _callbacks.Add(new KeyValuePair(weight, release)); + } + return true; } } diff --git a/src/Umbraco.Core/SimpleMainDom.cs b/src/Umbraco.Core/SimpleMainDom.cs index a2ef0b8d78..76e6d9a919 100644 --- a/src/Umbraco.Core/SimpleMainDom.cs +++ b/src/Umbraco.Core/SimpleMainDom.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Hosting; @@ -21,10 +21,6 @@ namespace Umbraco.Core // always acquire public bool Acquire(IApplicationShutdownRegistry hostingEnvironment) => true; - /// - public bool Register(Action release, int weight = 100) - => Register(null, release, weight); - /// public bool Register(Action install, Action release, int weight = 100) { diff --git a/src/Umbraco.Infrastructure/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Infrastructure/Scheduling/BackgroundTaskRunner.cs index 0356bdefc6..826ad12c0d 100644 --- a/src/Umbraco.Infrastructure/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Infrastructure/Scheduling/BackgroundTaskRunner.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; @@ -51,7 +51,9 @@ namespace Umbraco.Web.Scheduling internal bool Register() { if (MainDom != null) + { return MainDom.Register(Install, Release); + } // tests Install?.Invoke(); diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index e866fac560..6c6e566acb 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -72,7 +72,7 @@ namespace Umbraco.Web.Search public void Initialize() { //let's deal with shutting down Examine with MainDom - var examineShutdownRegistered = _mainDom.Register(() => + var examineShutdownRegistered = _mainDom.Register(release: () => { using (_profilingLogger.TraceDuration("Examine shutting down")) { diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs index 09c90461ac..0b1a8340a2 100644 --- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs @@ -151,7 +151,7 @@ namespace Umbraco.Core.Sync const int weight = 10; var registered = _mainDom.Register( - () => + release: () => { lock (_locko) { @@ -169,7 +169,7 @@ namespace Umbraco.Core.Sync Logger.LogWarning("The wait lock timed out, application is shutting down. The current instruction batch will be re-processed."); } }, - weight); + weight: weight); if (registered == false) { diff --git a/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs index 618c3d7703..ba43667fbe 100644 --- a/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs +++ b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParser.cs @@ -1,24 +1,32 @@ -using System; using System.Collections.Generic; +using System.Threading.Tasks; using Newtonsoft.Json.Linq; +using Umbraco.Core.Events; namespace Umbraco.Web.WebAssets { + /// + /// Ensures the server variables are included in the outgoing JS script + /// public class ServerVariablesParser { + private const string Token = "##Variables##"; + private readonly IEventAggregator _eventAggregator; + /// - /// Allows developers to add custom variables on parsing + /// Initializes a new instance of the class. /// - public static event EventHandler> Parsing; + public ServerVariablesParser(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator; - internal const string Token = "##Variables##"; - - public static string Parse(Dictionary items) + /// + /// Ensures the server variables in the dictionary are included in the outgoing JS script + /// + public async Task ParseAsync(Dictionary items) { var vars = Resources.ServerVariables; - //Raise event for developers to add custom variables - Parsing?.Invoke(null, items); + // Raise event for developers to add custom variables + await _eventAggregator.PublishAsync(new ServerVariablesParsing(items)); var json = JObject.FromObject(items); return vars.Replace(Token, json.ToString()); diff --git a/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParsing.cs b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParsing.cs new file mode 100644 index 0000000000..2fae2b13e0 --- /dev/null +++ b/src/Umbraco.Infrastructure/WebAssets/ServerVariablesParsing.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Umbraco.Core.Events; + +namespace Umbraco.Web.WebAssets +{ + /// + /// A notification for when server variables are parsing + /// + public class ServerVariablesParsing : INotification + { + /// + /// Initializes a new instance of the class. + /// + public ServerVariablesParsing(IDictionary serverVariables) => ServerVariables = serverVariables; + + /// + /// Gets a mutable dictionary of server variables + /// + public IDictionary ServerVariables { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs index ef5ff14954..c34f4516e4 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ContentTypeModelValidatorBase.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.Extensions.Options; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Editors; @@ -17,62 +18,75 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice { private readonly IOptions _config; - public ContentTypeModelValidatorBase(IOptions config) - { - _config = config; - } + public ContentTypeModelValidatorBase(IOptions config) => _config = config; protected override IEnumerable Validate(TModel model) { - //don't do anything if we're not enabled - if (!_config.Value.Enable) yield break; + // don't do anything if we're not enabled + if (_config.Value.ModelsMode == ModelsMode.Nothing) + { + yield break; + } - //list of reserved/disallowed aliases for content/media/member types - more can be added as the need arises + // list of reserved/disallowed aliases for content/media/member types - more can be added as the need arises var reservedModelAliases = new[] { "system" }; - if(reservedModelAliases.Contains(model.Alias, StringComparer.OrdinalIgnoreCase)) + if (reservedModelAliases.Contains(model.Alias, StringComparer.OrdinalIgnoreCase)) { yield return new ValidationResult($"The model alias {model.Alias} is a reserved term and cannot be used", new[] { "Alias" }); } - var properties = model.Groups.SelectMany(x => x.Properties) + TProperty[] properties = model.Groups.SelectMany(x => x.Properties) .Where(x => x.Inherited == false) .ToArray(); - foreach (var prop in properties) + foreach (TProperty prop in properties) { - var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); + PropertyGroupBasic propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) - yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] - { - $"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias" - }); + { + string[] memberNames = new[] + { + $"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias" + }; - //we need to return the field name with an index so it's wired up correctly + yield return new ValidationResult( + string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), + memberNames); + } + + // we need to return the field name with an index so it's wired up correctly var groupIndex = model.Groups.IndexOf(propertyGroup); var propertyIndex = propertyGroup.Properties.IndexOf(prop); - var validationResult = ValidateProperty(prop, groupIndex, propertyIndex); + ValidationResult validationResult = ValidateProperty(prop, groupIndex, propertyIndex); if (validationResult != null) + { yield return validationResult; + } } } private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex) { - //don't let them match any properties or methods in IPublishedContent - //TODO: There are probably more! + // don't let them match any properties or methods in IPublishedContent + // TODO: There are probably more! var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray(); var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray(); var alias = property.Alias; if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias)) - return new ValidationResult( - $"The alias {alias} is a reserved term and cannot be used", new[] + { + string[] memberNames = new[] { $"Groups[{groupIndex}].Properties[{propertyIndex}].Alias" - }); + }; + + return new ValidationResult( + $"The alias {alias} is a reserved term and cannot be used", + memberNames); + } return null; } diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs index c615559920..6425673916 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs @@ -1,6 +1,7 @@ -using System.Text; +using System.Text; using Microsoft.Extensions.Options; using Umbraco.Configuration; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; @@ -27,34 +28,46 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice public string Text() { - if (!_config.Enable) - return "Version: " + ApiVersion.Current.Version + "
 
ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true')."; - var sb = new StringBuilder(); - sb.Append("Version: "); + sb.Append("

Version: "); sb.Append(ApiVersion.Current.Version); - sb.Append("
 
"); + sb.Append("

"); - sb.Append("ModelsBuilder is enabled, with the following configuration:"); + sb.Append("

ModelsBuilder is enabled, with the following configuration:

"); sb.Append("
    "); - sb.Append("
  • The models factory is "); - sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive - ? "enabled" - : "not enabled. Umbraco will not use models"); - sb.Append(".
  • "); + sb.Append("
  • The models mode is '"); + sb.Append(_config.ModelsMode.ToString()); + sb.Append("'. "); - sb.Append(_config.ModelsMode != ModelsMode.Nothing - ? $"
  • {_config.ModelsMode} models are enabled.
  • " - : "
  • No models mode is specified: models will not be generated.
  • "); + switch (_config.ModelsMode) + { + case ModelsMode.Nothing: + sb.Append("Strongly typed models are not generated. All content and cache will operate from instance of IPublishedContent only."); + break; + case ModelsMode.PureLive: + sb.Append("Strongly typed models are re-generated on startup and anytime schema changes (i.e. Content Type) are made. No recompilation necessary but the generated models are not available to code outside of Razor."); + break; + case ModelsMode.AppData: + sb.Append("Strongly typed models are generated on demand. Recompilation is necessary and models are available to all CSharp code."); + break; + case ModelsMode.LiveAppData: + sb.Append("Strong typed models are generated on demand and anytime schema changes (i.e. Content Type) are made. Recompilation is necessary and models are available to all CSharp code."); + break; + } - sb.Append($"
  • Models namespace is {_config.ModelsNamespace}.
  • "); + sb.Append(""); - sb.Append("
  • Tracking of out-of-date models is "); - sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled"); - sb.Append(".
  • "); + if (_config.ModelsMode != ModelsMode.Nothing) + { + sb.Append($"
  • Models namespace is {_config.ModelsNamespace ?? Constants.ModelsBuilder.DefaultModelsNamespace}.
  • "); + + sb.Append("
  • Tracking of out-of-date models is "); + sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled"); + sb.Append(".
  • "); + } sb.Append("
"); diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs index 0b67498f01..f242854b3f 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs @@ -4,12 +4,10 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Umbraco.Configuration; +using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Hosting; using Umbraco.ModelsBuilder.Embedded.Building; using Umbraco.Web.BackOffice.Controllers; -using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Authorization; namespace Umbraco.ModelsBuilder.Embedded.BackOffice @@ -30,17 +28,14 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice private readonly OutOfDateModelsStatus _outOfDateModels; private readonly ModelsGenerationError _mbErrors; private readonly DashboardReport _dashboardReport; - private readonly IHostingEnvironment _hostingEnvironment; - public ModelsBuilderDashboardController(IOptions config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors, IHostingEnvironment hostingEnvironment) + public ModelsBuilderDashboardController(IOptions config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors) { - //_umbracoServices = umbracoServices; _config = config.Value; _modelGenerator = modelsGenerator; _outOfDateModels = outOfDateModels; _mbErrors = mbErrors; _dashboardReport = new DashboardReport(config, outOfDateModels, mbErrors); - _hostingEnvironment = hostingEnvironment; } // invoked by the dashboard @@ -51,19 +46,12 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice { try { - var config = _config; - - if (!config.ModelsMode.SupportsExplicitGeneration()) + if (!_config.ModelsMode.SupportsExplicitGeneration()) { var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; return Ok(result2); } - var bin = _hostingEnvironment.MapPathContentRoot("~/bin"); - if (bin == null) - throw new PanicException("bin is null."); - - // EnableDllModels will recycle the app domain - but this request will end properly _modelGenerator.GenerateModels(); _mbErrors.Clear(); } @@ -93,45 +81,44 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice // requires that the user is logged into the backoffice and has access to the settings section // beware! the name of the method appears in modelsbuilder.controller.js [HttpGet] // use the http one, not mvc, with api controllers! - public ActionResult GetDashboard() - { - return GetDashboardResult(); - } + public ActionResult GetDashboard() => GetDashboardResult(); - private Dashboard GetDashboardResult() + private Dashboard GetDashboardResult() => new Dashboard { - return new Dashboard - { - Enable = _config.Enable, - Text = _dashboardReport.Text(), - CanGenerate = _dashboardReport.CanGenerate(), - OutOfDateModels = _dashboardReport.AreModelsOutOfDate(), - LastError = _dashboardReport.LastError(), - }; - } + Mode = _config.ModelsMode, + Text = _dashboardReport.Text(), + CanGenerate = _dashboardReport.CanGenerate(), + OutOfDateModels = _dashboardReport.AreModelsOutOfDate(), + LastError = _dashboardReport.LastError(), + }; [DataContract] public class BuildResult { [DataMember(Name = "success")] - public bool Success; + public bool Success { get; set; } + [DataMember(Name = "message")] - public string Message; + public string Message { get; set; } } [DataContract] public class Dashboard { - [DataMember(Name = "enable")] - public bool Enable; + [DataMember(Name = "mode")] + public ModelsMode Mode { get; set; } + [DataMember(Name = "text")] - public string Text; + public string Text { get; set; } + [DataMember(Name = "canGenerate")] - public bool CanGenerate; + public bool CanGenerate { get; set; } + [DataMember(Name = "outOfDateModels")] - public bool OutOfDateModels; + public bool OutOfDateModels { get; set; } + [DataMember(Name = "lastError")] - public string LastError; + public string LastError { get; set; } } public enum OutOfDateType diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs index bc97118ee4..1c5160df18 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; @@ -27,16 +27,20 @@ namespace Umbraco.ModelsBuilder.Embedded.Building { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) + { File.Delete(file); + } - var typeModels = _umbracoService.GetAllTypes(); + System.Collections.Generic.IList typeModels = _umbracoService.GetAllTypes(); var builder = new TextBuilder(_config, typeModels); - foreach (var typeModel in builder.GetModelsToGenerate()) + foreach (TypeModel typeModel in builder.GetModelsToGenerate()) { var sb = new StringBuilder(); builder.Generate(sb, typeModel); diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs deleted file mode 100644 index f7fab098b0..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Umbraco.Core.Composing; -using Umbraco.ModelsBuilder.Embedded.BackOffice; -using Umbraco.Web.Features; - -namespace Umbraco.ModelsBuilder.Embedded.Compose -{ - // TODO: This needs to die, see TODO in ModelsBuilderComposer. This is also no longer used in this netcore - // codebase. Potentially this could be changed to ext methods if necessary that could be used by end users who will - // install the community MB package to disable any built in MB stuff. - - /// - /// Special component used for when MB is disabled with the legacy MB is detected - /// - public sealed class DisabledModelsBuilderComponent : IComponent - { - private readonly UmbracoFeatures _features; - - public DisabledModelsBuilderComponent(UmbracoFeatures features) - { - _features = features; - } - - public void Initialize() - { - //disable the embedded dashboard controller - _features.Disabled.Controllers.Add(); - } - - public void Terminate() - { - } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs deleted file mode 100644 index 94237ccf3d..0000000000 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core.Configuration; -using Umbraco.Core.Composing; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Embedded.Building; -using Umbraco.Core.Configuration.Models; -using Microsoft.Extensions.Options; -using Umbraco.Core.DependencyInjection; - -namespace Umbraco.ModelsBuilder.Embedded.Compose -{ - // TODO: We'll need to change this stuff to IUmbracoBuilder ext and control the order of things there - // This needs to execute before the AddNuCache call - public sealed class ModelsBuilderComposer : ICoreComposer - { - public void Compose(IUmbracoBuilder builder) - { - builder.Components().Append(); - builder.Services.AddSingleton(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => - { - var config = factory.GetRequiredService>().Value; - if (config.ModelsMode == ModelsMode.PureLive) - { - return factory.GetRequiredService(); - // the following would add @using statement in every view so user's don't - // have to do it - however, then noone understands where the @using statement - // comes from, and it cannot be avoided / removed --- DISABLED - // - /* - // no need for @using in views - // note: - // we are NOT using the in-code attribute here, config is required - // because that would require parsing the code... and what if it changes? - // we can AddGlobalImport not sure we can remove one anyways - var modelsNamespace = Configuration.Config.ModelsNamespace; - if (string.IsNullOrWhiteSpace(modelsNamespace)) - modelsNamespace = Configuration.Config.DefaultModelsNamespace; - System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace); - */ - } - else if (config.EnableFactory) - { - var typeLoader = factory.GetRequiredService(); - var publishedValueFallback = factory.GetRequiredService(); - var types = typeLoader - .GetTypes() // element models - .Concat(typeLoader.GetTypes()); // content models - return new PublishedModelFactory(types, publishedValueFallback); - } - - return null; - }); - - } - } -} diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..05df1f9a7b --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +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.Web.Common.DependencyInjection; +using Umbraco.Web.WebAssets; + +/* + * 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 PureLiveModelFactory 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 works, we will use that but need to verify how this affects memory since ideally the old services will be GC'd. + * + * Option C, how its done: + * - Before we add our custom razor services to the container, we make a copy of the services collection which is the snapshot of registered services + * with razor defaults before ours are added. + * - We replace the default implementation of IRazorViewEngine with our own. This is a wrapping service that wraps the default RazorViewEngine instance. + * The ctor for this service takes in a Factory method to re-construct the default RazorViewEngine and all of it's dependency graph. + * - When the PureLive models change, the Factory is invoked and the default razor services are all re-created, thus clearing their caches and the newly + * created instance is wrapped. The RazorViewEngine is the only service that needs to be replaced and wrapped for this to work because it's dependency + * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature. + */ + +namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection +{ + /// + /// Extension methods for for the common Umbraco functionality + /// + public static class UmbracoBuilderExtensions + { + /// + /// Adds umbraco's embedded model builder support + /// + public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder) + { + builder.AddPureLiveRazorEngine(); + 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.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => + { + ModelsBuilderSettings config = factory.GetRequiredService>().Value; + if (config.ModelsMode == ModelsMode.PureLive) + { + return factory.GetRequiredService(); + } + else + { + TypeLoader typeLoader = factory.GetRequiredService(); + IPublishedValueFallback publishedValueFallback = factory.GetRequiredService(); + IEnumerable types = typeLoader + .GetTypes() // element models + .Concat(typeLoader.GetTypes()); // content models + return new PublishedModelFactory(types, publishedValueFallback); + } + }); + + return builder; + } + + /// + /// Can be called if using an external models builder to remove the embedded models builder controller features + /// + public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + return builder; + } + + private static IUmbracoBuilder AddPureLiveRazorEngine(this IUmbracoBuilder builder) + { + // See notes in RefreshingRazorViewEngine for information on what this is doing. + + // 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 + { + builder.Services + }; + + // Replace the default with our custom engine + 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; + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs b/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs new file mode 100644 index 0000000000..8f7e118b6a --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs @@ -0,0 +1,29 @@ +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Core.Events; +using Umbraco.ModelsBuilder.Embedded.BackOffice; +using Umbraco.ModelsBuilder.Embedded.DependencyInjection; +using Umbraco.Web.Features; + +namespace Umbraco.ModelsBuilder.Embedded +{ + /// + /// Used in conjunction with + /// + internal class DisableModelsBuilderNotificationHandler : INotificationHandler + { + private readonly UmbracoFeatures _features; + + public DisableModelsBuilderNotificationHandler(UmbracoFeatures features) => _features = features; + + /// + /// Handles the notification to disable MB controller features + /// + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) + { + // disable the embedded dashboard controller + _features.Disabled.Controllers.Add(); + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs index d7fc051500..11c5d8b605 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs @@ -1,120 +1,134 @@ using System; using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; -using Umbraco.Core.Configuration; +using Microsoft.Extensions.Options; using Umbraco.Configuration; using Umbraco.Core; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Events; using Umbraco.Core.Hosting; +using Umbraco.Extensions; using Umbraco.ModelsBuilder.Embedded.Building; using Umbraco.Web.Cache; -using Umbraco.Core.Configuration.Models; -using Microsoft.Extensions.Options; -using Umbraco.Extensions; +using Umbraco.Web.Common.Lifetime; namespace Umbraco.ModelsBuilder.Embedded { // supports LiveAppData - but not PureLive - public sealed class LiveModelsProvider + public sealed class LiveModelsProvider : INotificationHandler { - private static Mutex _mutex; - private static int _req; + private static int s_req; private readonly ILogger _logger; private readonly ModelsBuilderSettings _config; private readonly ModelsGenerator _modelGenerator; private readonly ModelsGenerationError _mbErrors; - private readonly IHostingEnvironment _hostingEnvironment; + private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; + private readonly IMainDom _mainDom; - // we do not manage pure live here - internal bool IsEnabled => _config.ModelsMode.IsLiveNotPure(); - - public LiveModelsProvider(ILogger logger, IOptions config, ModelsGenerator modelGenerator, ModelsGenerationError mbErrors, IHostingEnvironment hostingEnvironment) + /// + /// Initializes a new instance of the class. + /// + public LiveModelsProvider( + ILogger logger, + IOptions config, + ModelsGenerator modelGenerator, + ModelsGenerationError mbErrors, + IUmbracoRequestLifetime umbracoRequestLifetime, + IMainDom mainDom) { _logger = logger; _config = config.Value ?? throw new ArgumentNullException(nameof(config)); _modelGenerator = modelGenerator; _mbErrors = mbErrors; - _hostingEnvironment = hostingEnvironment; + _umbracoRequestLifetime = umbracoRequestLifetime; + _mainDom = mainDom; } - internal void Install() + // we do not manage pure live here + internal bool IsEnabled => _config.ModelsMode.IsLiveNotPure(); + + /// + /// Handles the notification + /// + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) { - // just be sure + Install(); + return Task.CompletedTask; + } + + private void Install() + { + // don't run if not enabled if (!IsEnabled) + { return; + } - // initialize mutex - // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" - // name is system-wide and must be less than 260 chars - var name = _hostingEnvironment.ApplicationId + "/UmbracoLiveModelsProvider"; + // Must register with maindom in order to function. + // If registration is not successful then events are not bound + // and we also don't generate models. + _mainDom.Register(() => + { + _umbracoRequestLifetime.RequestEnd += (sender, context) => AppEndRequest(context); - _mutex = new Mutex(false, name); //TODO: Replace this with MainDom? Seems we now have 2x implementations of almost the same thing - - // anything changes, and we want to re-generate models. - ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - - // at the end of a request since we're restarting the pool - // NOTE - this does NOT trigger - see module below - //umbracoApplication.EndRequest += GenerateModelsIfRequested; + // anything changes, and we want to re-generate models. + ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + }); } // NOTE - // Using HttpContext Items fails because CacheUpdated triggers within - // some asynchronous backend task where we seem to have no HttpContext. - - // So we use a static (non request-bound) var to register that models + // CacheUpdated triggers within some asynchronous backend task where + // we have no HttpContext. So we use a static (non request-bound) + // var to register that models // need to be generated. Could be by another request. Anyway. We could // have collisions but... you know the risk. private void RequestModelsGeneration(object sender, EventArgs args) { - //HttpContext.Current.Items[this] = true; _logger.LogDebug("Requested to generate models."); - Interlocked.Exchange(ref _req, 1); + Interlocked.Exchange(ref s_req, 1); } - public void GenerateModelsIfRequested() + private void GenerateModelsIfRequested() { - //if (HttpContext.Current.Items[this] == null) return; - if (Interlocked.Exchange(ref _req, 0) == 0) return; + if (Interlocked.Exchange(ref s_req, 0) == 0) + { + return; + } - // cannot use a simple lock here because we don't want another AppDomain - // to generate while we do... and there could be 2 AppDomains if the app restarts. - - try + // cannot proceed unless we are MainDom + if (_mainDom.IsMainDom) { - _logger.LogDebug("Generate models..."); - const int timeout = 2 * 60 * 1000; // 2 mins - _mutex.WaitOne(timeout); // wait until it is safe, and acquire - _logger.LogInformation("Generate models now."); - GenerateModels(); - _mbErrors.Clear(); - _logger.LogInformation("Generated."); + try + { + _logger.LogDebug("Generate models..."); + _logger.LogInformation("Generate models now."); + _modelGenerator.GenerateModels(); + _mbErrors.Clear(); + _logger.LogInformation("Generated."); + } + catch (TimeoutException) + { + _logger.LogWarning("Timeout, models were NOT generated."); + } + catch (Exception e) + { + _mbErrors.Report("Failed to build Live models.", e); + _logger.LogError("Failed to generate models.", e); + } } - catch (TimeoutException) + else { - _logger.LogWarning("Timeout, models were NOT generated."); - } - catch (Exception e) - { - _mbErrors.Report("Failed to build Live models.", e); - _logger.LogError("Failed to generate models.", e); - } - finally - { - _mutex.ReleaseMutex(); // release + // this will only occur if this appdomain was MainDom and it has + // been released while trying to regenerate models. + _logger.LogWarning("Cannot generate models while app is shutting down"); } } - private void GenerateModels() - { - // EnableDllModels will recycle the app domain - but this request will end properly - _modelGenerator.GenerateModels(); - } - - public void AppEndRequest(HttpContext context) + private void AppEndRequest(HttpContext context) { if (context.Request.IsClientSideRequest()) { diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs similarity index 68% rename from src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs rename to src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs index 9a139014fa..37ff1c735a 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs @@ -1,102 +1,110 @@ -using System; +using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; -using Umbraco.Configuration; -using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Events; using Umbraco.Core.IO; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Strings; using Umbraco.Extensions; using Umbraco.ModelsBuilder.Embedded.BackOffice; -using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.WebAssets; -namespace Umbraco.ModelsBuilder.Embedded.Compose +namespace Umbraco.ModelsBuilder.Embedded { - internal class ModelsBuilderComponent : IComponent + + /// + /// Handles and notifications to initialize MB + /// + internal class ModelsBuilderNotificationHandler : INotificationHandler, INotificationHandler { private readonly ModelsBuilderSettings _config; private readonly IShortStringHelper _shortStringHelper; - private readonly LiveModelsProvider _liveModelsProvider; - private readonly OutOfDateModelsStatus _outOfDateModels; private readonly LinkGenerator _linkGenerator; - private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; + private readonly ContentModelBinder _modelBinder; - public ModelsBuilderComponent(IOptions config, IShortStringHelper shortStringHelper, - LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels, LinkGenerator linkGenerator, - IUmbracoRequestLifetime umbracoRequestLifetime) + public ModelsBuilderNotificationHandler( + IOptions config, + IShortStringHelper shortStringHelper, + LinkGenerator linkGenerator, + ContentModelBinder modelBinder) { _config = config.Value; _shortStringHelper = shortStringHelper; - _liveModelsProvider = liveModelsProvider; - _outOfDateModels = outOfDateModels; _shortStringHelper = shortStringHelper; _linkGenerator = linkGenerator; - _umbracoRequestLifetime = umbracoRequestLifetime; + _modelBinder = modelBinder; } - public void Initialize() + /// + /// Handles the notification + /// + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) { // always setup the dashboard // note: UmbracoApiController instances are automatically registered - InstallServerVars(); - _umbracoRequestLifetime.RequestEnd += (sender, context) => _liveModelsProvider.AppEndRequest(context); + _modelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; - ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; - - if (_config.Enable) + if (_config.ModelsMode != ModelsMode.Nothing) + { FileService.SavingTemplate += FileService_SavingTemplate; + } - if (_config.ModelsMode.IsLiveNotPure()) - _liveModelsProvider.Install(); - - if (_config.FlagOutOfDateModels) - _outOfDateModels.Install(); + return Task.CompletedTask; } - public void Terminate() + /// + /// Handles the notification + /// + public Task HandleAsync(ServerVariablesParsing notification, CancellationToken cancellationToken) { - ServerVariablesParser.Parsing -= ServerVariablesParser_Parsing; - ContentModelBinder.ModelBindingException -= ContentModelBinder_ModelBindingException; - FileService.SavingTemplate -= FileService_SavingTemplate; - } + IDictionary serverVars = notification.ServerVariables; - - private void InstallServerVars() - { - // register our URL - for the backoffice API - ServerVariablesParser.Parsing += ServerVariablesParser_Parsing; - } - - private void ServerVariablesParser_Parsing(object sender, Dictionary serverVars) - { if (!serverVars.ContainsKey("umbracoUrls")) + { throw new ArgumentException("Missing umbracoUrls."); + } + var umbracoUrlsObject = serverVars["umbracoUrls"]; if (umbracoUrlsObject == null) + { throw new ArgumentException("Null umbracoUrls"); + } + if (!(umbracoUrlsObject is Dictionary umbracoUrls)) + { throw new ArgumentException("Invalid umbracoUrls"); + } if (!serverVars.ContainsKey("umbracoPlugins")) + { throw new ArgumentException("Missing umbracoPlugins."); - if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) - throw new ArgumentException("Invalid umbracoPlugins"); + } - umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) + { + throw new ArgumentException("Invalid umbracoPlugins"); + } + + umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + + return Task.CompletedTask; } private Dictionary GetModelsBuilderSettings() { var settings = new Dictionary { - {"enabled", _config.Enable} + {"mode", _config.ModelsMode.ToString()} }; return settings; @@ -106,22 +114,22 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose /// Used to check if a template is being created based on a document type, in this case we need to /// ensure the template markup is correct based on the model name of the document type ///
- /// - /// - private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) + private void FileService_SavingTemplate(IFileService sender, SaveEventArgs e) { - // don't do anything if the factory is not enabled - // because, no factory = no models (even if generation is enabled) - if (!_config.EnableFactory) return; - // don't do anything if this special key is not found - if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; + if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) + { + return; + } // ensure we have the content type alias if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) + { throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); + } - foreach (var template in e.SavedEntities) + foreach (ITemplate template in e.SavedEntities) + { // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key // is found, then it means a new template is being created based on the creation of a document type if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) @@ -135,29 +143,31 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose var modelNamespace = _config.ModelsNamespace; // we do not support configuring this at the moment, so just let Umbraco use its default value - //var modelNamespaceAlias = ...; - + // var modelNamespaceAlias = ...; var markup = ViewHelper.GetDefaultFileContent( modelClassName: className, modelNamespace: modelNamespace/*, modelNamespaceAlias: modelNamespaceAlias*/); - //set the template content to the new markup + // set the template content to the new markup template.Content = markup; } + } } private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args) { - var sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); - var modelAttr = args.ModelType.Assembly.GetCustomAttribute(); + ModelsBuilderAssemblyAttribute sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); + ModelsBuilderAssemblyAttribute modelAttr = args.ModelType.Assembly.GetCustomAttribute(); // 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?) args.Message.Append(" The "); @@ -173,6 +183,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose var pureModel = modelAttr.PureLive; if (sourceAttr.PureLive || modelAttr.PureLive) + { if (pureSource == false || pureModel == false) { // only one is pure - report, but better not restart (loops?) @@ -193,6 +204,7 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose : "different versions. The application is in an unstable state and is going to be restarted."); args.Restart = sourceVersion != modelVersion; } + } } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs index 92e0604a16..0a8283b507 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/OutOfDateModelsStatus.cs @@ -1,13 +1,16 @@ -using System.IO; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration; -using Umbraco.Core.Hosting; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Events; +using Umbraco.Core.Hosting; using Umbraco.Web.Cache; namespace Umbraco.ModelsBuilder.Embedded { - public sealed class OutOfDateModelsStatus + public sealed class OutOfDateModelsStatus : INotificationHandler { private readonly ModelsBuilderSettings _config; private readonly IHostingEnvironment _hostingEnvironment; @@ -18,11 +21,38 @@ namespace Umbraco.ModelsBuilder.Embedded _hostingEnvironment = hostingEnvironment; } - internal void Install() + public bool IsEnabled => _config.FlagOutOfDateModels; + + public bool IsOutOfDate { - // just be sure - if (_config.FlagOutOfDateModels == false) + get + { + if (_config.FlagOutOfDateModels == false) + { + return false; + } + + var path = GetFlagPath(); + return path != null && File.Exists(path); + } + } + + /// + /// Handles the notification + /// + public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken) + { + Install(); + return Task.CompletedTask; + } + + private void Install() + { + // don't run if not configured + if (!IsEnabled) + { return; + } ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); @@ -32,35 +62,38 @@ namespace Umbraco.ModelsBuilder.Embedded { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } + return Path.Combine(modelsDirectory, "ood.flag"); } private void Write() { var path = GetFlagPath(); - if (path == null || File.Exists(path)) return; + if (path == null || File.Exists(path)) + { + return; + } + File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); } public void Clear() { - if (_config.FlagOutOfDateModels == false) return; - var path = GetFlagPath(); - if (path == null || !File.Exists(path)) return; - File.Delete(path); - } - - public bool IsEnabled => _config.FlagOutOfDateModels; - - public bool IsOutOfDate - { - get + if (_config.FlagOutOfDateModels == false) { - if (_config.FlagOutOfDateModels == false) return false; - var path = GetFlagPath(); - return path != null && File.Exists(path); + return; } + + var path = GetFlagPath(); + if (path == null || !File.Exists(path)) + { + return; + } + + File.Delete(path); } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index b5997b8419..41a0ac86f9 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -1,23 +1,26 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; +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 Umbraco.Core.Configuration; +using Microsoft.Extensions.Options; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.ModelsBuilder.Embedded.Building; using File = System.IO.File; -using Umbraco.Core.Configuration.Models; -using Microsoft.Extensions.Options; namespace Umbraco.ModelsBuilder.Embedded { @@ -30,21 +33,22 @@ namespace Umbraco.ModelsBuilder.Embedded private readonly IProfilingLogger _profilingLogger; private readonly ILogger _logger; private readonly FileSystemWatcher _watcher; - private int _ver, _skipver; + private int _ver; + private int _skipver; private readonly int _debugLevel; private RoslynCompiler _roslynCompiler; private UmbracoAssemblyLoadContext _currentAssemblyLoadContext; private readonly Lazy _umbracoServices; // fixme: this is because of circular refs :( - private UmbracoServices UmbracoServices => _umbracoServices.Value; - - private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); - private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err", "Compiled" }; - + 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" }; private readonly ModelsBuilderSettings _config; private readonly IHostingEnvironment _hostingEnvironment; 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); public PureLiveModelFactory( Lazy umbracoServices, @@ -53,7 +57,8 @@ namespace Umbraco.ModelsBuilder.Embedded IOptions config, IHostingEnvironment hostingEnvironment, IApplicationShutdownRegistry hostingLifetime, - IPublishedValueFallback publishedValueFallback) + IPublishedValueFallback publishedValueFallback, + ApplicationPartManager applicationPartManager) { _umbracoServices = umbracoServices; _profilingLogger = profilingLogger; @@ -62,14 +67,20 @@ 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 - if (!hostingEnvironment.IsHosted) return; + if (!hostingEnvironment.IsHosted) + { + return; + } var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } // BEWARE! if the watcher is not properly released then for some reason the // BuildManager will start confusing types - using a 'registered object' here @@ -81,30 +92,63 @@ namespace Umbraco.ModelsBuilder.Embedded // get it here, this need to be fast _debugLevel = _config.DebugLevel; + + AssemblyLoadContext.Default.Resolving += OnResolvingDefaultAssemblyLoadContext; } - #region ILivePublishedModelFactory + public event EventHandler ModelsChanged; + + private UmbracoServices UmbracoServices => _umbracoServices.Value; + + /// + /// Gets the currently loaded pure live models assembly + /// + /// + /// Can be null + /// + public Assembly CurrentModelsAssembly { get; private set; } /// public object SyncRoot { get; } = new object(); - /// - public void Refresh() + /// + /// Gets the RoslynCompiler + /// + private RoslynCompiler RoslynCompiler { - ResetModels(); - EnsureModels(); + get + { + if (_roslynCompiler != null) + { + return _roslynCompiler; + } + + _roslynCompiler = new RoslynCompiler(AssemblyLoadContext.All.SelectMany(x => x.Assemblies)); + return _roslynCompiler; + } } - #endregion + /// + public bool Enabled => _config.ModelsMode == ModelsMode.PureLive; - #region IPublishedModelFactory + /// + /// 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 - var infos = EnsureModels()?.ModelInfos; + Dictionary infos = EnsureModels()?.ModelInfos; if (infos == null) + { return element; + } // be case-insensitive var contentTypeAlias = element.ContentType.Alias; @@ -120,7 +164,7 @@ namespace Umbraco.ModelsBuilder.Embedded // NOT when building models public Type MapModelType(Type type) { - var infos = EnsureModels(); + Infos infos = EnsureModels(); return ModelType.Map(type, infos.ModelTypeMap); } @@ -128,93 +172,39 @@ namespace Umbraco.ModelsBuilder.Embedded // NOT when building models public IList CreateModelList(string alias) { - var infos = EnsureModels(); + Infos infos = EnsureModels(); // fail fast if (infos == null) + { return new List(); + } - if (!infos.ModelInfos.TryGetValue(alias, out var modelInfo)) + if (!infos.ModelInfos.TryGetValue(alias, out ModelInfo modelInfo)) + { return new List(); + } - var ctor = modelInfo.ListCtor; - if (ctor != null) return ctor(); + Func ctor = modelInfo.ListCtor; + if (ctor != null) + { + return ctor(); + } - var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); + Type listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); return ctor(); } - /// - public bool Enabled => _config.Enable; - /// public void Reset() { - if (_config.Enable) + if (Enabled) + { ResetModels(); + } } - #endregion - - #region Compilation - // deadlock note - // - // when RazorBuildProvider_CodeGenerationStarted runs, the thread has Monitor.Enter-ed the BuildManager - // singleton instance, through a call to CompilationLock.GetLock in BuildManager.GetVPathBuildResultInternal, - // and now wants to lock _locker. - // when EnsureModels runs, the thread locks _locker and then wants BuildManager to compile, which in turns - // requires that the BuildManager can Monitor.Enter-ed itself. - // so: - // - // T1 - needs to ensure models, locks _locker - // T2 - needs to compile a view, locks BuildManager - // hits RazorBuildProvider_CodeGenerationStarted - // wants to lock _locker, wait - // T1 - needs to compile models, using BuildManager - // wants to lock itself, wait - // - // - // until ASP.NET kills the long-running request (thread abort) - // - // problem is, we *want* to suspend views compilation while the models assembly is being changed else we - // end up with views compiled and cached with the old assembly, while models come from the new assembly, - // which gives more YSOD. so we *have* to lock _locker in RazorBuildProvider_CodeGenerationStarted. - // - // one "easy" solution consists in locking the BuildManager *before* _locker in EnsureModels, thus ensuring - // we always lock in the same order, and getting rid of deadlocks - but that requires having access to the - // current BuildManager instance, which is BuildManager.TheBuildManager, which is an internal property. - // - // well, that's what we are doing in this class' TheBuildManager property, using reflection. - - // private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e) - // { - // try - // { - // _locker.EnterReadLock(); - // - // // just be safe - can happen if the first view is not an Umbraco view, - // // or if something went wrong and we don't have an assembly at all - // if (_modelsAssembly == null) return; - // - // if (_debugLevel > 0) - // _logger.Debug("RazorBuildProvider.CodeGenerationStarted"); - // if (!(sender is RazorBuildProvider provider)) return; - // - // // add the assembly, and add a dependency to a text file that will change on each - // // compilation as in some environments (could not figure which/why) the BuildManager - // // would not re-compile the views when the models assembly is rebuilt. - // provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly); - // provider.AddVirtualPathDependency(ProjVirt); - // } - // finally - // { - // if (_locker.IsReadLockHeld) - // _locker.ExitReadLock(); - // } - // } - - // tells the factory that it should build a new generation of models private void ResetModels() { @@ -229,30 +219,30 @@ namespace Umbraco.ModelsBuilder.Embedded var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } // clear stuff var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); - if (File.Exists(dllPathFile)) File.Delete(dllPathFile); - if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); + if (File.Exists(dllPathFile)) + { + File.Delete(dllPathFile); + } + + if (File.Exists(modelsHashFile)) + { + File.Delete(modelsHashFile); + } } finally { if (_locker.IsWriteLockHeld) + { _locker.ExitWriteLock(); - } - } - - // gets the RoslynCompiler - private RoslynCompiler RoslynCompiler - { - get - { - if (_roslynCompiler != null) return _roslynCompiler; - _roslynCompiler = new RoslynCompiler(System.Runtime.Loader.AssemblyLoadContext.All.SelectMany(x => x.Assemblies)); - return _roslynCompiler; + } } } @@ -260,50 +250,57 @@ namespace Umbraco.ModelsBuilder.Embedded internal Infos EnsureModels() { if (_debugLevel > 0) + { _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(); + } } - var roslynLocked = false; try { - // 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) return _infos; + 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.DebugDuration("Get models.", "Got models.")) { try { - var assembly = GetModelsAssembly(_pendingRebuild); + Assembly assembly = GetModelsAssembly(_pendingRebuild); - // 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; + CurrentModelsAssembly = assembly; - var types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); + // Raise the model changing event. + // 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 unecessarily 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. + ModelsChanged?.Invoke(this, new EventArgs()); + + IEnumerable types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); _infos = RegisterModels(types); _errors.Clear(); } @@ -317,6 +314,7 @@ namespace Umbraco.ModelsBuilder.Embedded } finally { + CurrentModelsAssembly = null; _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; } } @@ -330,43 +328,73 @@ namespace Umbraco.ModelsBuilder.Embedded finally { if (_locker.IsWriteLockHeld) + { _locker.ExitWriteLock(); + } + if (_locker.IsUpgradeableReadLockHeld) + { _locker.ExitUpgradeableReadLock(); - if (roslynLocked) - Monitor.Exit(RoslynCompiler); + } } } + // This is NOT thread safe but it is only called from within a lock private Assembly ReloadAssembly(string pathToAssembly) { // If there's a current AssemblyLoadContext, unload it before creating a new one. - if(!(_currentAssemblyLoadContext is null)) + 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(); - // 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)) + // 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 = _currentAssemblyLoadContext.LoadFromAssemblyPath(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)) { - return _currentAssemblyLoadContext.LoadFromStream(fs); + _applicationPartManager.ApplicationParts.Add(applicationPart); } + + return assembly; } + // This is NOT thread safe but it is only called from within a lock private Assembly GetModelsAssembly(bool forceRebuild) { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } - var typeModels = UmbracoServices.GetAllTypes(); + IList typeModels = UmbracoServices.GetAllTypes(); var currentHash = TypeModelHasher.Hash(typeModels); var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); @@ -375,7 +403,6 @@ namespace Umbraco.ModelsBuilder.Embedded // caching the generated models speeds up booting // currentHash hashes both the types & the user's partials - if (!forceRebuild) { _logger.LogDebug("Looking for cached models."); @@ -415,7 +442,7 @@ namespace Umbraco.ModelsBuilder.Embedded { assembly = ReloadAssembly(dllPath); - var attr = assembly.GetCustomAttribute(); + ModelsBuilderAssemblyAttribute attr = assembly.GetCustomAttribute(); if (attr != null && attr.PureLive && attr.SourceHash == currentHash) { // if we were to resume at that revision, then _ver would keep increasing @@ -431,17 +458,23 @@ namespace Umbraco.ModelsBuilder.Embedded _logger.LogDebug("Cached models dll cannot be loaded (invalid assembly)."); } else if (!File.Exists(dllPath)) + { _logger.LogDebug("Cached models dll does not exist."); + } else if (File.Exists(dllPath + ".delete")) + { _logger.LogDebug("Cached models dll is marked for deletion."); + } else + { _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); - var match = AssemblyVersionRegex.Match(text); + Match match = s_assemblyVersionRegex.Match(text); if (match.Success) { text = text.Replace(match.Value, "AssemblyVersion(\"0.0.0." + _ver + "\")"); @@ -478,8 +511,9 @@ namespace Umbraco.ModelsBuilder.Embedded // AssemblyVersion is so that we have a different version for each rebuild var ver = _ver == _skipver ? ++_ver : _ver; _ver++; - code = code.Replace("//ASSATTR", $@"[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] -[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"); + string mbAssemblyDirective = $@"[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] +[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"; + code = code.Replace("//ASSATTR", mbAssemblyDirective); File.WriteAllText(modelsSrcFile, code); // generate proj, save @@ -515,21 +549,20 @@ namespace Umbraco.ModelsBuilder.Embedded if (File.Exists(dllPathFile)) { var dllPath = File.ReadAllText(dllPathFile); - var dirInfo = new DirectoryInfo(dllPath).Parent; - var files = dirInfo.GetFiles().Where(f => f.FullName != dllPath); - foreach(var file in files) + DirectoryInfo dirInfo = new DirectoryInfo(dllPath).Parent; + IEnumerable files = dirInfo.GetFiles().Where(f => f.FullName != dllPath); + foreach (FileInfo file in files) { try { File.Delete(file.FullName); } - catch(UnauthorizedAccessException) + catch (UnauthorizedAccessException) { // The file is in use, we'll try again next time... // This shouldn't happen anymore. } } - } } @@ -537,7 +570,10 @@ namespace Umbraco.ModelsBuilder.Embedded { var dirInfo = new DirectoryInfo(Path.Combine(_config.ModelsDirectoryAbsolute(_hostingEnvironment), "Compiled")); if (!dirInfo.Exists) + { Directory.CreateDirectory(dirInfo.FullName); + } + return Path.Combine(dirInfo.FullName, $"generated.cs{currentHash}.dll"); } @@ -551,51 +587,69 @@ namespace Umbraco.ModelsBuilder.Embedded // 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); + 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 types) { - var ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) }; + Type[] ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) }; var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); var map = new Dictionary(); - foreach (var type in types) + foreach (Type type in types) { ConstructorInfo constructor = null; Type parameterType = null; - foreach (var ctor in type.GetConstructors()) + foreach (ConstructorInfo ctor in type.GetConstructors()) { - var parms = ctor.GetParameters(); + 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."); + } - var attribute = type.GetCustomAttribute(false); + PublishedModelAttribute attribute = type.GetCustomAttribute(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); - var gen = meth.GetILGenerator(); + ILGenerator gen = meth.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldarg_1); gen.Emit(OpCodes.Newobj, constructor); @@ -613,10 +667,14 @@ namespace Umbraco.ModelsBuilder.Embedded { var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); if (!Directory.Exists(modelsDirectory)) + { Directory.CreateDirectory(modelsDirectory); + } foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) + { File.Delete(file); + } var builder = new TextBuilder(_config, typeModels); @@ -627,9 +685,6 @@ namespace Umbraco.ModelsBuilder.Embedded return code; } - private static readonly Regex UsingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); - private static readonly Regex AattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); - private static string GenerateModelsProj(IDictionary files) { // ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj @@ -637,21 +692,25 @@ namespace Umbraco.ModelsBuilder.Embedded // group all 'using' at the top of the file (else fails) var usings = new List(); - foreach (var k in files.Keys.ToList()) - files[k] = UsingRegex.Replace(files[k], m => + 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(); - foreach (var k in files.Keys.ToList()) - files[k] = AattrRegex.Replace(files[k], m => + 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()) @@ -660,14 +719,16 @@ namespace Umbraco.ModelsBuilder.Embedded 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 (var f in files) + foreach (KeyValuePair f in files) { text.Append("// FILE: "); text.Append(f.Key); @@ -675,29 +736,12 @@ namespace Umbraco.ModelsBuilder.Embedded text.Append(f.Value); text.Append("\r\n\r\n\r\n"); } + text.Append("// EOF\r\n"); return text.ToString(); } - internal class Infos - { - public Dictionary ModelTypeMap { get; set; } - public Dictionary ModelInfos { get; set; } - } - - internal class ModelInfo - { - public Type ParameterType { get; set; } - public Func Ctor { get; set; } - public Type ModelType { get; set; } - public Func ListCtor { get; set; } - } - - #endregion - - #region Watching - private void WatcherOnChanged(object sender, FileSystemEventArgs args) { var changed = args.Name; @@ -715,13 +759,17 @@ namespace Umbraco.ModelsBuilder.Embedded //} // always ignore our own file changes - if (OurFiles.Contains(changed)) + 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) @@ -731,6 +779,22 @@ namespace Umbraco.ModelsBuilder.Embedded _hostingLifetime.UnregisterObject(this); } - #endregion + internal class Infos + { + public Dictionary ModelTypeMap { get; set; } + + public Dictionary ModelInfos { get; set; } + } + + internal class ModelInfo + { + public Type ParameterType { get; set; } + + public Func Ctor { get; set; } + + public Type ModelType { get; set; } + + public Func ListCtor { get; set; } + } } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs b/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs new file mode 100644 index 0000000000..ad82d1d7b3 --- /dev/null +++ b/src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs @@ -0,0 +1,176 @@ +using System; +using System.Threading; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.ViewEngines; + +/* + * 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 PureLiveModelFactory 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. + * + * 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 works, we will use that but need to verify how this affects memory since ideally the old services will be GC'd. + * + * Option C, how its done: + * - Before we add our custom razor services to the container, we make a copy of the services collection which is the snapshot of registered services + * with razor defaults before ours are added. + * - We replace the default implementation of IRazorViewEngine with our own. This is a wrapping service that wraps the default RazorViewEngine instance. + * The ctor for this service takes in a Factory method to re-construct the default RazorViewEngine and all of it's dependency graph. + * - When the PureLive models change, the Factory is invoked and the default razor services are all re-created, thus clearing their caches and the newly + * created instance is wrapped. The RazorViewEngine is the only service that needs to be replaced and wrapped for this to work because it's dependency + * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature. + */ + +namespace Umbraco.ModelsBuilder.Embedded +{ + /// + /// Custom that wraps aspnetcore's default implementation + /// + /// + /// This is used so that when new PureLive models are built, the entire razor stack is re-constructed so all razor + /// caches and assembly references, etc... are cleared. + /// + internal class RefreshingRazorViewEngine : IRazorViewEngine + { + private IRazorViewEngine _current; + private readonly PureLiveModelFactory _pureLiveModelFactory; + private readonly Func _defaultRazorViewEngineFactory; + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// A factory method used to re-construct the default aspnetcore + /// + /// The + public RefreshingRazorViewEngine(Func defaultRazorViewEngineFactory, PureLiveModelFactory pureLiveModelFactory) + { + _pureLiveModelFactory = pureLiveModelFactory; + _defaultRazorViewEngineFactory = defaultRazorViewEngineFactory; + _current = _defaultRazorViewEngineFactory(); + _pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged; + } + + /// + /// When the pure live models change, re-construct the razor stack + /// + private void PureLiveModelFactory_ModelsChanged(object sender, EventArgs e) + { + _locker.EnterWriteLock(); + try + { + _current = _defaultRazorViewEngineFactory(); + } + finally + { + _locker.ExitWriteLock(); + } + } + + public RazorPageResult FindPage(ActionContext context, string pageName) + { + _locker.EnterReadLock(); + try + { + return _current.FindPage(context, pageName); + } + finally + { + _locker.ExitReadLock(); + } + } + + public string GetAbsolutePath(string executingFilePath, string pagePath) + { + _locker.EnterReadLock(); + try + { + return _current.GetAbsolutePath(executingFilePath, pagePath); + } + finally + { + _locker.ExitReadLock(); + } + } + + public RazorPageResult GetPage(string executingFilePath, string pagePath) + { + _locker.EnterReadLock(); + try + { + return _current.GetPage(executingFilePath, pagePath); + } + finally + { + _locker.ExitReadLock(); + } + } + + public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) + { + _locker.EnterReadLock(); + try + { + return _current.FindView(context, viewName, isMainPage); + + } + finally + { + _locker.ExitReadLock(); + } + } + + public ViewEngineResult GetView(string executingFilePath, string viewPath, bool isMainPage) + { + _locker.EnterReadLock(); + try + { + return _current.GetView(executingFilePath, viewPath, isMainPage); + } + finally + { + _locker.ExitReadLock(); + } + } + } +} diff --git a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs index 7f1443b156..1321078f98 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/RoslynCompiler.cs @@ -1,24 +1,29 @@ -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 { 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; @@ -28,7 +33,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 +59,15 @@ namespace Umbraco.ModelsBuilder.Embedded var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); - var compilation = CSharpCompilation.Create("ModelsGeneratedAssembly", + var compilation = CSharpCompilation.Create( + GeneratedAssemblyName, 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 acdbc14acf..de87c597fc 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj +++ b/src/Umbraco.ModelsBuilder.Embedded/Umbraco.ModelsBuilder.Embedded.csproj @@ -12,6 +12,8 @@ + + diff --git a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs b/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs index b7feeaaaeb..d89714adbe 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/UmbracoAssemblyLoadContext.cs @@ -1,26 +1,23 @@ -using System; -using System.Collections.Generic; using System.Reflection; using System.Runtime.Loader; -using System.Text; namespace Umbraco.ModelsBuilder.Embedded { - class UmbracoAssemblyLoadContext : AssemblyLoadContext + internal class UmbracoAssemblyLoadContext : AssemblyLoadContext { - /// + /// Initializes a new instance of the class. + /// + /// /// Collectible AssemblyLoadContext used to load in the compiled generated models. /// Must be a collectible assembly in order to be able to be unloaded. - /// - public UmbracoAssemblyLoadContext() : base(isCollectible: true) + /// + public UmbracoAssemblyLoadContext() + : base(isCollectible: true) { - } - protected override Assembly Load(AssemblyName assemblyName) - { - return null; - } + // we never load anything directly by assembly name. This method will never be called + protected override Assembly Load(AssemblyName assemblyName) => null; } } 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.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.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index 95387ec97e..c542739361 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -176,6 +176,7 @@ namespace Umbraco.Tests.Persistence Assert.IsNull(exceptions[i]); } + [Retry(5)] // TODO make this test non-flaky. [Test] public void DeadLockTest() { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs index 5049f6cc0b..865a77fbdc 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeLoaderTests.cs @@ -196,6 +196,7 @@ AnotherContentFinder Assert.IsNotNull(_typeLoader.ReadCache()); // works } + [Retry(5)] // TODO make this test non-flaky. [Test] public void Create_Cached_Plugin_File() { diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs index fd9f28946f..6ca8bb588c 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AngularIntegration/ServerVariablesParserTests.cs @@ -2,8 +2,11 @@ // See LICENSE for more details. using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Events; using Umbraco.Web.WebAssets; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.AngularIntegration @@ -12,8 +15,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.AngularIntegration public class ServerVariablesParserTests { [Test] - public void Parse() + public async Task Parse() { + var parser = new ServerVariablesParser(Mock.Of()); + var d = new Dictionary { { "test1", "Test 1" }, @@ -23,7 +28,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.AngularIntegration { "test5", "Test 5" } }; - var output = ServerVariablesParser.Parse(d).StripWhitespace(); + var output = (await parser.ParseAsync(d)).StripWhitespace(); Assert.IsTrue(output.Contains(@"Umbraco.Sys.ServerVariables = { ""test1"": ""Test 1"", diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs index 90f491b15f..f6f7eaa2d5 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Views/UmbracoViewPageTests.cs @@ -311,9 +311,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views public class TestPage : UmbracoViewPage { + private readonly ContentModelBinder _modelBinder = new ContentModelBinder(); + public override Task ExecuteAsync() => throw new NotImplementedException(); - public void SetViewData(ViewDataDictionary viewData) => ViewData = (ViewDataDictionary)BindViewData(viewData); + public void SetViewData(ViewDataDictionary viewData) => ViewData = (ViewDataDictionary)BindViewData(_modelBinder, viewData); } public class RenderModelTestPage : TestPage diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 34d3a96ca3..63e7e09513 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -63,6 +63,7 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IBackOfficeExternalLoginProviders _externalLogins; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; + private readonly ServerVariablesParser _serverVariables; public BackOfficeController( IBackOfficeUserManager userManager, @@ -79,7 +80,8 @@ namespace Umbraco.Web.BackOffice.Controllers IJsonSerializer jsonSerializer, IBackOfficeExternalLoginProviders externalLogins, IHttpContextAccessor httpContextAccessor, - IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions) + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, + ServerVariablesParser serverVariables) { _userManager = userManager; _runtimeMinifier = runtimeMinifier; @@ -96,6 +98,7 @@ namespace Umbraco.Web.BackOffice.Controllers _externalLogins = externalLogins; _httpContextAccessor = httpContextAccessor; _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; + _serverVariables = serverVariables; } [HttpGet] @@ -266,13 +269,12 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// Returns the JavaScript object representing the static server variables javascript object /// - /// [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)] [MinifyJavaScriptResult(Order = 1)] public async Task ServerVariables() { - //cache the result if debugging is disabled - var serverVars = ServerVariablesParser.Parse(await _backOfficeServerVariables.GetServerVariablesAsync()); + // cache the result if debugging is disabled + var serverVars = await _serverVariables.ParseAsync(await _backOfficeServerVariables.GetServerVariablesAsync()); var result = _hostingEnvironment.IsDebugMode ? serverVars : _appCaches.RuntimeCache.GetCacheItem( diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index eec088478f..c1aa0e83de 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -128,7 +128,7 @@ namespace Umbraco.Web.BackOffice.Controllers // use a numeric URL because content may not be in cache and so .Url would fail var query = culture.IsNullOrWhiteSpace() ? string.Empty : $"?culture={culture}"; - return RedirectPermanent($"../../{id}.aspx{query}"); + return RedirectPermanent($"../../{id}{query}"); } public ActionResult EnterPreview(int id) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 92b604caaf..6587f3c6e4 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -19,6 +19,7 @@ using Umbraco.Web.BackOffice.Services; using Umbraco.Web.BackOffice.Trees; using Umbraco.Web.Common.Authorization; using Umbraco.Web.Common.DependencyInjection; +using Umbraco.Web.WebAssets; namespace Umbraco.Web.BackOffice.DependencyInjection { @@ -136,6 +137,7 @@ namespace Umbraco.Web.BackOffice.DependencyInjection public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder) { + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs index 512f56179a..cd429ec458 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs @@ -1,5 +1,6 @@ using System; using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; @@ -10,6 +11,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; @@ -41,8 +43,6 @@ namespace Umbraco.Web.Common.AspNetCore private IIOHelper IOHelper => Context.RequestServices.GetRequiredService(); - private ContentModelBinder ContentModelBinder => new ContentModelBinder(); - /// /// Gets the /// @@ -56,7 +56,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.RequestServices.GetRequiredService(), ctx.ViewData); base.ViewContext = ctx; } } @@ -123,8 +123,18 @@ 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(ContentModelBinder contentModelBinder, ViewDataDictionary viewData) { + if (contentModelBinder is null) + { + throw new ArgumentNullException(nameof(contentModelBinder)); + } + + if (viewData is null) + { + throw new ArgumentNullException(nameof(viewData)); + } + // check if it's already the correct type and continue if it is if (viewData is ViewDataDictionary vdd) { @@ -150,7 +160,7 @@ namespace Umbraco.Web.Common.AspNetCore // bind the model 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 fab0866cdb..18d027be2d 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -274,6 +274,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/Filters/ModelBindingExceptionAttribute.cs b/src/Umbraco.Web.Common/Filters/ModelBindingExceptionAttribute.cs index 6e9d6d29a7..d22ac70d8d 100644 --- a/src/Umbraco.Web.Common/Filters/ModelBindingExceptionAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/ModelBindingExceptionAttribute.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.Common.Filters public void OnException(ExceptionContext filterContext) { var disabled = _exceptionFilterSettings?.Disabled ?? false; - if (_publishedModelFactory.IsLiveFactory() + if (_publishedModelFactory.IsLiveFactoryEnabled() && !disabled && !filterContext.ExceptionHandled && (filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException) diff --git a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs index 616a75bfe7..38ad66121e 100644 --- a/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs +++ b/src/Umbraco.Web.Common/Lifetime/IUmbracoRequestLifetime.cs @@ -1,10 +1,11 @@ -using System; +using System; using Microsoft.AspNetCore.Http; namespace Umbraco.Web.Common.Lifetime { + // TODO: Should be killed and replaced with IEventAggregator public interface IUmbracoRequestLifetime - { + { event EventHandler RequestStart; event EventHandler RequestEnd; } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs index e2a7c35daa..4993b68568 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoBackOfficeIdentityCultureProvider.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Common.Localization return NullProviderCultureResult; } - lock(_locker) + lock (_locker) { // We need to dynamically change the supported cultures since we won't ever know what languages are used since // they are dynamic within Umbraco. 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.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index 9cec15d519..eb401ebe5f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -28,7 +28,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje generateModels: function () { var deferred = $q.defer(); var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null; - var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; + var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.mode !== "Nothing"; if (modelsBuilderEnabled && modelsResource) { modelsResource.buildModels().then(function (result) { deferred.resolve(result); @@ -49,7 +49,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje checkModelsBuilderStatus: function () { var deferred = $q.defer(); var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null; - var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); + var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.mode !== "Nothing"); if (modelsBuilderEnabled && modelsResource) { modelsResource.getModelsOutOfDateStatus().then(function (result) { diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 46f7b2d7ae..3029423bf5 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Core.DependencyInjection; using Umbraco.Extensions; +using Umbraco.ModelsBuilder.Embedded.DependencyInjection; using Umbraco.Web.BackOffice.DependencyInjection; using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.DependencyInjection; @@ -48,6 +49,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 diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 045743dfec..979f476441 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -1,4 +1,4 @@ - + net5.0 @@ -20,68 +20,98 @@ + + netcoreapp3.1 + Umbraco.Web.UI.NetCore + latest + - - - - - - - - - - - + + bin\Release\Umbraco.Web.UI.NetCore.xml + + + true + + + + + + + + + - - - - + + + + + + + + - - - + + + + + + + + - - - - + + + + + + + + + + + + + + + + - - - true - PreserveNewest - Always - - - true - PreserveNewest - Always - - - - - + + + true + PreserveNewest + Always + + + true + PreserveNewest + Always + + + + + + + + + - - - - - - - - - - - - + + + + + + + + false + diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index 44d148acdc..a3e57978da 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -67,7 +67,7 @@ }, "ModelsBuilder": { "ModelsMode": "PureLive", - "Enable": false + "Enable": true } } } diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 98b823647d..d8075d8867 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -261,7 +261,6 @@ - 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>();