Merge remote-tracking branch 'origin/netcore/netcore' into netcore/feature/net5

This commit is contained in:
Bjarke Berg
2021-01-21 09:37:44 +01:00
52 changed files with 1186 additions and 725 deletions

1
.gitignore vendored
View File

@@ -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/*

View File

@@ -14,19 +14,10 @@ namespace Umbraco.Core.Configuration.Models
private static string DefaultModelsDirectory => "~/umbraco/models";
/// <summary>
/// Gets or sets a value indicating whether the whole models experience is enabled.
/// </summary>
/// <remarks>
/// <para>If this is false then absolutely nothing happens.</para>
/// <para>Default value is <c>false</c> which means that unless we have this setting, nothing happens.</para>
/// </remarks>
public bool Enable { get; set; } = false;
/// <summary>
/// Gets or sets a value for the models mode.
/// </summary>
public ModelsMode ModelsMode { get; set; } = ModelsMode.Nothing;
public ModelsMode ModelsMode { get; set; } = ModelsMode.PureLive;
/// <summary>
/// Gets or sets a value for models namespace.
@@ -34,12 +25,6 @@ namespace Umbraco.Core.Configuration.Models
/// <remarks>That value could be overriden by other (attribute in user's code...). Return default if no value was supplied.</remarks>
public string ModelsNamespace { get; set; }
/// <summary>
/// Gets or sets a value indicating whether we should enable the models factory.
/// </summary>
/// <remarks>Default value is <c>true</c> because no factory is enabled by default in Umbraco.</remarks>
public bool EnableFactory { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether we should flag out-of-date models.
/// </summary>

View File

@@ -1,4 +1,4 @@
namespace Umbraco.Core.Configuration
namespace Umbraco.Core.Configuration
{
/// <summary>
/// Defines the models generation modes.
@@ -6,9 +6,12 @@
public enum ModelsMode
{
/// <summary>
/// Do not generate models.
/// Do not generate strongly typed models.
/// </summary>
Nothing = 0, // default value
/// <remarks>
/// This means that only IPublishedContent instances will be used.
/// </remarks>
Nothing = 0,
/// <summary>
/// Generate models in memory.

View File

@@ -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.
/// </summary>
public static bool IsLive(this ModelsMode modelsMode)
{
return
modelsMode == ModelsMode.PureLive
|| modelsMode == ModelsMode.LiveAppData;
}
=> modelsMode == ModelsMode.PureLive || modelsMode == ModelsMode.LiveAppData;
/// <summary>
/// Gets a value indicating whether the mode is LiveAnything but not PureLive.
/// </summary>
public static bool IsLiveNotPure(this ModelsMode modelsMode)
{
return
modelsMode == ModelsMode.LiveAppData;
}
=> modelsMode == ModelsMode.LiveAppData;
/// <summary>
/// Gets a value indicating whether the mode supports explicit generation (as opposed to pure live).
/// </summary>
public static bool SupportsExplicitGeneration(this ModelsMode modelsMode)
{
return
modelsMode == ModelsMode.AppData
|| modelsMode == ModelsMode.LiveAppData;
}
=> modelsMode == ModelsMode.AppData || modelsMode == ModelsMode.LiveAppData;
}
}

View File

@@ -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<TNotification>), typeof(TNotificationHandler), ServiceLifetime.Transient);
var descriptor = new UniqueServiceDescriptor(typeof(INotificationHandler<TNotification>), 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<UniqueServiceDescriptor>
{
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<Type>.Default.Equals(ServiceType, other.ServiceType) && EqualityComparer<Type>.Default.Equals(ImplementationType, other.ImplementationType) && EqualityComparer<object>.Default.Equals(ImplementationInstance, other.ImplementationInstance) && EqualityComparer<Func<IServiceProvider, object>>.Default.Equals(ImplementationFactory, other.ImplementationFactory);
public override int GetHashCode()
{
int hashCode = 493849952;
hashCode = hashCode * -1521134295 + Lifetime.GetHashCode();
hashCode = hashCode * -1521134295 + EqualityComparer<Type>.Default.GetHashCode(ServiceType);
hashCode = hashCode * -1521134295 + EqualityComparer<Type>.Default.GetHashCode(ImplementationType);
hashCode = hashCode * -1521134295 + EqualityComparer<object>.Default.GetHashCode(ImplementationInstance);
hashCode = hashCode * -1521134295 + EqualityComparer<Func<IServiceProvider, object>>.Default.GetHashCode(ImplementationFactory);
return hashCode;
}
}
}
}

View File

@@ -1,4 +1,4 @@
namespace Umbraco.Core.Models.PublishedContent
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
@@ -11,15 +11,6 @@
/// </summary>
object SyncRoot { get; }
/// <summary>
/// Refreshes the factory.
/// </summary>
/// <remarks>
/// <para>This will typically re-compiled models/classes into a new DLL that are used to populate the cache.</para>
/// <para>This is called prior to refreshing the cache.</para>
/// </remarks>
void Refresh();
/// <summary>
/// Tells the factory that it should build a new generation of models
/// </summary>

View File

@@ -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
/// </summary>
public static class PublishedModelFactoryExtensions
{
/// <summary>
/// Returns true if the current <see cref="IPublishedModelFactory"/> is an implementation of <see cref="ILivePublishedModelFactory"/>
/// </summary>
/// <param name="factory"></param>
/// <returns></returns>
public static bool IsLiveFactory(this IPublishedModelFactory factory) => factory is ILivePublishedModelFactory;
/// <summary>
/// Returns true if the current <see cref="IPublishedModelFactory"/> is an implementation of <see cref="ILivePublishedModelFactory2"/> and is enabled
/// </summary>
/// <param name="factory"></param>
/// <returns></returns>
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();
}
}
/// <summary>
/// Sets a flag to reset the ModelsBuilder models if the <see cref="IPublishedModelFactory"/> is <see cref="ILivePublishedModelFactory"/>
/// </summary>
/// <param name="factory"></param>
/// <param name="action"></param>
/// <remarks>
/// This does not recompile the pure live models, only sets a flag to tell models builder to recompile when they are requested.
/// </remarks>

View File

@@ -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
{
/// <summary>
@@ -24,18 +24,8 @@ namespace Umbraco.Core
/// <summary>
/// Tries to acquire the MainDom, returns true if successful else false
/// </summary>
/// <param name="hostingEnvironment"></param>
/// <returns></returns>
bool Acquire(IApplicationShutdownRegistry hostingEnvironment);
/// <summary>
/// Registers a resource that requires the current AppDomain to be the main domain to function.
/// </summary>
/// <param name="release">An action to execute before the AppDomain releases the main domain status.</param>
/// <param name="weight">An optional weight (lower goes first).</param>
/// <returns>A value indicating whether it was possible to register.</returns>
bool Register(Action release, int weight = 100);
/// <summary>
/// Registers a resource that requires the current AppDomain to be the main domain to function.
/// </summary>
@@ -45,6 +35,6 @@ namespace Umbraco.Core
/// <returns>A value indicating whether it was possible to register.</returns>
/// <remarks>If registering is successful, then the <paramref name="install"/> action
/// is guaranteed to execute before the AppDomain releases the main domain status.</remarks>
bool Register(Action install, Action release, int weight = 100);
bool Register(Action install = null, Action release = null, int weight = 100);
}
}

View File

@@ -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
/// <inheritdoc/>
public bool Acquire(IApplicationShutdownRegistry hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
@@ -65,15 +66,6 @@ namespace Umbraco.Core.Runtime
});
}
/// <summary>
/// Registers a resource that requires the current AppDomain to be the main domain to function.
/// </summary>
/// <param name="release">An action to execute before the AppDomain releases the main domain status.</param>
/// <param name="weight">An optional weight (lower goes first).</param>
/// <returns>A value indicating whether it was possible to register.</returns>
public bool Register(Action release, int weight = 100)
=> Register(null, release, weight);
/// <summary>
/// Registers a resource that requires the current AppDomain to be the main domain to function.
/// </summary>
@@ -83,11 +75,15 @@ namespace Umbraco.Core.Runtime
/// <returns>A value indicating whether it was possible to register.</returns>
/// <remarks>If registering is successful, then the <paramref name="install"/> action
/// is guaranteed to execute before the AppDomain releases the main domain status.</remarks>
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<int, Action>(weight, release));
}
return true;
}
}

View File

@@ -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;
/// <inheritdoc />
public bool Register(Action release, int weight = 100)
=> Register(null, release, weight);
/// <inheritdoc />
public bool Register(Action install, Action release, int weight = 100)
{

View File

@@ -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();

View File

@@ -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<ExamineComponent>("Examine shutting down"))
{

View File

@@ -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)
{

View File

@@ -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
{
/// <summary>
/// Ensures the server variables are included in the outgoing JS script
/// </summary>
public class ServerVariablesParser
{
private const string Token = "##Variables##";
private readonly IEventAggregator _eventAggregator;
/// <summary>
/// Allows developers to add custom variables on parsing
/// Initializes a new instance of the <see cref="ServerVariablesParser"/> class.
/// </summary>
public static event EventHandler<Dictionary<string, object>> Parsing;
public ServerVariablesParser(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator;
internal const string Token = "##Variables##";
public static string Parse(Dictionary<string, object> items)
/// <summary>
/// Ensures the server variables in the dictionary are included in the outgoing JS script
/// </summary>
public async Task<string> ParseAsync(Dictionary<string, object> 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());

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
using Umbraco.Core.Events;
namespace Umbraco.Web.WebAssets
{
/// <summary>
/// A notification for when server variables are parsing
/// </summary>
public class ServerVariablesParsing : INotification
{
/// <summary>
/// Initializes a new instance of the <see cref="ServerVariablesParsing"/> class.
/// </summary>
public ServerVariablesParsing(IDictionary<string, object> serverVariables) => ServerVariables = serverVariables;
/// <summary>
/// Gets a mutable dictionary of server variables
/// </summary>
public IDictionary<string, object> ServerVariables { get; }
}
}

View File

@@ -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<ModelsBuilderSettings> _config;
public ContentTypeModelValidatorBase(IOptions<ModelsBuilderSettings> config)
{
_config = config;
}
public ContentTypeModelValidatorBase(IOptions<ModelsBuilderSettings> config) => _config = config;
protected override IEnumerable<ValidationResult> 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<TProperty> 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[]
{
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;
}

View File

@@ -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 + "<br />&nbsp;<br />ModelsBuilder is disabled<br />(the .Enable key is missing, or its value is not 'true').";
var sb = new StringBuilder();
sb.Append("Version: ");
sb.Append("<p>Version: ");
sb.Append(ApiVersion.Current.Version);
sb.Append("<br />&nbsp;<br />");
sb.Append("</p>");
sb.Append("ModelsBuilder is enabled, with the following configuration:");
sb.Append("<p>ModelsBuilder is enabled, with the following configuration:</p>");
sb.Append("<ul>");
sb.Append("<li>The <strong>models factory</strong> is ");
sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive
? "enabled"
: "not enabled. Umbraco will <em>not</em> use models");
sb.Append(".</li>");
sb.Append("<li>The <strong>models mode</strong> is '");
sb.Append(_config.ModelsMode.ToString());
sb.Append("'. ");
sb.Append(_config.ModelsMode != ModelsMode.Nothing
? $"<li><strong>{_config.ModelsMode} models</strong> are enabled.</li>"
: "<li>No models mode is specified: models will <em>not</em> be generated.</li>");
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($"<li>Models namespace is {_config.ModelsNamespace}.</li>");
sb.Append("</li>");
if (_config.ModelsMode != ModelsMode.Nothing)
{
sb.Append($"<li>Models namespace is {_config.ModelsNamespace ?? Constants.ModelsBuilder.DefaultModelsNamespace}.</li>");
sb.Append("<li>Tracking of <strong>out-of-date models</strong> is ");
sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled");
sb.Append(".</li>");
}
sb.Append("</ul>");

View File

@@ -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<ModelsBuilderSettings> config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors, IHostingEnvironment hostingEnvironment)
public ModelsBuilderDashboardController(IOptions<ModelsBuilderSettings> 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<Dashboard> GetDashboard()
{
return GetDashboardResult();
}
public ActionResult<Dashboard> GetDashboard() => GetDashboardResult();
private Dashboard GetDashboardResult()
private Dashboard GetDashboardResult() => new Dashboard
{
return new Dashboard
{
Enable = _config.Enable,
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

View File

@@ -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<TypeModel> 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);

View File

@@ -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.
/// <summary>
/// Special component used for when MB is disabled with the legacy MB is detected
/// </summary>
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<ModelsBuilderDashboardController>();
}
public void Terminate()
{
}
}
}

View File

@@ -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<ModelsBuilderComponent>();
builder.Services.AddSingleton<UmbracoServices>();
builder.Services.AddUnique<ModelsGenerator>();
builder.Services.AddUnique<LiveModelsProvider>();
builder.Services.AddUnique<OutOfDateModelsStatus>();
builder.Services.AddUnique<ModelsGenerationError>();
builder.Services.AddUnique<PureLiveModelFactory>();
builder.Services.AddUnique<IPublishedModelFactory>(factory =>
{
var config = factory.GetRequiredService<IOptions<ModelsBuilderSettings>>().Value;
if (config.ModelsMode == ModelsMode.PureLive)
{
return factory.GetRequiredService<PureLiveModelFactory>();
// 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<TypeLoader>();
var publishedValueFallback = factory.GetRequiredService<IPublishedValueFallback>();
var types = typeLoader
.GetTypes<PublishedElementModel>() // element models
.Concat(typeLoader.GetTypes<PublishedContentModel>()); // content models
return new PublishedModelFactory(types, publishedValueFallback);
}
return null;
});
}
}
}

View File

@@ -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
{
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/> for the common Umbraco functionality
/// </summary>
public static class UmbracoBuilderExtensions
{
/// <summary>
/// Adds umbraco's embedded model builder support
/// </summary>
public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder)
{
builder.AddPureLiveRazorEngine();
builder.Services.AddSingleton<UmbracoServices>();
// TODO: I feel like we could just do builder.AddNotificationHandler<ModelsBuilderNotificationHandler>() and it
// would automatically just register for all implemented INotificationHandler{T}?
builder.AddNotificationHandler<UmbracoApplicationStarting, ModelsBuilderNotificationHandler>();
builder.AddNotificationHandler<ServerVariablesParsing, ModelsBuilderNotificationHandler>();
builder.AddNotificationHandler<UmbracoApplicationStarting, LiveModelsProvider>();
builder.AddNotificationHandler<UmbracoApplicationStarting, OutOfDateModelsStatus>();
builder.Services.AddUnique<ModelsGenerator>();
builder.Services.AddUnique<LiveModelsProvider>();
builder.Services.AddUnique<OutOfDateModelsStatus>();
builder.Services.AddUnique<ModelsGenerationError>();
builder.Services.AddUnique<PureLiveModelFactory>();
builder.Services.AddUnique<IPublishedModelFactory>(factory =>
{
ModelsBuilderSettings config = factory.GetRequiredService<IOptions<ModelsBuilderSettings>>().Value;
if (config.ModelsMode == ModelsMode.PureLive)
{
return factory.GetRequiredService<PureLiveModelFactory>();
}
else
{
TypeLoader typeLoader = factory.GetRequiredService<TypeLoader>();
IPublishedValueFallback publishedValueFallback = factory.GetRequiredService<IPublishedValueFallback>();
IEnumerable<Type> types = typeLoader
.GetTypes<PublishedElementModel>() // element models
.Concat(typeLoader.GetTypes<PublishedContentModel>()); // content models
return new PublishedModelFactory(types, publishedValueFallback);
}
});
return builder;
}
/// <summary>
/// Can be called if using an external models builder to remove the embedded models builder controller features
/// </summary>
public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder)
{
builder.Services.AddSingleton<DisableModelsBuilderNotificationHandler>();
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<IRazorViewEngine>(
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<IRazorViewEngine>();
}, s.GetRequiredService<PureLiveModelFactory>()));
return builder;
}
}
}

View File

@@ -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
{
/// <summary>
/// Used in conjunction with <see cref="UmbracoBuilderExtensions.DisableModelsBuilderControllers"/>
/// </summary>
internal class DisableModelsBuilderNotificationHandler : INotificationHandler<UmbracoApplicationStarting>
{
private readonly UmbracoFeatures _features;
public DisableModelsBuilderNotificationHandler(UmbracoFeatures features) => _features = features;
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification to disable MB controller features
/// </summary>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
{
// disable the embedded dashboard controller
_features.Disabled.Controllers.Add<ModelsBuilderDashboardController>();
return Task.CompletedTask;
}
}
}

View File

@@ -1,95 +1,112 @@
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<UmbracoApplicationStarting>
{
private static Mutex _mutex;
private static int _req;
private static int s_req;
private readonly ILogger<LiveModelsProvider> _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<LiveModelsProvider> logger, IOptions<ModelsBuilderSettings> config, ModelsGenerator modelGenerator, ModelsGenerationError mbErrors, IHostingEnvironment hostingEnvironment)
/// <summary>
/// Initializes a new instance of the <see cref="LiveModelsProvider"/> class.
/// </summary>
public LiveModelsProvider(
ILogger<LiveModelsProvider> logger,
IOptions<ModelsBuilderSettings> 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();
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification
/// </summary>
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";
_mutex = new Mutex(false, name); //TODO: Replace this with MainDom? Seems we now have 2x implementations of almost the same thing
// 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);
// 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;
});
}
// 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;
// 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.
if (Interlocked.Exchange(ref s_req, 0) == 0)
{
return;
}
// cannot proceed unless we are MainDom
if (_mainDom.IsMainDom)
{
try
{
_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();
_modelGenerator.GenerateModels();
_mbErrors.Clear();
_logger.LogInformation("Generated.");
}
@@ -102,19 +119,16 @@ namespace Umbraco.ModelsBuilder.Embedded
_mbErrors.Report("Failed to build Live models.", e);
_logger.LogError("Failed to generate models.", e);
}
finally
}
else
{
_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())
{

View File

@@ -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
/// <summary>
/// Handles <see cref="UmbracoApplicationStarting"/> and <see cref="ServerVariablesParsing"/> notifications to initialize MB
/// </summary>
internal class ModelsBuilderNotificationHandler : INotificationHandler<UmbracoApplicationStarting>, INotificationHandler<ServerVariablesParsing>
{
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<ModelsBuilderSettings> config, IShortStringHelper shortStringHelper,
LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels, LinkGenerator linkGenerator,
IUmbracoRequestLifetime umbracoRequestLifetime)
public ModelsBuilderNotificationHandler(
IOptions<ModelsBuilderSettings> 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()
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification
/// </summary>
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();
}
public void Terminate()
{
ServerVariablesParser.Parsing -= ServerVariablesParser_Parsing;
ContentModelBinder.ModelBindingException -= ContentModelBinder_ModelBindingException;
FileService.SavingTemplate -= FileService_SavingTemplate;
return Task.CompletedTask;
}
private void InstallServerVars()
/// <summary>
/// Handles the <see cref="ServerVariablesParsing"/> notification
/// </summary>
public Task HandleAsync(ServerVariablesParsing notification, CancellationToken cancellationToken)
{
// register our URL - for the backoffice API
ServerVariablesParser.Parsing += ServerVariablesParser_Parsing;
}
IDictionary<string, object> serverVars = notification.ServerVariables;
private void ServerVariablesParser_Parsing(object sender, Dictionary<string, object> 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<string, object> umbracoUrls))
{
throw new ArgumentException("Invalid umbracoUrls");
}
if (!serverVars.ContainsKey("umbracoPlugins"))
{
throw new ArgumentException("Missing umbracoPlugins.");
}
if (!(serverVars["umbracoPlugins"] is Dictionary<string, object> umbracoPlugins))
{
throw new ArgumentException("Invalid umbracoPlugins");
}
umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl<ModelsBuilderDashboardController>(controller => controller.BuildModels());
umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
return Task.CompletedTask;
}
private Dictionary<string, object> GetModelsBuilderSettings()
{
var settings = new Dictionary<string, object>
{
{"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
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs<Core.Models.ITemplate> e)
private void FileService_SavingTemplate(IFileService sender, SaveEventArgs<ITemplate> 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<ModelsBuilderAssemblyAttribute>();
var modelAttr = args.ModelType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
ModelsBuilderAssemblyAttribute sourceAttr = args.SourceType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
ModelsBuilderAssemblyAttribute modelAttr = args.ModelType.Assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
// if source or model is not a ModelsBuider type...
if (sourceAttr == null || modelAttr == null)
{
// if neither are ModelsBuilder types, give up entirely
if (sourceAttr == null && modelAttr == null)
{
return;
}
// else report, but better not restart (loops?)
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?)
@@ -195,4 +206,5 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose
}
}
}
}
}

View File

@@ -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<UmbracoApplicationStarting>
{
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
{
get
{
// just be sure
if (_config.FlagOutOfDateModels == false)
{
return false;
}
var path = GetFlagPath();
return path != null && File.Exists(path);
}
}
/// <summary>
/// Handles the <see cref="UmbracoApplicationStarting"/> notification
/// </summary>
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;
if (_config.FlagOutOfDateModels == false)
{
return;
}
var path = GetFlagPath();
if (path == null || !File.Exists(path)) return;
if (path == null || !File.Exists(path))
{
return;
}
File.Delete(path);
}
public bool IsEnabled => _config.FlagOutOfDateModels;
public bool IsOutOfDate
{
get
{
if (_config.FlagOutOfDateModels == false) return false;
var path = GetFlagPath();
return path != null && File.Exists(path);
}
}
}
}

View File

@@ -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<PureLiveModelFactory> _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> _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> umbracoServices,
@@ -53,7 +57,8 @@ namespace Umbraco.ModelsBuilder.Embedded
IOptions<ModelsBuilderSettings> 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;
/// <summary>
/// Gets the currently loaded pure live models assembly
/// </summary>
/// <remarks>
/// Can be null
/// </remarks>
public Assembly CurrentModelsAssembly { get; private set; }
/// <inheritdoc />
public object SyncRoot { get; } = new object();
/// <inheritdoc />
public void Refresh()
/// <summary>
/// Gets the RoslynCompiler
/// </summary>
private RoslynCompiler RoslynCompiler
{
ResetModels();
EnsureModels();
get
{
if (_roslynCompiler != null)
{
return _roslynCompiler;
}
#endregion
_roslynCompiler = new RoslynCompiler(AssemblyLoadContext.All.SelectMany(x => x.Assemblies));
return _roslynCompiler;
}
}
#region IPublishedModelFactory
/// <inheritdoc />
public bool Enabled => _config.ModelsMode == ModelsMode.PureLive;
/// <summary>
/// Handle the event when a reference cannot be resolved from the default context and return our custom MB assembly reference if we have one
/// </summary>
/// <remarks>
/// This is required because the razor engine will only try to load things from the default context, it doesn't know anything
/// about our context so we need to proxy.
/// </remarks>
private Assembly OnResolvingDefaultAssemblyLoadContext(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
=> _currentAssemblyLoadContext?.LoadFromAssemblyName(assemblyName);
public IPublishedElement CreateModel(IPublishedElement element)
{
// get models, rebuilding them if needed
var infos = EnsureModels()?.ModelInfos;
Dictionary<string, ModelInfo> 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,92 +172,38 @@ 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<IPublishedElement>();
}
if (!infos.ModelInfos.TryGetValue(alias, out var modelInfo))
if (!infos.ModelInfos.TryGetValue(alias, out ModelInfo modelInfo))
{
return new List<IPublishedElement>();
}
var ctor = modelInfo.ListCtor;
if (ctor != null) return ctor();
Func<IList> 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<Func<IList>>(declaring: listType);
return ctor();
}
/// <inheritdoc />
public bool Enabled => _config.Enable;
/// <inheritdoc />
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
// <deadlock>
//
// 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<PureLiveModelFactory>("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,81 +219,88 @@ 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;
}
}
// ensure that the factory is running with the lastest generation of models
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<PureLiveModelFactory>("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<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
// 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<Type> types = assembly.ExportedTypes.Where(x => x.Inherits<PublishedContentModel>() || x.Inherits<PublishedElementModel>());
_infos = RegisterModels(types);
_errors.Clear();
}
@@ -317,6 +314,7 @@ namespace Umbraco.ModelsBuilder.Embedded
}
finally
{
CurrentModelsAssembly = null;
_infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary<string, Type>() };
}
}
@@ -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<TypeModel> 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>();
ModelsBuilderAssemblyAttribute attr = assembly.GetCustomAttribute<ModelsBuilderAssemblyAttribute>();
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<FileInfo> 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<Type> types)
{
var ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) };
Type[] ctorArgTypes = new[] { typeof(IPublishedElement), typeof(IPublishedValueFallback) };
var modelInfos = new Dictionary<string, ModelInfo>(StringComparer.InvariantCultureIgnoreCase);
var map = new Dictionary<string, Type>();
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<PublishedModelAttribute>(false);
PublishedModelAttribute attribute = type.GetCustomAttribute<PublishedModelAttribute>(false);
var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias;
if (modelInfos.TryGetValue(typeName, out var modelInfo))
{
throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\".");
}
// TODO: use Core's ReflectionUtilities.EmitCtor !!
// Yes .. DynamicMethod is uber slow
// TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still?
// See CtorInvokeBenchmarks
var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true);
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<string, string> 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<string>();
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<string>();
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<string, string> 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<string, Type> ModelTypeMap { get; set; }
public Dictionary<string, ModelInfo> ModelInfos { get; set; }
}
internal class ModelInfo
{
public Type ParameterType { get; set; }
public Func<IPublishedElement, IPublishedValueFallback, IPublishedElement> Ctor { get; set; }
public Type ModelType { get; set; }
public Func<IList> ListCtor { get; set; }
}
#endregion
#region Watching
private void WatcherOnChanged(object sender, FileSystemEventArgs args)
{
var changed = args.Name;
@@ -715,14 +759,18 @@ 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<string, Type> ModelTypeMap { get; set; }
public Dictionary<string, ModelInfo> ModelInfos { get; set; }
}
internal class ModelInfo
{
public Type ParameterType { get; set; }
public Func<IPublishedElement, IPublishedValueFallback, IPublishedElement> Ctor { get; set; }
public Type ModelType { get; set; }
public Func<IList> ListCtor { get; set; }
}
}
}

View File

@@ -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
{
/// <summary>
/// Custom <see cref="IRazorViewEngine"/> that wraps aspnetcore's default implementation
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal class RefreshingRazorViewEngine : IRazorViewEngine
{
private IRazorViewEngine _current;
private readonly PureLiveModelFactory _pureLiveModelFactory;
private readonly Func<IRazorViewEngine> _defaultRazorViewEngineFactory;
private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim();
/// <summary>
/// Initializes a new instance of the <see cref="RefreshingRazorViewEngine"/> class.
/// </summary>
/// <param name="defaultRazorViewEngineFactory">
/// A factory method used to re-construct the default aspnetcore <see cref="RazorViewEngine"/>
/// </param>
/// <param name="pureLiveModelFactory">The <see cref="PureLiveModelFactory"/></param>
public RefreshingRazorViewEngine(Func<IRazorViewEngine> defaultRazorViewEngineFactory, PureLiveModelFactory pureLiveModelFactory)
{
_pureLiveModelFactory = pureLiveModelFactory;
_defaultRazorViewEngineFactory = defaultRazorViewEngineFactory;
_current = _defaultRazorViewEngineFactory();
_pureLiveModelFactory.ModelsChanged += PureLiveModelFactory_ModelsChanged;
}
/// <summary>
/// When the pure live models change, re-construct the razor stack
/// </summary>
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();
}
}
}
}

View File

@@ -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<MetadataReference> _refs;
/// <summary>
/// Roslyn compiler which can be used to compile a c# file to a Dll assembly
/// Initializes a new instance of the <see cref="RoslynCompiler"/> class.
/// </summary>
/// <param name="referenceAssemblies">Referenced assemblies used in the source file</param>
/// <remarks>
/// Roslyn compiler which can be used to compile a c# file to a Dll assembly
/// </remarks>
public RoslynCompiler(IEnumerable<Assembly> 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<MetadataReference>();
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,10 +59,12 @@ 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,
options: new CSharpCompilationOptions(
_outputKind,
optimizationLevel: OptimizationLevel.Release,
// Not entirely certain that assemblyIdentityComparer is nececary?
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));

View File

@@ -12,6 +12,8 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.8" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.7.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -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
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoAssemblyLoadContext"/> class.
/// </summary>
/// <remarks>
/// Collectible AssemblyLoadContext used to load in the compiled generated models.
/// Must be a collectible assembly in order to be able to be unloaded.
/// </summary>
public UmbracoAssemblyLoadContext() : base(isCollectible: true)
/// </remarks>
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;
}
}

View File

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

View File

@@ -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<T>(ContentStore store, ContentTypeCacheRefresher.JsonPayload[] payloads, Action<List<int>, List<int>, List<int>, List<int>> 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);
}

View File

@@ -176,6 +176,7 @@ namespace Umbraco.Tests.Persistence
Assert.IsNull(exceptions[i]);
}
[Retry(5)] // TODO make this test non-flaky.
[Test]
public void DeadLockTest()
{

View File

@@ -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()
{

View File

@@ -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<IEventAggregator>());
var d = new Dictionary<string, object>
{
{ "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"",

View File

@@ -311,9 +311,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Views
public class TestPage<TModel> : UmbracoViewPage<TModel>
{
private readonly ContentModelBinder _modelBinder = new ContentModelBinder();
public override Task ExecuteAsync() => throw new NotImplementedException();
public void SetViewData(ViewDataDictionary viewData) => ViewData = (ViewDataDictionary<TModel>)BindViewData(viewData);
public void SetViewData(ViewDataDictionary viewData) => ViewData = (ViewDataDictionary<TModel>)BindViewData(_modelBinder, viewData);
}
public class RenderModelTestPage : TestPage<ContentModel>

View File

@@ -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
/// <summary>
/// Returns the JavaScript object representing the static server variables javascript object
/// </summary>
/// <returns></returns>
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[MinifyJavaScriptResult(Order = 1)]
public async Task<JavaScriptResult> 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<string>(

View File

@@ -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)

View File

@@ -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<ServerVariablesParser>();
builder.Services.AddUnique<BackOfficeAreaRoutes>();
builder.Services.AddUnique<PreviewRoutes>();
builder.Services.AddUnique<BackOfficeServerVariables>();

View File

@@ -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<IIOHelper>();
private ContentModelBinder ContentModelBinder => new ContentModelBinder();
/// <summary>
/// Gets the <see cref="IUmbracoContext"/>
/// </summary>
@@ -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<ContentModelBinder>(), ctx.ViewData);
base.ViewContext = ctx;
}
}
@@ -123,8 +123,18 @@ namespace Umbraco.Web.Common.AspNetCore
/// <see cref="IContentModel"/> or <see cref="IPublishedContent"/>. This will use the <see cref="ContentModelBinder"/> to bind the models
/// to the correct output type.
/// </remarks>
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<TModel> 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;

View File

@@ -274,6 +274,8 @@ namespace Umbraco.Web.Common.DependencyInjection
builder.Services.AddUnique<ITemplateRenderer, TemplateRenderer>();
builder.Services.AddUnique<IPublicAccessChecker, PublicAccessChecker>();
builder.Services.AddSingleton<ContentModelBinder>();
builder.AddHttpClients();
// TODO: Does this belong in web components??

View File

@@ -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)

View File

@@ -1,8 +1,9 @@
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<HttpContext> RequestStart;

View File

@@ -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.

View File

@@ -14,6 +14,11 @@ namespace Umbraco.Web.Common.ModelBinders
/// </summary>
public class ContentModelBinder : IModelBinder
{
/// <summary>
/// Occurs on model binding exceptions.
/// </summary>
public event EventHandler<ModelBindingArgs> ModelBindingException; // TODO: This cannot use IEventAggregator currently because it cannot be async
/// <inheritdoc/>
public Task BindModelAsync(ModelBindingContext bindingContext)
{
@@ -193,10 +198,5 @@ namespace Umbraco.Web.Common.ModelBinders
/// </summary>
public bool Restart { get; set; }
}
/// <summary>
/// Occurs on model binding exceptions.
/// </summary>
public static event EventHandler<ModelBindingArgs> ModelBindingException;
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
@@ -20,13 +20,30 @@
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
<ProjectReference Include="..\Umbraco.Persistence.SqlCe\Umbraco.Persistence.SqlCe.csproj" Condition="'$(OS)' == 'Windows_NT'" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Umbraco.Web.UI.NetCore</RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\Release\Umbraco.Web.UI.NetCore.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.ModelsBuilder.Embedded\Umbraco.ModelsBuilder.Embedded.csproj" />
<ProjectReference Include="..\Umbraco.PublishedCache.NuCache\Umbraco.PublishedCache.NuCache.csproj" />
<ProjectReference Include="..\Umbraco.Web.BackOffice\Umbraco.Web.BackOffice.csproj" />
<ProjectReference Include="..\Umbraco.Web.Common\Umbraco.Web.Common.csproj" />
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
<ProjectReference Include="..\Umbraco.Persistance.SqlCe\Umbraco.Persistance.SqlCe.csproj" Condition="'$(OS)' == 'Windows_NT'" />
</ItemGroup>
<ItemGroup>
<Folder Include="App_Plugins" />
<Folder Include="scripts" />
<Folder Include="umbraco\MediaCache\2\c\6\9\3\a\6\5" />
<Folder Include="umbraco\MediaCache\a\e\e\1\9\e\4\b" />
<Folder Include="umbraco\MediaCache\c\3\b\5\0\9\f\9" />
<Folder Include="Views" />
<Folder Include="wwwroot\Media" />
<Folder Include="Views" />
@@ -34,16 +51,28 @@
</ItemGroup>
<ItemGroup>
<Compile Remove="umbraco\Data\**" />
<Compile Remove="umbraco\logs\**" />
<Compile Remove="umbraco\MediaCache\**" />
<Compile Remove="umbraco\models\**" />
<Compile Remove="wwwroot\Umbraco\**" />
<Compile Remove="App_Data\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="App_Data\**" />
<EmbeddedResource Remove="umbraco\Data\**" />
<EmbeddedResource Remove="umbraco\logs\**" />
<EmbeddedResource Remove="umbraco\MediaCache\**" />
<EmbeddedResource Remove="umbraco\models\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="App_Data\**" />
<Content Remove="umbraco\Data\**" />
<Content Remove="umbraco\logs\**" />
<Content Remove="umbraco\MediaCache\**" />
<Content Remove="umbraco\models\**" />
<Content Remove="wwwroot\Web.config" />
</ItemGroup>
@@ -60,28 +89,29 @@
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</None>
<None Remove="App_Data\**" />
<None Remove="umbraco\Data\**" />
<None Remove="umbraco\logs\**" />
<None Remove="umbraco\MediaCache\**" />
<None Remove="umbraco\models\**" />
<None Include="umbraco\UmbracoWebsite\NoNodes.cshtml" />
<None Remove="scripts\aaa\fc75309db05f41609a9e1adb8cf0998c.tmp" />
</ItemGroup>
<!-- We don't want to include the generated files, they will throw a lot of errors -->
<ItemGroup>
<None Remove="umbraco\Models\all.generated.cs" />
<Compile Remove="umbraco\Models\all.generated.cs" />
<None Remove="umbraco\Models\models.generated.cs" />
<Compile Remove="umbraco\Models\models.generated.cs" />
<Folder Remove="umbraco\Models\Compiled" />
<None Remove="umbraco\Models\Compiled\**" />
<None Remove="umbraco\Models\all.dll.path" />
<None Remove="umbraco\Models\models.hash" />
<None Remove="umbraco\Models\models.err" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.2" />
<!-- TODO: remove the reference to System.Configuration.ConfigurationManager when Examine/lucene dont need it-->
<PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.8" />
<!-- TODO: remove the reference to System.Configuration.ConfigurationManager when Examine/lucene dont need it-->
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.7.0" />
</ItemGroup>
<PropertyGroup>
<RazorCompileOnBuild>false</RazorCompileOnBuild>
</PropertyGroup>
</Project>

View File

@@ -67,7 +67,7 @@
},
"ModelsBuilder": {
"ModelsMode": "PureLive",
"Enable": false
"Enable": true
}
}
}

View File

@@ -261,7 +261,6 @@
<!-- Create ClientDependency.config file from Template if it doesn't exist -->
<Message Text="Copy ClientDependency.$(Configuration).config to ClientDependency.config" Importance="high" Condition="!Exists('$(ProjectDir)Config\ClientDependency.config')" />
<Copy SourceFiles="$(ProjectDir)Config\ClientDependency.Release.config" DestinationFiles="$(ProjectDir)Config\ClientDependency.config" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" Condition="!Exists('$(ProjectDir)Config\ClientDependency.config')" />
<Copy SourceFiles="$(ProjectDir)Config\umbracoSettings.Release.config" DestinationFiles="$(ProjectDir)Config\umbracoSettings.config" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" Condition="!Exists('$(ProjectDir)Config\umbracoSettings.config')" />
<!-- Create Serilog.config & serilog.user.config file from Templates if it doesn't exist -->
<Message Text="Copy serilog.$(Configuration).config to serilog.config" Importance="high" Condition="!Exists('$(ProjectDir)Config\serilog.config')" />
<Copy SourceFiles="$(ProjectDir)Config\serilog.Release.config" DestinationFiles="$(ProjectDir)Config\serilog.config" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" Condition="!Exists('$(ProjectDir)Config\serilog.config')" />

View File

@@ -28,8 +28,8 @@ namespace Umbraco.Web.Website.DependencyInjection
.Add(builder.TypeLoader.GetSurfaceControllers());
// Configure MVC startup options for custom view locations
builder.Services.AddTransient<IConfigureOptions<RazorViewEngineOptions>, RenderRazorViewEngineOptionsSetup>();
builder.Services.AddTransient<IConfigureOptions<RazorViewEngineOptions>, PluginRazorViewEngineOptionsSetup>();
builder.Services.ConfigureOptions<RenderRazorViewEngineOptionsSetup>();
builder.Services.ConfigureOptions<PluginRazorViewEngineOptionsSetup>();
// Wraps all existing view engines in a ProfilerViewEngine
builder.Services.AddTransient<IConfigureOptions<MvcViewOptions>, ProfilingViewEngineWrapperMvcViewOptionsSetup>();