From 23072a500c155c3c4222c4e7979ec5f16d23da71 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 3 May 2022 19:23:15 +0200 Subject: [PATCH] v10: Fix Block List settings exception and optimize PVCs (#12342) * Don't use MapModelType to get model type * Optimize block list item activation (cache constructors) * Fix exceptions in NestedContentSingleValueConverter (zero content types or multiple stored items) * Add IPublishedModelFactory.GetModelType method to remove work-around --- .../IPublishedModelFactory.cs | 29 ++- .../NoopPublishedModelFactory.cs | 5 +- .../PublishedContent/PublishedModelFactory.cs | 51 +++-- .../ValueConverters/BlockEditorConverter.cs | 7 +- .../BlockListPropertyValueConverter.cs | 186 +++++++++++------- .../NestedContentManyValueConverter.cs | 25 +-- .../NestedContentSingleValueConverter.cs | 31 ++- .../NestedContentValueConverterBase.cs | 42 ++-- .../ModelsBuilder/InMemoryModelFactory.cs | 33 ++-- 9 files changed, 254 insertions(+), 155 deletions(-) diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs index de292a8112..c34a4a6ba4 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs @@ -1,9 +1,7 @@ -using System; using System.Collections; namespace Umbraco.Cms.Core.Models.PublishedContent { - /// /// Provides the published model creation service. /// @@ -13,23 +11,40 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// Creates a strongly-typed model representing a published element. /// /// The original published element. - /// The strongly-typed model representing the published element, or the published element - /// itself it the factory has no model for the corresponding element type. + /// + /// The strongly-typed model representing the published element, + /// or the published element itself it the factory has no model for the corresponding element type. + /// IPublishedElement CreateModel(IPublishedElement element); /// /// Creates a List{T} of a strongly-typed model for a model type alias. /// /// The model type alias. - /// A List{T} of the strongly-typed model, exposed as an IList. + /// + /// A List{T} of the strongly-typed model, exposed as an IList. + /// IList? CreateModelList(string? alias); + /// + /// Gets the Type of a strongly-typed model for a model type alias. + /// + /// The model type alias. + /// + /// The type of the strongly-typed model. + /// + Type GetModelType(string? alias); + /// /// Maps a CLR type that may contain model types, to an actual CLR type. /// /// The CLR type. - /// The actual CLR type. - /// See for more details. + /// + /// The actual CLR type. + /// + /// + /// See for more details. + /// Type MapModelType(Type type); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs index f53a5236a9..93b6948edc 100644 --- a/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/NoopPublishedModelFactory.cs @@ -1,6 +1,4 @@ -using System; using System.Collections; -using System.Collections.Generic; namespace Umbraco.Cms.Core.Models.PublishedContent { @@ -14,6 +12,9 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// public IList CreateModelList(string? alias) => new List(); + /// + public Type GetModelType(string? alias) => typeof(IPublishedElement); + /// public Type MapModelType(Type type) => typeof(IPublishedElement); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs index 2d40874b57..7053a238e6 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs @@ -1,6 +1,4 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Reflection; namespace Umbraco.Cms.Core.Models.PublishedContent @@ -59,20 +57,27 @@ namespace Umbraco.Cms.Core.Models.PublishedContent 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, IPublishedElement."); + } + 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, IPublishedElement."); + } var attribute = type.GetCustomAttribute(false); var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; if (modelInfos.TryGetValue(typeName, out var modelInfo)) + { throw new InvalidOperationException($"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType?.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); + } // have to use an unsafe ctor because we don't know the types, really var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); @@ -89,15 +94,16 @@ namespace Umbraco.Cms.Core.Models.PublishedContent public IPublishedElement CreateModel(IPublishedElement element) { // fail fast - if (_modelInfos == null) - return element; - - if (element.ContentType.Alias is null || !_modelInfos.TryGetValue(element.ContentType.Alias, out var modelInfo)) + if (_modelInfos is null || element.ContentType.Alias is null || !_modelInfos.TryGetValue(element.ContentType.Alias, out var modelInfo)) + { return element; + } // ReSharper disable once UseMethodIsInstanceOfType if (modelInfo.ParameterType?.IsAssignableFrom(element.GetType()) == false) + { throw new InvalidOperationException($"Model {modelInfo.ModelType} expects argument of type {modelInfo.ParameterType.FullName}, but got {element.GetType().FullName}."); + } // can cast, because we checked when creating the ctor return (IPublishedElement)modelInfo.Ctor!(element, _publishedValueFallback); @@ -107,21 +113,42 @@ namespace Umbraco.Cms.Core.Models.PublishedContent public IList? CreateModelList(string? alias) { // fail fast - if (_modelInfos == null) - return new List(); - - if (alias is null || !_modelInfos.TryGetValue(alias, out var modelInfo) || modelInfo.ModelType is null) + if (_modelInfos is null || alias is null || !_modelInfos.TryGetValue(alias, out var modelInfo) || modelInfo.ModelType is null) + { return new List(); + } var ctor = modelInfo.ListCtor; - if (ctor != null) return ctor(); + if (ctor != null) + { + return ctor(); + } var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); - if(ctor is not null) return ctor(); + if (ctor is not null) + { + return ctor(); + } + return null; } + /// + public Type GetModelType(string? alias) + { + // fail fast + if (_modelInfos is null || + alias is null || + !_modelInfos.TryGetValue(alias, out var modelInfo) || + modelInfo.ModelType is null) + { + return typeof(IPublishedElement); + } + + return modelInfo.ModelType; + } + /// public Type MapModelType(Type type) => ModelType.Map(type, _modelTypeMap); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs index 8f68baeb40..99dfd29c0b 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -53,11 +52,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { var publishedContentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content; var publishedContentType = publishedContentCache?.GetContentType(contentTypeKey); - if (publishedContentType != null) + if (publishedContentType is not null && publishedContentType.IsElement) { - var modelType = ModelType.For(publishedContentType.Alias); - - return _publishedModelFactory.MapModelType(modelType); + return _publishedModelFactory.GetModelType(publishedContentType.Alias); } return typeof(IPublishedElement); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index dc1663b14d..a35bb83519 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -1,9 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.PublishedContent; @@ -30,37 +27,42 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList); /// - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(BlockListModel); + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(BlockListModel); /// public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; /// - public override object? ConvertSourceToIntermediate(IPublishedElement owner, - IPublishedPropertyType propertyType, object? source, bool preview) - { - return source?.ToString(); - } + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source?.ToString(); /// - public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, - PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { - // NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string - - using (_proflog.DebugDuration( - $"ConvertPropertyToBlockList ({propertyType.DataType.Id})")) + // NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string + using (_proflog.DebugDuration($"ConvertPropertyToBlockList ({propertyType.DataType.Id})")) { var value = (string?)inter; // Short-circuit on empty values - if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty; + if (string.IsNullOrWhiteSpace(value)) + { + return BlockListModel.Empty; + } var converted = _blockListEditorDataConverter.Deserialize(value); - if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty; + if (converted.BlockValue.ContentData.Count == 0) + { + return BlockListModel.Empty; + } var blockListLayout = converted.Layout?.ToObject>(); + if (blockListLayout is null) + { + return BlockListModel.Empty; + } // Get configuration var configuration = propertyType.DataType.ConfigurationAs(); @@ -68,84 +70,130 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { return null; } + var blockConfigMap = configuration.Blocks.ToDictionary(x => x.ContentElementTypeKey); - var validSettingsElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey) - .Where(x => x.HasValue).Distinct().ToList(); // Convert the content data var contentPublishedElements = new Dictionary(); foreach (var data in converted.BlockValue.ContentData) { - if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) continue; + if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) + { + continue; + } var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview); - if (element == null) continue; + if (element == null) + { + continue; + } contentPublishedElements[element.Key] = element; } // If there are no content elements, it doesn't matter what is stored in layout - if (contentPublishedElements.Count == 0) return BlockListModel.Empty; + if (contentPublishedElements.Count == 0) + { + return BlockListModel.Empty; + } // Convert the settings data var settingsPublishedElements = new Dictionary(); - foreach (var data in converted.BlockValue.SettingsData) + var validSettingsElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey).Where(x => x.HasValue).Distinct().ToList(); + if (validSettingsElementTypes is not null) { - if (!validSettingsElementTypes?.Contains(data.ContentTypeKey) ?? false) continue; - - var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview); - if (element == null) continue; - - settingsPublishedElements[element.Key] = element; - } - - var layout = new List(); - if (blockListLayout is not null) - { - foreach (var layoutItem in blockListLayout) + foreach (var data in converted.BlockValue.SettingsData) { - // Get the content reference - var contentGuidUdi = (GuidUdi?)layoutItem.ContentUdi; - if (contentGuidUdi is null || !contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData)) - continue; - - if (contentData is null || (!blockConfigMap.TryGetValue(contentData.ContentType.Key, out var blockConfig))) - continue; - - // Get the setting reference - IPublishedElement? settingsData = null; - var settingGuidUdi = layoutItem.SettingsUdi is not null ? (GuidUdi)layoutItem.SettingsUdi : null; - if (settingGuidUdi is not null) - settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData); - - // This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again - // We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted - if (settingsData != null && (!blockConfig.SettingsElementTypeKey.HasValue || - settingsData.ContentType.Key != - blockConfig.SettingsElementTypeKey)) + if (!validSettingsElementTypes.Contains(data.ContentTypeKey)) { - settingsData = null; + continue; } - // Get settings type from configuration - var settingsType = blockConfig.SettingsElementTypeKey.HasValue - ? _blockConverter.GetModelType(blockConfig.SettingsElementTypeKey.Value) - : typeof(IPublishedElement); - - // TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow - var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsType); - var layoutRef = (BlockListItem?)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, - settingGuidUdi, settingsData); - - if (layoutRef is not null) + var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview); + if (element is null) { - layout.Add(layoutRef); + continue; } + + settingsPublishedElements[element.Key] = element; } } - var model = new BlockListModel(layout); - return model; + // Cache constructors locally (it's tied to the current IPublishedSnapshot and IPublishedModelFactory) + var blockListItemActivator = new BlockListItemActivator(_blockConverter); + + var list = new List(); + foreach (var layoutItem in blockListLayout) + { + // Get the content reference + var contentGuidUdi = (GuidUdi?)layoutItem.ContentUdi; + if (contentGuidUdi is null || !contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData)) + { + continue; + } + + if (!blockConfigMap.TryGetValue(contentData.ContentType.Key, out var blockConfig)) + { + continue; + } + + // Get the setting reference + IPublishedElement? settingsData = null; + var settingGuidUdi = (GuidUdi?)layoutItem.SettingsUdi; + if (settingGuidUdi is not null) + { + settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData); + } + + // This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again + // We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted + if (settingsData is not null && (!blockConfig.SettingsElementTypeKey.HasValue || settingsData.ContentType.Key != blockConfig.SettingsElementTypeKey)) + { + settingsData = null; + } + + // Create instance (use content/settings type from configuration) + var layoutRef = blockListItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, contentGuidUdi, contentData, settingGuidUdi, settingsData); + + list.Add(layoutRef); + } + + return new BlockListModel(list); + } + } + + private class BlockListItemActivator + { + private readonly BlockEditorConverter _blockConverter; + private readonly Dictionary<(Guid, Guid?), Func> _contructorCache = new(); + + public BlockListItemActivator(BlockEditorConverter blockConverter) + => _blockConverter = blockConverter; + + public BlockListItem CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentUdi, IPublishedElement contentData, Udi? settingsUdi, IPublishedElement? settingsData) + { + if (!_contructorCache.TryGetValue((contentTypeKey, settingsTypeKey), out var constructor)) + { + constructor = _contructorCache[(contentTypeKey, settingsTypeKey)] = EmitConstructor(contentTypeKey, settingsTypeKey); + } + + return constructor(contentUdi, contentData, settingsUdi, settingsData); + } + + private Func EmitConstructor(Guid contentTypeKey, Guid? settingsTypeKey) + { + var contentType = _blockConverter.GetModelType(contentTypeKey); + var settingsType = settingsTypeKey.HasValue ? _blockConverter.GetModelType(settingsTypeKey.Value) : typeof(IPublishedElement); + var type = typeof(BlockListItem<,>).MakeGenericType(contentType, settingsType); + + var constructor = type.GetConstructor(new[] { typeof(Udi), contentType, typeof(Udi), settingsType }); + if (constructor == null) + { + throw new InvalidOperationException($"Could not find the required public constructor on {type}."); + } + + // We use unsafe here, because we know the contructor parameter count and types match + return ReflectionUtilities.EmitConstructorUnsafe>(constructor); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 545ea73bf9..1f482d19fd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -1,8 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Logging; @@ -25,9 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public NestedContentManyValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog) : base(publishedSnapshotAccessor, publishedModelFactory) - { - _proflog = proflog; - } + => _proflog = proflog; /// public override bool IsConverter(IPublishedPropertyType propertyType) @@ -37,6 +33,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var contentTypes = propertyType.DataType.ConfigurationAs()?.ContentTypes; + return contentTypes?.Length == 1 ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) : typeof(IEnumerable); @@ -48,9 +45,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source?.ToString(); - } + => source?.ToString(); /// public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) @@ -64,16 +59,24 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters : new List(); var value = (string?)inter; - if (string.IsNullOrWhiteSpace(value)) return elements; + if (string.IsNullOrWhiteSpace(value)) + { + return elements; + } var objects = JsonConvert.DeserializeObject>(value); - if (objects is null || objects.Count == 0) return elements; + if (objects is null || objects.Count == 0) + { + return elements; + } foreach (var sourceObject in objects) { var element = ConvertToElement(sourceObject, referenceCacheLevel, preview); if (element != null) + { elements.Add(element); + } } return elements; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs index 19ced217f7..0ec450606f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs @@ -1,8 +1,6 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Logging; @@ -25,9 +23,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public NestedContentSingleValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog) : base(publishedSnapshotAccessor, publishedModelFactory) - { - _proflog = proflog; - } + => _proflog = proflog; /// public override bool IsConverter(IPublishedPropertyType propertyType) @@ -36,10 +32,11 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { - var contentTypes = propertyType.DataType.ConfigurationAs()!.ContentTypes; - return contentTypes?.Length > 1 - ? typeof(IPublishedElement) - : ModelType.For(contentTypes?[0].Alias); + var contentTypes = propertyType.DataType.ConfigurationAs()?.ContentTypes; + + return contentTypes?.Length == 1 + ? ModelType.For(contentTypes[0].Alias) + : typeof(IPublishedElement); } /// @@ -48,9 +45,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) - { - return source?.ToString(); - } + => source?.ToString(); /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) @@ -58,14 +53,18 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) { var value = (string?)inter; - if (string.IsNullOrWhiteSpace(value)) return null; + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } var objects = JsonConvert.DeserializeObject>(value)!; if (objects.Count == 0) + { return null; - if (objects.Count > 1) - throw new InvalidOperationException(); + } + // Only return the first (existing data might contain more than is currently configured) return ConvertToElement(objects[0], referenceCacheLevel, preview); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs index 580cd22930..7942ab3c68 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -14,52 +12,56 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + protected IPublishedModelFactory PublishedModelFactory { get; } + protected NestedContentValueConverterBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory) { _publishedSnapshotAccessor = publishedSnapshotAccessor; PublishedModelFactory = publishedModelFactory; } - protected IPublishedModelFactory PublishedModelFactory { get; } - public static bool IsNested(IPublishedPropertyType publishedProperty) + => publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); + + private static bool IsSingle(IPublishedPropertyType publishedProperty) { - return publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent); + var config = publishedProperty.DataType.ConfigurationAs(); + + return config is not null && config.MinItems == 1 && config.MaxItems == 1; } public static bool IsNestedSingle(IPublishedPropertyType publishedProperty) - { - if (!IsNested(publishedProperty)) - return false; - - var config = publishedProperty.DataType.ConfigurationAs(); - return config?.MinItems == 1 && config.MaxItems == 1; - } + => IsNested(publishedProperty) && IsSingle(publishedProperty); public static bool IsNestedMany(IPublishedPropertyType publishedProperty) - { - return IsNested(publishedProperty) && !IsNestedSingle(publishedProperty); - } + => IsNested(publishedProperty) && !IsSingle(publishedProperty); protected IPublishedElement? ConvertToElement(JObject sourceObject, PropertyCacheLevel referenceCacheLevel, bool preview) { var elementTypeAlias = sourceObject[NestedContentPropertyEditor.ContentTypeAliasPropertyKey]?.ToObject(); if (string.IsNullOrEmpty(elementTypeAlias)) + { return null; + } + var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - // only convert element types - content types will cause an exception when PublishedModelFactory creates the model + + // Only convert element types - content types will cause an exception when PublishedModelFactory creates the model var publishedContentType = publishedSnapshot.Content?.GetContentType(elementTypeAlias); - if (publishedContentType == null || publishedContentType.IsElement == false) + if (publishedContentType is null || publishedContentType.IsElement == false) + { return null; + } var propertyValues = sourceObject.ToObject>(); - - if (propertyValues is null || !propertyValues.TryGetValue("key", out var keyo) - || !Guid.TryParse(keyo!.ToString(), out var key)) + if (propertyValues is null || !propertyValues.TryGetValue("key", out var keyo) || !Guid.TryParse(keyo?.ToString(), out var key)) + { key = Guid.Empty; + } IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor); element = PublishedModelFactory.CreateModel(element); + return element; } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs b/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs index cdf3d774c2..cd5272b500 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/InMemoryModelFactory.cs @@ -1,16 +1,10 @@ -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.Extensions.DependencyModel; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -54,7 +48,6 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder private readonly Lazy _pureLiveDirectory = null!; private bool _disposedValue; - public InMemoryModelFactory( Lazy umbracoServices, IProfilingLogger profilingLogger, @@ -76,6 +69,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder _errors = new ModelsGenerationError(config, _hostingEnvironment); _ver = 1; // zero is for when we had no version _skipver = -1; // nothing to skip + if (!hostingEnvironment.IsHosted) { return; @@ -169,6 +163,24 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder return info is null || info.Ctor is null ? element : info.Ctor(element, _publishedValueFallback); } + /// + public Type GetModelType(string? alias) + { + Infos infos = EnsureModels(); + + // fail fast + if (infos is null || + alias is null || + infos.ModelInfos is null || + !infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo) || + modelInfo.ModelType is null) + { + return typeof(IPublishedElement); + } + + return modelInfo.ModelType; + } + // this runs only once the factory is ready // NOT when building models public Type MapModelType(Type type) @@ -184,12 +196,7 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder Infos infos = EnsureModels(); // fail fast - if (infos is null || alias is null) - { - return new List(); - } - - if (infos.ModelInfos is null || !infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo)) + if (infos is null || alias is null || infos.ModelInfos is null || !infos.ModelInfos.TryGetValue(alias, out ModelInfo? modelInfo)) { return new List(); }