Merge remote-tracking branch 'origin/netcore/netcore' into netcore/feature/net5
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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/*
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 /> <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 /> <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>");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs
Normal file
176
src/Umbraco.ModelsBuilder.Embedded/RefreshingRazorViewEngine.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ namespace Umbraco.Tests.Persistence
|
||||
Assert.IsNull(exceptions[i]);
|
||||
}
|
||||
|
||||
[Retry(5)] // TODO make this test non-flaky.
|
||||
[Test]
|
||||
public void DeadLockTest()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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"",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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??
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
},
|
||||
"ModelsBuilder": {
|
||||
"ModelsMode": "PureLive",
|
||||
"Enable": false
|
||||
"Enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')" />
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user