propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop));
if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant())
- yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[]
- {
- $"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias"
- });
+ {
+ string[] memberNames = new[]
+ {
+ $"Groups[{model.Groups.IndexOf(propertyGroup)}].Properties[{propertyGroup.Properties.IndexOf(prop)}].Alias"
+ };
- //we need to return the field name with an index so it's wired up correctly
+ yield return new ValidationResult(
+ string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias),
+ memberNames);
+ }
+
+ // we need to return the field name with an index so it's wired up correctly
var groupIndex = model.Groups.IndexOf(propertyGroup);
var propertyIndex = propertyGroup.Properties.IndexOf(prop);
- var validationResult = ValidateProperty(prop, groupIndex, propertyIndex);
+ ValidationResult validationResult = ValidateProperty(prop, groupIndex, propertyIndex);
if (validationResult != null)
+ {
yield return validationResult;
+ }
}
}
private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex)
{
- //don't let them match any properties or methods in IPublishedContent
- //TODO: There are probably more!
+ // don't let them match any properties or methods in IPublishedContent
+ // TODO: There are probably more!
var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray();
var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray();
var alias = property.Alias;
if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias))
- return new ValidationResult(
- $"The alias {alias} is a reserved term and cannot be used", new[]
+ {
+ string[] memberNames = new[]
{
$"Groups[{groupIndex}].Properties[{propertyIndex}].Alias"
- });
+ };
+
+ return new ValidationResult(
+ $"The alias {alias} is a reserved term and cannot be used",
+ memberNames);
+ }
return null;
}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
index c615559920..6425673916 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/DashboardReport.cs
@@ -1,6 +1,7 @@
-using System.Text;
+using System.Text;
using Microsoft.Extensions.Options;
using Umbraco.Configuration;
+using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
@@ -27,34 +28,46 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice
public string Text()
{
- if (!_config.Enable)
- return "Version: " + ApiVersion.Current.Version + "
ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true').";
-
var sb = new StringBuilder();
- sb.Append("Version: ");
+ sb.Append("Version: ");
sb.Append(ApiVersion.Current.Version);
- sb.Append("
");
+ sb.Append("
");
- sb.Append("ModelsBuilder is enabled, with the following configuration:");
+ sb.Append("ModelsBuilder is enabled, with the following configuration:
");
sb.Append("");
- sb.Append("- The models factory is ");
- sb.Append(_config.EnableFactory || _config.ModelsMode == ModelsMode.PureLive
- ? "enabled"
- : "not enabled. Umbraco will not use models");
- sb.Append(".
");
+ sb.Append("- The models mode is '");
+ sb.Append(_config.ModelsMode.ToString());
+ sb.Append("'. ");
- sb.Append(_config.ModelsMode != ModelsMode.Nothing
- ? $"
- {_config.ModelsMode} models are enabled.
"
- : "- No models mode is specified: models will not be generated.
");
+ switch (_config.ModelsMode)
+ {
+ case ModelsMode.Nothing:
+ sb.Append("Strongly typed models are not generated. All content and cache will operate from instance of IPublishedContent only.");
+ break;
+ case ModelsMode.PureLive:
+ sb.Append("Strongly typed models are re-generated on startup and anytime schema changes (i.e. Content Type) are made. No recompilation necessary but the generated models are not available to code outside of Razor.");
+ break;
+ case ModelsMode.AppData:
+ sb.Append("Strongly typed models are generated on demand. Recompilation is necessary and models are available to all CSharp code.");
+ break;
+ case ModelsMode.LiveAppData:
+ sb.Append("Strong typed models are generated on demand and anytime schema changes (i.e. Content Type) are made. Recompilation is necessary and models are available to all CSharp code.");
+ break;
+ }
- sb.Append($"- Models namespace is {_config.ModelsNamespace}.
");
+ sb.Append("");
- sb.Append("- Tracking of out-of-date models is ");
- sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled");
- sb.Append(".
");
+ if (_config.ModelsMode != ModelsMode.Nothing)
+ {
+ sb.Append($"- Models namespace is {_config.ModelsNamespace ?? Constants.ModelsBuilder.DefaultModelsNamespace}.
");
+
+ sb.Append("- Tracking of out-of-date models is ");
+ sb.Append(_config.FlagOutOfDateModels ? "enabled" : "not enabled");
+ sb.Append(".
");
+ }
sb.Append("
");
diff --git a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs
index 0b67498f01..f242854b3f 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/BackOffice/ModelsBuilderDashboardController.cs
@@ -4,12 +4,10 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Umbraco.Configuration;
+using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
-using Umbraco.Core.Exceptions;
-using Umbraco.Core.Hosting;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.Web.BackOffice.Controllers;
-using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.Common.Authorization;
namespace Umbraco.ModelsBuilder.Embedded.BackOffice
@@ -30,17 +28,14 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice
private readonly OutOfDateModelsStatus _outOfDateModels;
private readonly ModelsGenerationError _mbErrors;
private readonly DashboardReport _dashboardReport;
- private readonly IHostingEnvironment _hostingEnvironment;
- public ModelsBuilderDashboardController(IOptions config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors, IHostingEnvironment hostingEnvironment)
+ public ModelsBuilderDashboardController(IOptions config, ModelsGenerator modelsGenerator, OutOfDateModelsStatus outOfDateModels, ModelsGenerationError mbErrors)
{
- //_umbracoServices = umbracoServices;
_config = config.Value;
_modelGenerator = modelsGenerator;
_outOfDateModels = outOfDateModels;
_mbErrors = mbErrors;
_dashboardReport = new DashboardReport(config, outOfDateModels, mbErrors);
- _hostingEnvironment = hostingEnvironment;
}
// invoked by the dashboard
@@ -51,19 +46,12 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice
{
try
{
- var config = _config;
-
- if (!config.ModelsMode.SupportsExplicitGeneration())
+ if (!_config.ModelsMode.SupportsExplicitGeneration())
{
var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." };
return Ok(result2);
}
- var bin = _hostingEnvironment.MapPathContentRoot("~/bin");
- if (bin == null)
- throw new PanicException("bin is null.");
-
- // EnableDllModels will recycle the app domain - but this request will end properly
_modelGenerator.GenerateModels();
_mbErrors.Clear();
}
@@ -93,45 +81,44 @@ namespace Umbraco.ModelsBuilder.Embedded.BackOffice
// requires that the user is logged into the backoffice and has access to the settings section
// beware! the name of the method appears in modelsbuilder.controller.js
[HttpGet] // use the http one, not mvc, with api controllers!
- public ActionResult GetDashboard()
- {
- return GetDashboardResult();
- }
+ public ActionResult GetDashboard() => GetDashboardResult();
- private Dashboard GetDashboardResult()
+ private Dashboard GetDashboardResult() => new Dashboard
{
- return new Dashboard
- {
- Enable = _config.Enable,
- Text = _dashboardReport.Text(),
- CanGenerate = _dashboardReport.CanGenerate(),
- OutOfDateModels = _dashboardReport.AreModelsOutOfDate(),
- LastError = _dashboardReport.LastError(),
- };
- }
+ Mode = _config.ModelsMode,
+ Text = _dashboardReport.Text(),
+ CanGenerate = _dashboardReport.CanGenerate(),
+ OutOfDateModels = _dashboardReport.AreModelsOutOfDate(),
+ LastError = _dashboardReport.LastError(),
+ };
[DataContract]
public class BuildResult
{
[DataMember(Name = "success")]
- public bool Success;
+ public bool Success { get; set; }
+
[DataMember(Name = "message")]
- public string Message;
+ public string Message { get; set; }
}
[DataContract]
public class Dashboard
{
- [DataMember(Name = "enable")]
- public bool Enable;
+ [DataMember(Name = "mode")]
+ public ModelsMode Mode { get; set; }
+
[DataMember(Name = "text")]
- public string Text;
+ public string Text { get; set; }
+
[DataMember(Name = "canGenerate")]
- public bool CanGenerate;
+ public bool CanGenerate { get; set; }
+
[DataMember(Name = "outOfDateModels")]
- public bool OutOfDateModels;
+ public bool OutOfDateModels { get; set; }
+
[DataMember(Name = "lastError")]
- public string LastError;
+ public string LastError { get; set; }
}
public enum OutOfDateType
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs
index bc97118ee4..1c5160df18 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/Building/ModelsGenerator.cs
@@ -1,4 +1,4 @@
-using System.IO;
+using System.IO;
using System.Text;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration;
@@ -27,16 +27,20 @@ namespace Umbraco.ModelsBuilder.Embedded.Building
{
var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment);
if (!Directory.Exists(modelsDirectory))
+ {
Directory.CreateDirectory(modelsDirectory);
+ }
foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs"))
+ {
File.Delete(file);
+ }
- var typeModels = _umbracoService.GetAllTypes();
+ System.Collections.Generic.IList typeModels = _umbracoService.GetAllTypes();
var builder = new TextBuilder(_config, typeModels);
- foreach (var typeModel in builder.GetModelsToGenerate())
+ foreach (TypeModel typeModel in builder.GetModelsToGenerate())
{
var sb = new StringBuilder();
builder.Generate(sb, typeModel);
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs
deleted file mode 100644
index f7fab098b0..0000000000
--- a/src/Umbraco.ModelsBuilder.Embedded/Compose/DisabledModelsBuilderComponent.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using Umbraco.Core.Composing;
-using Umbraco.ModelsBuilder.Embedded.BackOffice;
-using Umbraco.Web.Features;
-
-namespace Umbraco.ModelsBuilder.Embedded.Compose
-{
- // TODO: This needs to die, see TODO in ModelsBuilderComposer. This is also no longer used in this netcore
- // codebase. Potentially this could be changed to ext methods if necessary that could be used by end users who will
- // install the community MB package to disable any built in MB stuff.
-
- ///
- /// Special component used for when MB is disabled with the legacy MB is detected
- ///
- public sealed class DisabledModelsBuilderComponent : IComponent
- {
- private readonly UmbracoFeatures _features;
-
- public DisabledModelsBuilderComponent(UmbracoFeatures features)
- {
- _features = features;
- }
-
- public void Initialize()
- {
- //disable the embedded dashboard controller
- _features.Disabled.Controllers.Add();
- }
-
- public void Terminate()
- {
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs
deleted file mode 100644
index 94237ccf3d..0000000000
--- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComposer.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using System.Linq;
-using Microsoft.Extensions.DependencyInjection;
-using Umbraco.Core.Configuration;
-using Umbraco.Core.Composing;
-using Umbraco.Core.Models.PublishedContent;
-using Umbraco.ModelsBuilder.Embedded.Building;
-using Umbraco.Core.Configuration.Models;
-using Microsoft.Extensions.Options;
-using Umbraco.Core.DependencyInjection;
-
-namespace Umbraco.ModelsBuilder.Embedded.Compose
-{
- // TODO: We'll need to change this stuff to IUmbracoBuilder ext and control the order of things there
- // This needs to execute before the AddNuCache call
- public sealed class ModelsBuilderComposer : ICoreComposer
- {
- public void Compose(IUmbracoBuilder builder)
- {
- builder.Components().Append();
- builder.Services.AddSingleton();
- builder.Services.AddUnique();
- builder.Services.AddUnique();
- builder.Services.AddUnique();
- builder.Services.AddUnique();
-
- builder.Services.AddUnique();
- builder.Services.AddUnique(factory =>
- {
- var config = factory.GetRequiredService>().Value;
- if (config.ModelsMode == ModelsMode.PureLive)
- {
- return factory.GetRequiredService();
- // the following would add @using statement in every view so user's don't
- // have to do it - however, then noone understands where the @using statement
- // comes from, and it cannot be avoided / removed --- DISABLED
- //
- /*
- // no need for @using in views
- // note:
- // we are NOT using the in-code attribute here, config is required
- // because that would require parsing the code... and what if it changes?
- // we can AddGlobalImport not sure we can remove one anyways
- var modelsNamespace = Configuration.Config.ModelsNamespace;
- if (string.IsNullOrWhiteSpace(modelsNamespace))
- modelsNamespace = Configuration.Config.DefaultModelsNamespace;
- System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace);
- */
- }
- else if (config.EnableFactory)
- {
- var typeLoader = factory.GetRequiredService();
- var publishedValueFallback = factory.GetRequiredService();
- var types = typeLoader
- .GetTypes() // element models
- .Concat(typeLoader.GetTypes()); // content models
- return new PublishedModelFactory(types, publishedValueFallback);
- }
-
- return null;
- });
-
- }
- }
-}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
new file mode 100644
index 0000000000..05df1f9a7b
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc.Razor;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.DependencyInjection;
+using Umbraco.Core.Events;
+using Umbraco.Core.Models.PublishedContent;
+using Umbraco.ModelsBuilder.Embedded.Building;
+using Umbraco.Web.Common.DependencyInjection;
+using Umbraco.Web.WebAssets;
+
+/*
+ * OVERVIEW:
+ *
+ * The CSharpCompiler is responsible for the actual compilation of razor at runtime.
+ * It creates a CSharpCompilation instance to do the compilation. This is where DLL references
+ * are applied. However, the way this works is not flexible for dynamic assemblies since the references
+ * are only discovered and loaded once before the first compilation occurs. This is done here:
+ * https://github.com/dotnet/aspnetcore/blob/114f0f6d1ef1d777fb93d90c87ac506027c55ea0/src/Mvc/Mvc.Razor.RuntimeCompilation/src/CSharpCompiler.cs#L79
+ * The CSharpCompiler is internal and cannot be replaced or extended, however it's references come from:
+ * RazorReferenceManager. Unfortunately this is also internal and cannot be replaced, though it can be extended
+ * using MvcRazorRuntimeCompilationOptions, except this is the place where references are only loaded once which
+ * is done with a LazyInitializer. See https://github.com/dotnet/aspnetcore/blob/master/src/Mvc/Mvc.Razor.RuntimeCompilation/src/RazorReferenceManager.cs#L35.
+ *
+ * The way that RazorReferenceManager works is by resolving references from the ApplicationPartsManager - either by
+ * an application part that is specifically an ICompilationReferencesProvider or an AssemblyPart. So to fulfill this
+ * requirement, we add the MB assembly to the assembly parts manager within the PureLiveModelFactory when the assembly
+ * is (re)generated. But due to the above restrictions, when re-generating, this will have no effect since the references
+ * have already been resolved with the LazyInitializer in the RazorReferenceManager. There is a known public API
+ * where you can add reference paths to the runtime razor compiler via it's IOptions: MvcRazorRuntimeCompilationOptions
+ * however this falls short too because those references are just loaded via the RazorReferenceManager and lazy initialized.
+ *
+ * The services that can be replaced are: IViewCompilerProvider (default is the internal RuntimeViewCompilerProvider) and
+ * IViewCompiler (default is the internal RuntimeViewCompiler). There is one specific public extension point that I was
+ * hoping would solve all of the problems which was IMetadataReferenceFeature (implemented by LazyMetadataReferenceFeature
+ * which uses RazorReferencesManager) which is a razor feature that you can add
+ * to the RazorProjectEngine. It is used to resolve roslyn references and by default is backed by RazorReferencesManager.
+ * Unfortunately, this service is not used by the CSharpCompiler, it seems to only be used by some tag helper compilations.
+ *
+ * There are caches at several levels, all of which are not publicly accessible APIs (apart from RazorViewEngine.ViewLookupCache
+ * which is possible to clear by casting and then calling cache.Compact(100); but that doesn't get us far enough).
+ *
+ * For this to work, several caches must be cleared:
+ * - RazorViewEngine.ViewLookupCache
+ * - RazorReferencesManager._compilationReferences
+ * - RazorPageActivator._activationInfo (though this one may be optional)
+ * - RuntimeViewCompiler._cache
+ *
+ * What are our options?
+ *
+ * a) We can copy a ton of code into our application: CSharpCompiler, RuntimeViewCompilerProvider, RuntimeViewCompiler and
+ * RazorReferenceManager (probably more depending on the extent of Internal references).
+ * b) We can use reflection to try to access all of the above resources and try to forcefully clear caches and reset initialization flags.
+ * c) We hack these replace-able services with our own implementations that wrap the default services. To do this
+ * requires re-resolving the original services from a pre-built DI container. In effect this re-creates these
+ * services from scratch which means there is no caches.
+ *
+ * ... Option C works, we will use that but need to verify how this affects memory since ideally the old services will be GC'd.
+ *
+ * Option C, how its done:
+ * - Before we add our custom razor services to the container, we make a copy of the services collection which is the snapshot of registered services
+ * with razor defaults before ours are added.
+ * - We replace the default implementation of IRazorViewEngine with our own. This is a wrapping service that wraps the default RazorViewEngine instance.
+ * The ctor for this service takes in a Factory method to re-construct the default RazorViewEngine and all of it's dependency graph.
+ * - When the PureLive models change, the Factory is invoked and the default razor services are all re-created, thus clearing their caches and the newly
+ * created instance is wrapped. The RazorViewEngine is the only service that needs to be replaced and wrapped for this to work because it's dependency
+ * graph includes all of the above mentioned services, all the way up to the RazorProjectEngine and it's LazyMetadataReferenceFeature.
+ */
+
+namespace Umbraco.ModelsBuilder.Embedded.DependencyInjection
+{
+ ///
+ /// Extension methods for for the common Umbraco functionality
+ ///
+ public static class UmbracoBuilderExtensions
+ {
+ ///
+ /// Adds umbraco's embedded model builder support
+ ///
+ public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder)
+ {
+ builder.AddPureLiveRazorEngine();
+ builder.Services.AddSingleton();
+
+ // TODO: I feel like we could just do builder.AddNotificationHandler() and it
+ // would automatically just register for all implemented INotificationHandler{T}?
+ builder.AddNotificationHandler();
+ builder.AddNotificationHandler();
+ builder.AddNotificationHandler();
+ builder.AddNotificationHandler();
+ builder.Services.AddUnique();
+ builder.Services.AddUnique();
+ builder.Services.AddUnique();
+ builder.Services.AddUnique();
+
+ builder.Services.AddUnique();
+ builder.Services.AddUnique(factory =>
+ {
+ ModelsBuilderSettings config = factory.GetRequiredService>().Value;
+ if (config.ModelsMode == ModelsMode.PureLive)
+ {
+ return factory.GetRequiredService();
+ }
+ else
+ {
+ TypeLoader typeLoader = factory.GetRequiredService();
+ IPublishedValueFallback publishedValueFallback = factory.GetRequiredService();
+ IEnumerable types = typeLoader
+ .GetTypes() // element models
+ .Concat(typeLoader.GetTypes()); // content models
+ return new PublishedModelFactory(types, publishedValueFallback);
+ }
+ });
+
+ return builder;
+ }
+
+ ///
+ /// Can be called if using an external models builder to remove the embedded models builder controller features
+ ///
+ public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder)
+ {
+ builder.Services.AddSingleton();
+ return builder;
+ }
+
+ private static IUmbracoBuilder AddPureLiveRazorEngine(this IUmbracoBuilder builder)
+ {
+ // See notes in RefreshingRazorViewEngine for information on what this is doing.
+
+ // copy the current collection, we need to use this later to rebuild a container
+ // to re-create the razor compiler provider
+ var initialCollection = new ServiceCollection
+ {
+ builder.Services
+ };
+
+ // Replace the default with our custom engine
+ builder.Services.AddSingleton(
+ s => new RefreshingRazorViewEngine(
+ () =>
+ {
+ // re-create the original container so that a brand new IRazorPageActivator
+ // is produced, if we don't re-create the container then it will just return the same instance.
+ ServiceProvider recreatedServices = initialCollection.BuildServiceProvider();
+ return recreatedServices.GetRequiredService();
+ }, s.GetRequiredService()));
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs b/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs
new file mode 100644
index 0000000000..8f7e118b6a
--- /dev/null
+++ b/src/Umbraco.ModelsBuilder.Embedded/DisableModelsBuilderNotificationHandler.cs
@@ -0,0 +1,29 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Umbraco.Core.Events;
+using Umbraco.ModelsBuilder.Embedded.BackOffice;
+using Umbraco.ModelsBuilder.Embedded.DependencyInjection;
+using Umbraco.Web.Features;
+
+namespace Umbraco.ModelsBuilder.Embedded
+{
+ ///
+ /// Used in conjunction with
+ ///
+ internal class DisableModelsBuilderNotificationHandler : INotificationHandler
+ {
+ private readonly UmbracoFeatures _features;
+
+ public DisableModelsBuilderNotificationHandler(UmbracoFeatures features) => _features = features;
+
+ ///
+ /// Handles the notification to disable MB controller features
+ ///
+ public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
+ {
+ // disable the embedded dashboard controller
+ _features.Disabled.Controllers.Add();
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs
index d7fc051500..11c5d8b605 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs
@@ -1,120 +1,134 @@
using System;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Logging;
-using Umbraco.Core.Configuration;
+using Microsoft.Extensions.Options;
using Umbraco.Configuration;
using Umbraco.Core;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Events;
using Umbraco.Core.Hosting;
+using Umbraco.Extensions;
using Umbraco.ModelsBuilder.Embedded.Building;
using Umbraco.Web.Cache;
-using Umbraco.Core.Configuration.Models;
-using Microsoft.Extensions.Options;
-using Umbraco.Extensions;
+using Umbraco.Web.Common.Lifetime;
namespace Umbraco.ModelsBuilder.Embedded
{
// supports LiveAppData - but not PureLive
- public sealed class LiveModelsProvider
+ public sealed class LiveModelsProvider : INotificationHandler
{
- private static Mutex _mutex;
- private static int _req;
+ private static int s_req;
private readonly ILogger _logger;
private readonly ModelsBuilderSettings _config;
private readonly ModelsGenerator _modelGenerator;
private readonly ModelsGenerationError _mbErrors;
- private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly IUmbracoRequestLifetime _umbracoRequestLifetime;
+ private readonly IMainDom _mainDom;
- // we do not manage pure live here
- internal bool IsEnabled => _config.ModelsMode.IsLiveNotPure();
-
- public LiveModelsProvider(ILogger logger, IOptions config, ModelsGenerator modelGenerator, ModelsGenerationError mbErrors, IHostingEnvironment hostingEnvironment)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public LiveModelsProvider(
+ ILogger logger,
+ IOptions config,
+ ModelsGenerator modelGenerator,
+ ModelsGenerationError mbErrors,
+ IUmbracoRequestLifetime umbracoRequestLifetime,
+ IMainDom mainDom)
{
_logger = logger;
_config = config.Value ?? throw new ArgumentNullException(nameof(config));
_modelGenerator = modelGenerator;
_mbErrors = mbErrors;
- _hostingEnvironment = hostingEnvironment;
+ _umbracoRequestLifetime = umbracoRequestLifetime;
+ _mainDom = mainDom;
}
- internal void Install()
+ // we do not manage pure live here
+ internal bool IsEnabled => _config.ModelsMode.IsLiveNotPure();
+
+ ///
+ /// Handles the notification
+ ///
+ public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
{
- // just be sure
+ Install();
+ return Task.CompletedTask;
+ }
+
+ private void Install()
+ {
+ // don't run if not enabled
if (!IsEnabled)
+ {
return;
+ }
- // initialize mutex
- // ApplicationId will look like "/LM/W3SVC/1/Root/AppName"
- // name is system-wide and must be less than 260 chars
- var name = _hostingEnvironment.ApplicationId + "/UmbracoLiveModelsProvider";
+ // Must register with maindom in order to function.
+ // If registration is not successful then events are not bound
+ // and we also don't generate models.
+ _mainDom.Register(() =>
+ {
+ _umbracoRequestLifetime.RequestEnd += (sender, context) => AppEndRequest(context);
- _mutex = new Mutex(false, name); //TODO: Replace this with MainDom? Seems we now have 2x implementations of almost the same thing
-
- // anything changes, and we want to re-generate models.
- ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
- DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
-
- // at the end of a request since we're restarting the pool
- // NOTE - this does NOT trigger - see module below
- //umbracoApplication.EndRequest += GenerateModelsIfRequested;
+ // anything changes, and we want to re-generate models.
+ ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
+ DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration;
+ });
}
// NOTE
- // Using HttpContext Items fails because CacheUpdated triggers within
- // some asynchronous backend task where we seem to have no HttpContext.
-
- // So we use a static (non request-bound) var to register that models
+ // CacheUpdated triggers within some asynchronous backend task where
+ // we have no HttpContext. So we use a static (non request-bound)
+ // var to register that models
// need to be generated. Could be by another request. Anyway. We could
// have collisions but... you know the risk.
private void RequestModelsGeneration(object sender, EventArgs args)
{
- //HttpContext.Current.Items[this] = true;
_logger.LogDebug("Requested to generate models.");
- Interlocked.Exchange(ref _req, 1);
+ Interlocked.Exchange(ref s_req, 1);
}
- public void GenerateModelsIfRequested()
+ private void GenerateModelsIfRequested()
{
- //if (HttpContext.Current.Items[this] == null) return;
- if (Interlocked.Exchange(ref _req, 0) == 0) return;
+ if (Interlocked.Exchange(ref s_req, 0) == 0)
+ {
+ return;
+ }
- // cannot use a simple lock here because we don't want another AppDomain
- // to generate while we do... and there could be 2 AppDomains if the app restarts.
-
- try
+ // cannot proceed unless we are MainDom
+ if (_mainDom.IsMainDom)
{
- _logger.LogDebug("Generate models...");
- const int timeout = 2 * 60 * 1000; // 2 mins
- _mutex.WaitOne(timeout); // wait until it is safe, and acquire
- _logger.LogInformation("Generate models now.");
- GenerateModels();
- _mbErrors.Clear();
- _logger.LogInformation("Generated.");
+ try
+ {
+ _logger.LogDebug("Generate models...");
+ _logger.LogInformation("Generate models now.");
+ _modelGenerator.GenerateModels();
+ _mbErrors.Clear();
+ _logger.LogInformation("Generated.");
+ }
+ catch (TimeoutException)
+ {
+ _logger.LogWarning("Timeout, models were NOT generated.");
+ }
+ catch (Exception e)
+ {
+ _mbErrors.Report("Failed to build Live models.", e);
+ _logger.LogError("Failed to generate models.", e);
+ }
}
- catch (TimeoutException)
+ else
{
- _logger.LogWarning("Timeout, models were NOT generated.");
- }
- catch (Exception e)
- {
- _mbErrors.Report("Failed to build Live models.", e);
- _logger.LogError("Failed to generate models.", e);
- }
- finally
- {
- _mutex.ReleaseMutex(); // release
+ // this will only occur if this appdomain was MainDom and it has
+ // been released while trying to regenerate models.
+ _logger.LogWarning("Cannot generate models while app is shutting down");
}
}
- private void GenerateModels()
- {
- // EnableDllModels will recycle the app domain - but this request will end properly
- _modelGenerator.GenerateModels();
- }
-
- public void AppEndRequest(HttpContext context)
+ private void AppEndRequest(HttpContext context)
{
if (context.Request.IsClientSideRequest())
{
diff --git a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs
similarity index 68%
rename from src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs
rename to src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs
index 9a139014fa..37ff1c735a 100644
--- a/src/Umbraco.ModelsBuilder.Embedded/Compose/ModelsBuilderComponent.cs
+++ b/src/Umbraco.ModelsBuilder.Embedded/ModelsBuilderNotificationHandler.cs
@@ -1,102 +1,110 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Options;
-using Umbraco.Configuration;
-using Umbraco.Core.Composing;
+using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Events;
using Umbraco.Core.IO;
+using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Core.Strings;
using Umbraco.Extensions;
using Umbraco.ModelsBuilder.Embedded.BackOffice;
-using Umbraco.Web.Common.Lifetime;
using Umbraco.Web.Common.ModelBinders;
using Umbraco.Web.WebAssets;
-namespace Umbraco.ModelsBuilder.Embedded.Compose
+namespace Umbraco.ModelsBuilder.Embedded
{
- internal class ModelsBuilderComponent : IComponent
+
+ ///
+ /// Handles and notifications to initialize MB
+ ///
+ internal class ModelsBuilderNotificationHandler : INotificationHandler, INotificationHandler
{
private readonly ModelsBuilderSettings _config;
private readonly IShortStringHelper _shortStringHelper;
- private readonly LiveModelsProvider _liveModelsProvider;
- private readonly OutOfDateModelsStatus _outOfDateModels;
private readonly LinkGenerator _linkGenerator;
- private readonly IUmbracoRequestLifetime _umbracoRequestLifetime;
+ private readonly ContentModelBinder _modelBinder;
- public ModelsBuilderComponent(IOptions config, IShortStringHelper shortStringHelper,
- LiveModelsProvider liveModelsProvider, OutOfDateModelsStatus outOfDateModels, LinkGenerator linkGenerator,
- IUmbracoRequestLifetime umbracoRequestLifetime)
+ public ModelsBuilderNotificationHandler(
+ IOptions config,
+ IShortStringHelper shortStringHelper,
+ LinkGenerator linkGenerator,
+ ContentModelBinder modelBinder)
{
_config = config.Value;
_shortStringHelper = shortStringHelper;
- _liveModelsProvider = liveModelsProvider;
- _outOfDateModels = outOfDateModels;
_shortStringHelper = shortStringHelper;
_linkGenerator = linkGenerator;
- _umbracoRequestLifetime = umbracoRequestLifetime;
+ _modelBinder = modelBinder;
}
- public void Initialize()
+ ///
+ /// Handles the notification
+ ///
+ public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
{
// always setup the dashboard
// note: UmbracoApiController instances are automatically registered
- InstallServerVars();
- _umbracoRequestLifetime.RequestEnd += (sender, context) => _liveModelsProvider.AppEndRequest(context);
+ _modelBinder.ModelBindingException += ContentModelBinder_ModelBindingException;
- ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException;
-
- if (_config.Enable)
+ if (_config.ModelsMode != ModelsMode.Nothing)
+ {
FileService.SavingTemplate += FileService_SavingTemplate;
+ }
- if (_config.ModelsMode.IsLiveNotPure())
- _liveModelsProvider.Install();
-
- if (_config.FlagOutOfDateModels)
- _outOfDateModels.Install();
+ return Task.CompletedTask;
}
- public void Terminate()
+ ///
+ /// Handles the notification
+ ///
+ public Task HandleAsync(ServerVariablesParsing notification, CancellationToken cancellationToken)
{
- ServerVariablesParser.Parsing -= ServerVariablesParser_Parsing;
- ContentModelBinder.ModelBindingException -= ContentModelBinder_ModelBindingException;
- FileService.SavingTemplate -= FileService_SavingTemplate;
- }
+ IDictionary serverVars = notification.ServerVariables;
-
- private void InstallServerVars()
- {
- // register our URL - for the backoffice API
- ServerVariablesParser.Parsing += ServerVariablesParser_Parsing;
- }
-
- private void ServerVariablesParser_Parsing(object sender, Dictionary serverVars)
- {
if (!serverVars.ContainsKey("umbracoUrls"))
+ {
throw new ArgumentException("Missing umbracoUrls.");
+ }
+
var umbracoUrlsObject = serverVars["umbracoUrls"];
if (umbracoUrlsObject == null)
+ {
throw new ArgumentException("Null umbracoUrls");
+ }
+
if (!(umbracoUrlsObject is Dictionary umbracoUrls))
+ {
throw new ArgumentException("Invalid umbracoUrls");
+ }
if (!serverVars.ContainsKey("umbracoPlugins"))
+ {
throw new ArgumentException("Missing umbracoPlugins.");
- if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins))
- throw new ArgumentException("Invalid umbracoPlugins");
+ }
- umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels());
- umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
+ if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins))
+ {
+ throw new ArgumentException("Invalid umbracoPlugins");
+ }
+
+ umbracoUrls["modelsBuilderBaseUrl"] = _linkGenerator.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels());
+ umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings();
+
+ return Task.CompletedTask;
}
private Dictionary GetModelsBuilderSettings()
{
var settings = new Dictionary
{
- {"enabled", _config.Enable}
+ {"mode", _config.ModelsMode.ToString()}
};
return settings;
@@ -106,22 +114,22 @@ namespace Umbraco.ModelsBuilder.Embedded.Compose
/// Used to check if a template is being created based on a document type, in this case we need to
/// ensure the template markup is correct based on the model name of the document type
///