diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.yml b/.github/ISSUE_TEMPLATE/02_feature_request.yml deleted file mode 100644 index 5d53b2f12e..0000000000 --- a/.github/ISSUE_TEMPLATE/02_feature_request.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: 📮 Feature Request -description: Open a feature request, if you want to propose a new feature. -labels: type/feature -body: -- type: dropdown - id: version - attributes: - label: Umbraco version - description: Which major Umbraco version are you proposing a feature for? - options: - - v8 - - v9 - validations: - required: true -- type: textarea - id: summary - attributes: - label: Description - description: Write a brief desciption of your proposed new feature. - validations: - required: true -- type: textarea - attributes: - label: How can you help? - id: help - description: Umbraco''s core team has limited available time, but maybe you can help? - placeholder: > - If we can not work on your suggestion, please don't take it personally. Most likely, it's either: - - - We think your idea is valid, but we can't find the time to work on it. - - - Your idea might be better suited as a package, if it's not suitable for the majority of users. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d5418ad270..ecf10b8854 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: 💡 Features and ideas + url: https://github.com/umbraco/Umbraco-CMS/discussions/new?category=features-and-ideas + about: Start a new discussion when you have ideas or feature requests, eventually discussions can turn into plans - name: ⁉️ Support Question url: https://our.umbraco.com about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum. diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs new file mode 100644 index 0000000000..ed5e6a5d26 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSerializerType.cs @@ -0,0 +1,15 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +namespace Umbraco.Cms.Core.Configuration.Models +{ + /// + /// The serializer type that nucache uses to persist documents in the database. + /// + public enum NuCacheSerializerType + { + JSON, + + MessagePack + } +} diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index aa67038702..865d50afbc 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -12,5 +12,15 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Gets or sets a value defining the BTree block size. /// public int? BTreeBlockSize { get; set; } + + /// + /// The serializer type that nucache uses to persist documents in the database. + /// + public NuCacheSerializerType NuCacheSerializerType { get; set; } + + /// + /// The paging size to use for nucache SQL queries. + /// + public int SqlPageSize { get; set; } = 1000; } } diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index 116f04925e..c6bee5ca4f 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { public static partial class Constants { @@ -24,6 +24,20 @@ { public const string EnsureUniqueNodeName = "Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName"; } + + public static class NuCacheDatabaseDataSource + { + public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; + public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; + public const string SourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin"; + public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect"; + public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount"; + public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect"; + public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount"; + public const string ObjectTypeNotTrashedFilter = "Umbraco.Web.PublishedCache.NuCache.DataSource.ObjectTypeNotTrashedFilter"; + public const string OrderByLevelIdSortOrder = "Umbraco.Web.PublishedCache.NuCache.DataSource.OrderByLevelIdSortOrder"; + + } } } } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 4900ab00e1..1aac7dd1a5 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models { + /// /// Provides a base class for content items. /// diff --git a/src/Umbraco.Core/Models/IReadOnlyContentBase.cs b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs new file mode 100644 index 0000000000..e327a49f29 --- /dev/null +++ b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs @@ -0,0 +1,72 @@ +using System; + +namespace Umbraco.Cms.Core.Models +{ + public interface IReadOnlyContentBase + { + /// + /// Gets the integer identifier of the entity. + /// + int Id { get; } + + /// + /// Gets the Guid unique identifier of the entity. + /// + Guid Key { get; } + + /// + /// Gets the creation date. + /// + DateTime CreateDate { get; } + + /// + /// Gets the last update date. + /// + DateTime UpdateDate { get; } + + /// + /// Gets the name of the entity. + /// + string Name { get; } + + /// + /// Gets the identifier of the user who created this entity. + /// + int CreatorId { get; } + + /// + /// Gets the identifier of the parent entity. + /// + int ParentId { get; } + + /// + /// Gets the level of the entity. + /// + int Level { get; } + + /// + /// Gets the path to the entity. + /// + string Path { get; } + + /// + /// Gets the sort order of the entity. + /// + int SortOrder { get; } + + /// + /// Gets the content type id + /// + int ContentTypeId { get; } + + /// + /// Gets the identifier of the writer. + /// + int WriterId { get; } + + /// + /// Gets the version identifier. + /// + int VersionId { get; } + } +} diff --git a/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs new file mode 100644 index 0000000000..77d285596a --- /dev/null +++ b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs @@ -0,0 +1,42 @@ +using System; + +namespace Umbraco.Cms.Core.Models +{ + public struct ReadOnlyContentBaseAdapter : IReadOnlyContentBase + { + private readonly IContentBase _content; + + private ReadOnlyContentBaseAdapter(IContentBase content) + { + _content = content ?? throw new ArgumentNullException(nameof(content)); + } + + public static ReadOnlyContentBaseAdapter Create(IContentBase content) => new ReadOnlyContentBaseAdapter(content); + + public int Id => _content.Id; + + public Guid Key => _content.Key; + + public DateTime CreateDate => _content.CreateDate; + + public DateTime UpdateDate => _content.UpdateDate; + + public string Name => _content.Name; + + public int CreatorId => _content.CreatorId; + + public int ParentId => _content.ParentId; + + public int Level => _content.Level; + + public string Path => _content.Path; + + public int SortOrder => _content.SortOrder; + + public int ContentTypeId => _content.ContentTypeId; + + public int WriterId => _content.WriterId; + + public int VersionId => _content.VersionId; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index f8fbc051cd..639dad1c23 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Core.PropertyEditors { + /// /// Marks a class that represents a data editor. /// diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs new file mode 100644 index 0000000000..817bc5aeae --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompression.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors +{ + /// + /// Determines if a property type's value should be compressed in memory + /// + /// + /// + /// + public interface IPropertyCacheCompression + { + bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs new file mode 100644 index 0000000000..86bda9e799 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/IPropertyCacheCompressionOptions.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors +{ + public interface IPropertyCacheCompressionOptions + { + bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor); + } +} diff --git a/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs new file mode 100644 index 0000000000..f2020ecbca --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/NoopPropertyCacheCompressionOptions.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors +{ + /// + /// Default implementation for which does not compress any property data + /// + public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions + { + public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor) => false; + } +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs new file mode 100644 index 0000000000..3664be6101 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheCompression.cs @@ -0,0 +1,51 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.PropertyEditors +{ + + /// + /// Compresses property data based on config + /// + public class PropertyCacheCompression : IPropertyCacheCompression + { + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly IReadOnlyDictionary _contentTypes; + private readonly PropertyEditorCollection _propertyEditors; + private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias), bool> _isCompressedCache; + + public PropertyCacheCompression( + IPropertyCacheCompressionOptions compressionOptions, + IReadOnlyDictionary contentTypes, + PropertyEditorCollection propertyEditors, + ConcurrentDictionary<(int, string), bool> compressedStoragePropertyEditorCache) + { + _compressionOptions = compressionOptions; + _contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes)); + _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); + _isCompressedCache = compressedStoragePropertyEditorCache; + } + + public bool IsCompressed(IReadOnlyContentBase content, string alias) + { + var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias), x => + { + if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct)) + return false; + + var propertyType = ct.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == alias); + if (propertyType == null) + return false; + + if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor)) + return false; + + return _compressionOptions.IsCompressed(content, propertyType, propertyEditor); + }); + + return compressedStorage; + } + } +} diff --git a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs index 7358611711..5fa8c16832 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs @@ -36,16 +36,26 @@ namespace Umbraco.Cms.Core.PublishedCache /// /// A value indicating whether to consider unpublished content. /// The content unique identifier. - /// The route. - /// The value of overrides defaults. + /// A special string formatted route path. + /// + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item + /// + /// The value of overrides defaults. + /// string GetRouteById(bool preview, int contentId, string culture = null); /// /// Gets the route for a content identified by its unique identifier. /// /// The content unique identifier. - /// The route. + /// A special string formatted route path. /// Considers published or unpublished content depending on defaults. + /// + /// The resulting string is a special encoded route string that may contain the domain ID + /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: {domainId}/route-path-of-item + /// string GetRouteById(int contentId, string culture = null); } } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index d1e113d16c..360f277da3 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -36,7 +36,6 @@ namespace Umbraco.Cms.Core.PublishedCache /// /// Rebuilds internal database caches (but does not reload). /// - /// The operation batch size to process the items /// If not null will process content for the matching content types, if empty will process all content /// If not null will process content for the matching media types, if empty will process all media /// If not null will process content for the matching members types, if empty will process all members @@ -47,7 +46,6 @@ namespace Umbraco.Cms.Core.PublishedCache /// RefreshAllPublishedSnapshot method. /// void Rebuild( - int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs index 9becde2ebe..34a14d7deb 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; namespace Umbraco.Cms.Infrastructure.DependencyInjection @@ -14,5 +15,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection /// The builder. public static MapperCollectionBuilder Mappers(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + + public static NPocoMapperCollectionBuilder NPocoMappers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index f49931c105..e974c633a0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -45,6 +45,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; @@ -66,6 +67,8 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(factory => factory.GetRequiredService().CreateDatabase()); builder.Services.AddUnique(factory => factory.GetRequiredService().SqlContext); + builder.NPocoMappers().Add(); + builder.Services.AddUnique(); builder.Services.AddUnique(); @@ -206,9 +209,18 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection var databaseSchemaCreatorFactory = factory.GetRequiredService(); var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var loggerFactory = factory.GetRequiredService(); + var npocoMappers = factory.GetRequiredService(); return globalSettings.Value.MainDomLock.Equals("SqlMainDomLock") || isWindows == false - ? (IMainDomLock)new SqlMainDomLock(loggerFactory.CreateLogger(), loggerFactory, globalSettings, connectionStrings, dbCreator, hostingEnvironment, databaseSchemaCreatorFactory) + ? (IMainDomLock)new SqlMainDomLock( + loggerFactory.CreateLogger(), + loggerFactory, + globalSettings, + connectionStrings, + dbCreator, + hostingEnvironment, + databaseSchemaCreatorFactory, + npocoMappers) : new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment); }); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 12d8a4afd2..56555fe006 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; using Umbraco.Extensions; @@ -200,6 +201,10 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade // to 8.10.0 To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); + // to 8.15.0... + To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); + To("{4695D0C9-0729-4976-985B-048D503665D8}"); + // to 9.0.0 To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs new file mode 100644 index 0000000000..d73e617e45 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +{ + public class AddCmsContentNuByteColumn : MigrationBase + { + public AddCmsContentNuByteColumn(IMigrationContext context) + : base(context) + { + + } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "dataRaw"); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs new file mode 100644 index 0000000000..e7989962b8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs @@ -0,0 +1,66 @@ +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +{ + public class UpgradedIncludeIndexes : MigrationBase + { + public UpgradedIncludeIndexes(IMigrationContext context) + : base(context) + { + + } + + public override void Migrate() + { + // Need to drop the FK for the redirect table before modifying the unique id index + Delete.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); + var nodeDtoIndexes = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType", $"IX_{NodeDto.TableName}_Level" }; + DeleteIndexes(nodeDtoIndexes); // delete existing ones + CreateIndexes(nodeDtoIndexes); // update/add + // Now re-create the FK for the redirect table + Create.ForeignKey() + .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl) + .ForeignColumn("contentKey") + .ToTable(NodeDto.TableName) + .PrimaryColumn("uniqueID") + .Do(); + + + var contentVersionIndexes = new[] { $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current" }; + DeleteIndexes(contentVersionIndexes); // delete existing ones + CreateIndexes(contentVersionIndexes); // update/add + } + + private void DeleteIndexes(params string[] toDelete) + { + var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach (var i in toDelete) + if (IndexExists(i)) + Delete.Index(i).OnTable(tableDef.Name).Do(); + + } + + private void CreateIndexes(params string[] toCreate) + { + var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach (var c in toCreate) + { + // get the definition by name + var index = tableDef.Indexes.First(x => x.Name == c); + new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); + } + + } + } +} diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs index e8d7e89522..cc80a34310 100644 --- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs +++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Core.Models +namespace Umbraco.Cms.Core.Models { /// /// Model used in Razor Views for rendering diff --git a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs index 8758d17d07..e7286d683f 100644 --- a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs +++ b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs @@ -1,7 +1,6 @@ -using System; +using System; using System.IO; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs index 5c9f41b097..6d1db2dc5f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs @@ -31,5 +31,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations /// Gets or sets the column name(s) for the current index /// public string ForColumns { get; set; } + + /// + /// Gets or sets the column name(s) for the columns to include in the index + /// + public string IncludeColumns { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index a3ca285918..407672c995 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -167,6 +167,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); } } + if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + { + var columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim()); + foreach (var column in columns) + { + definition.IncludeColumns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); + } + } return definition; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index a1e14ac580..822bf79383 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -5,17 +5,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions { public class IndexDefinition { - public IndexDefinition() - { - Columns = new List(); - } - public virtual string Name { get; set; } public virtual string SchemaName { get; set; } public virtual string TableName { get; set; } public virtual string ColumnName { get; set; } - public virtual ICollection Columns { get; set; } + public virtual ICollection Columns { get; set; } = new List(); + public virtual ICollection IncludeColumns { get; set; } = new List(); public IndexTypes IndexType { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs index 27f277293c..a5ca3496ee 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs @@ -25,9 +25,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos /// [Column("data")] [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] public string Data { get; set; } [Column("rv")] public long Rv { get; set; } + + [Column("dataRaw")] + [NullSetting(NullSetting = NullSettings.Null)] + public byte[] RawData { get; set; } + + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs index 72e4edf85d..53e90859d9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [Column("nodeId")] [ForeignKey(typeof(ContentDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId")] public int NodeId { get; set; } [Column("versionDate")] // TODO: db rename to 'updateDate' @@ -32,6 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero [Column("current")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")] public bool Current { get; set; } // about current: diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index d401a6f5b8..4639e4529a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [Column("uniqueId")] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] [Constraint(Default = SystemMethods.NewGuid)] public Guid UniqueId { get; set; } @@ -29,7 +29,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] public int ParentId { get; set; } + // NOTE: This index is primarily for the nucache data lookup, see https://github.com/umbraco/Umbraco-CMS/pull/8365#issuecomment-673404177 [Column("level")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Level", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")] public short Level { get; set; } [Column("path")] @@ -55,8 +57,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos public string Text { get; set; } [Column("nodeObjectType")] // TODO: db rename to 'objectType' - [NullSetting(NullSetting = NullSettings.Null)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")] + [NullSetting(NullSetting = NullSettings.Null)] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")] public Guid? NodeObjectType { get; set; } [Column("createDate")] diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PocoMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs similarity index 89% rename from src/Umbraco.Infrastructure/Persistence/Mappers/PocoMapper.cs rename to src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs index 835451755b..c647c4b93e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PocoMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using NPoco; @@ -7,7 +7,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers /// /// Extends NPoco default mapper and ensures that nullable dates are not saved to the database. /// - public class PocoMapper : DefaultMapper + public class NullableDateMapper : DefaultMapper { public override Func GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) { @@ -19,7 +19,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers { var datetime = datetimeVal as DateTime?; if (datetime.HasValue && datetime.Value > DateTime.MinValue) + { return datetime.Value; + } return null; }; diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs index 8f32ad6d72..813eea58ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs @@ -25,12 +25,13 @@ namespace Umbraco.Extensions /// The number of rows to load per page /// /// + /// Specify a custom Sql command to get the total count, if null is specified than the auto-generated sql count will be used /// /// /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to /// iterate over each row with a reader using Query vs Fetch. /// - public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) + public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql, Sql sqlCount) { var sqlString = sql.SQL; var sqlArgs = sql.Arguments; @@ -40,12 +41,12 @@ namespace Umbraco.Extensions do { // Get the paged queries - database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var sqlCount, out var sqlPage); + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage); // get the item count once if (itemCount == null) { - itemCount = database.ExecuteScalar(sqlCount, sqlArgs); + itemCount = database.ExecuteScalar(sqlCount?.SQL ?? generatedSqlCount, sqlCount?.Arguments ?? sqlArgs); } pageIndex++; @@ -58,6 +59,22 @@ namespace Umbraco.Extensions } while ((pageIndex * pageSize) < itemCount); } + /// + /// Iterates over the result of a paged data set with a db reader + /// + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + public static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) => database.QueryPaged(pageSize, sql, null); + // NOTE // // proper way to do it with TSQL and SQLCE diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs new file mode 100644 index 0000000000..a1b61198d3 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using NPoco; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Infrastructure.Persistence +{ + public sealed class NPocoMapperCollection : BuilderCollectionBase + { + public NPocoMapperCollection(IEnumerable items) : base(items) + { + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs new file mode 100644 index 0000000000..4840ceafe8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs @@ -0,0 +1,10 @@ +using NPoco; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Infrastructure.Persistence +{ + public sealed class NPocoMapperCollectionBuilder : SetCollectionBuilderBase + { + protected override NPocoMapperCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs index 6e08bad7c3..c8121077d9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs @@ -330,8 +330,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying { case ExpressionType.MemberAccess: // false property , i.e. x => !Trashed - SqlParameters.Add(true); - return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; + // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used + // so we want to do an == false + SqlParameters.Add(false); + return Visited ? string.Empty : $"{o} = @{SqlParameters.Count - 1}"; + //return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})"; default: // could be anything else, such as: x => !x.Path.StartsWith("-20") return Visited ? string.Empty : string.Concat("NOT (", o, ")"); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 790f82e0a2..2055006415 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -649,6 +649,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } if (versions.Count == 0) return new Dictionary(); + // TODO: This is a bugger of a query and I believe is the main issue with regards to SQL performance drain when querying content + // which is done when rebuilding caches/indexes/etc... in bulk. We are using an "IN" query on umbracoPropertyData.VersionId + // which then performs a Clustered Index Scan on PK_umbracoPropertyData which means it iterates the entire table which can be enormous! + // especially if there are both a lot of content but worse if there is a lot of versions of that content. + // So is it possible to return this property data without doing an index scan on PK_umbracoPropertyData and without iterating every row + // in the table? + // get all PropertyDataDto for all definitions / versions var allPropertyDataDtos = Database.FetchByGroups(versions, 2000, batch => SqlContext.Sql() diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 0270f77904..0beaae113e 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -414,5 +414,24 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) public override string DropIndex => "DROP INDEX {0} ON {1}"; public override string RenameColumn => "sp_rename '{0}.{1}', '{2}', 'COLUMN'"; + + public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4}){5}"; + public override string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; + + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + var includeColumns = index.IncludeColumns?.Any() ?? false + ? $" INCLUDE ({string.Join(",", index.IncludeColumns.Select(x => GetQuotedColumnName(x.Name)))})" + : string.Empty; + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), + GetQuotedTableName(index.TableName), columns, includeColumns); + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index b0afa9d75b..a864f536b0 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -576,7 +576,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; - public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 102)"; + public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 120)"; public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index aecd0f4c2b..d95b5feaf7 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Data.Common; @@ -7,6 +7,7 @@ using System.Text; using Microsoft.Extensions.Logging; using NPoco; using StackExchange.Profiling; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Extensions; @@ -28,6 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private readonly RetryPolicy _connectionRetryPolicy; private readonly RetryPolicy _commandRetryPolicy; + //private readonly IEnumerable _mapperCollection; private readonly Guid _instanceGuid = Guid.NewGuid(); private List _commands; @@ -40,36 +42,53 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// Used by UmbracoDatabaseFactory to create databases. /// Also used by DatabaseBuilder for creating databases and installing/upgrading. /// - public UmbracoDatabase(string connectionString, ISqlContext sqlContext, DbProviderFactory provider, ILogger logger, IBulkSqlInsertProvider bulkSqlInsertProvider, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, RetryPolicy connectionRetryPolicy = null, RetryPolicy commandRetryPolicy = null) + public UmbracoDatabase( + string connectionString, + ISqlContext sqlContext, + DbProviderFactory provider, + ILogger logger, + IBulkSqlInsertProvider bulkSqlInsertProvider, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + RetryPolicy connectionRetryPolicy = null, + RetryPolicy commandRetryPolicy = null + /*IEnumerable mapperCollection = null*/) : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { SqlContext = sqlContext; - _logger = logger; _bulkSqlInsertProvider = bulkSqlInsertProvider; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; _connectionRetryPolicy = connectionRetryPolicy; _commandRetryPolicy = commandRetryPolicy; - - EnableSqlTrace = EnableSqlTraceDefault; - - NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + //_mapperCollection = mapperCollection; + Init(); } /// /// Initializes a new instance of the class. /// /// Internal for unit tests only. - internal UmbracoDatabase(DbConnection connection, ISqlContext sqlContext, ILogger logger, IBulkSqlInsertProvider bulkSqlInsertProvider) + internal UmbracoDatabase( + DbConnection connection, + ISqlContext sqlContext, + ILogger logger, + IBulkSqlInsertProvider bulkSqlInsertProvider) : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) { SqlContext = sqlContext; _logger = logger; _bulkSqlInsertProvider = bulkSqlInsertProvider; + Init(); + } + private void Init() + { EnableSqlTrace = EnableSqlTraceDefault; - NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + //if (_mapperCollection != null) + //{ + // Mappers.AddRange(_mapperCollection); + //} } #endregion diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 10d6fa2278..72bde97e95 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data.Common; using System.Threading; using Microsoft.Extensions.Logging; @@ -15,6 +15,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence { + /// /// Default implementation of . /// @@ -31,6 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence { private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly NPocoMapperCollection _npocoMappers; private readonly IOptions _globalSettings; private readonly Lazy _mappers; private readonly ILogger _logger; @@ -81,13 +83,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence IOptions connectionStrings, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + NPocoMapperCollection npocoMappers) { _globalSettings = globalSettings; _mappers = mappers ?? throw new ArgumentNullException(nameof(mappers)); _dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator)); _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); + _npocoMappers = npocoMappers; _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _loggerFactory = loggerFactory; @@ -220,46 +224,62 @@ namespace Umbraco.Cms.Infrastructure.Persistence // rest to be lazy-initialized } - private void EnsureInitialized() - { - LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); - } + private void EnsureInitialized() => LazyInitializer.EnsureInitialized(ref _sqlContext, ref _initialized, ref _lock, Initialize); private SqlContext Initialize() { _logger.LogDebug("Initializing."); - if (ConnectionString.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper connection string."); - if (_providerName.IsNullOrWhiteSpace()) throw new InvalidOperationException("The factory has not been configured with a proper provider name."); + if (ConnectionString.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("The factory has not been configured with a proper connection string."); + } + + if (_providerName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("The factory has not been configured with a proper provider name."); + } if (DbProviderFactory == null) + { throw new Exception($"Can't find a provider factory for provider name \"{_providerName}\"."); + } // cannot initialize without being able to talk to the database // TODO: Why not? if (!DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory)) + { throw new Exception("Cannot connect to the database."); + } _connectionRetryPolicy = RetryPolicyFactory.GetDefaultSqlConnectionRetryPolicyByConnectionString(ConnectionString); _commandRetryPolicy = RetryPolicyFactory.GetDefaultSqlCommandRetryPolicyByConnectionString(ConnectionString); - _databaseType = DatabaseType.Resolve(DbProviderFactory.GetType().Name, _providerName); if (_databaseType == null) + { throw new Exception($"Can't find an NPoco database type for provider name \"{_providerName}\"."); + } _sqlSyntax = _dbProviderFactoryCreator.GetSqlSyntaxProvider(_providerName); if (_sqlSyntax == null) + { throw new Exception($"Can't find a sql syntax provider for provider name \"{_providerName}\"."); + } _bulkSqlInsertProvider = _dbProviderFactoryCreator.CreateBulkSqlInsertProvider(_providerName); if (_databaseType.IsSqlServer()) + { UpdateSqlServerDatabaseType(); + } // ensure we have only 1 set of mappers, and 1 PocoDataFactory, for all database // so that everything NPoco is properly cached for the lifetime of the application - _pocoMappers = new NPoco.MapperCollection { new PocoMapper() }; + _pocoMappers = new NPoco.MapperCollection(); + // add all registered mappers for NPoco + _pocoMappers.AddRange(_npocoMappers); + var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver); _pocoDataFactory = factory; var config = new FluentConfig(xmappers => factory); @@ -270,7 +290,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence .WithFluentConfig(config)); // with proper configuration if (_npocoDatabaseFactory == null) + { throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); + } _logger.LogDebug("Initialized."); @@ -293,9 +315,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance private UmbracoDatabase CreateDatabaseInstance() - { - return new UmbracoDatabase(ConnectionString, SqlContext, DbProviderFactory, _loggerFactory.CreateLogger(), _bulkSqlInsertProvider, _databaseSchemaCreatorFactory, _connectionRetryPolicy, _commandRetryPolicy); - } + => new UmbracoDatabase( + ConnectionString, + SqlContext, + DbProviderFactory, + _loggerFactory.CreateLogger(), + _bulkSqlInsertProvider, + _databaseSchemaCreatorFactory, + _connectionRetryPolicy, + _commandRetryPolicy); protected override void DisposeResources() { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs index 5d5bc593a6..1e661866b1 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -1,13 +1,13 @@ - + using System; using System.Collections; using System.Collections.Generic; using System.Runtime.Serialization; using Newtonsoft.Json; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; -using Umbraco.Core.Models; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 1c02898334..dc25007e7c 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -25,22 +25,30 @@ namespace Umbraco.Cms.Infrastructure.Runtime { public class SqlMainDomLock : IMainDomLock { - private string _lockId; + private readonly string _lockId; private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; private readonly IOptions _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; - private IUmbracoDatabase _db; - private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly IUmbracoDatabase _db; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax; private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; private bool _errorDuringAcquiring; - private object _locker = new object(); + private readonly object _locker = new object(); private bool _hasTable = false; - public SqlMainDomLock(ILogger logger, ILoggerFactory loggerFactory, IOptions globalSettings, IOptions connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + public SqlMainDomLock( + ILogger logger, + ILoggerFactory loggerFactory, + IOptions globalSettings, + IOptions connectionStrings, + IDbProviderFactoryCreator dbProviderFactoryCreator, + IHostingEnvironment hostingEnvironment, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + NPocoMapperCollection npocoMappers) { // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); @@ -48,13 +56,15 @@ namespace Umbraco.Cms.Infrastructure.Runtime _globalSettings = globalSettings; _sqlServerSyntax = new SqlServerSyntaxProvider(_globalSettings); _hostingEnvironment = hostingEnvironment; - _dbFactory = new UmbracoDatabaseFactory(loggerFactory.CreateLogger(), + _dbFactory = new UmbracoDatabaseFactory( + loggerFactory.CreateLogger(), loggerFactory, _globalSettings, connectionStrings, new Lazy(() => new MapperCollection(Enumerable.Empty())), dbProviderFactoryCreator, - databaseSchemaCreatorFactory); + databaseSchemaCreatorFactory, + npocoMappers); MainDomKey = MainDomKeyPrefix + "-" + (Environment.MachineName + MainDom.GetMainDomId(_hostingEnvironment)).GenerateHash(); } diff --git a/src/Umbraco.Infrastructure/Serialization/AutoInterningStringConverter.cs b/src/Umbraco.Infrastructure/Serialization/AutoInterningStringConverter.cs new file mode 100644 index 0000000000..b8a6baaec8 --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/AutoInterningStringConverter.cs @@ -0,0 +1,37 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Cms.Infrastructure.Serialization +{ + + /// + /// When applied to a string or string collection field will ensure the deserialized strings are interned + /// + /// + /// Borrowed from https://stackoverflow.com/a/34906004/694494 + /// On the same page an interesting approach of using a local intern pool https://stackoverflow.com/a/39605620/694494 which re-uses .NET System.Xml.NameTable + /// + public class AutoInterningStringConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + // CanConvert is not called when a converter is applied directly to a property. + throw new NotImplementedException($"{nameof(AutoInterningStringConverter)} should not be used globally"); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + // Check is in case the value is a non-string literal such as an integer. + var s = reader.TokenType == JsonToken.String + ? string.Intern((string)reader.Value) + : string.Intern((string)JToken.Load(reader)); + return s; + } + + public override bool CanWrite => false; + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); + } +} diff --git a/src/Umbraco.Infrastructure/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs b/src/Umbraco.Infrastructure/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs new file mode 100644 index 0000000000..fa87a9a203 --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Umbraco.Cms.Infrastructure.Serialization +{ + /// + /// When applied to a dictionary with a string key, will ensure the deserialized string keys are interned + /// + /// + /// + /// borrowed from https://stackoverflow.com/a/36116462/694494 + /// + public class AutoInterningStringKeyCaseInsensitiveDictionaryConverter : CaseInsensitiveDictionaryConverter + { + public AutoInterningStringKeyCaseInsensitiveDictionaryConverter() + { + } + public AutoInterningStringKeyCaseInsensitiveDictionaryConverter(StringComparer comparer) : base(comparer) + { + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.StartObject) + { + var dictionary = new Dictionary(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + var key = string.Intern(reader.Value.ToString()); + + if (!reader.Read()) + throw new Exception("Unexpected end when reading object."); + + var v = serializer.Deserialize(reader); + dictionary[key] = v; + break; + case JsonToken.Comment: + break; + case JsonToken.EndObject: + return dictionary; + } + } + } + return null; + } + + } +} diff --git a/src/Umbraco.Infrastructure/Serialization/CaseInsensitiveDictionaryConverter.cs b/src/Umbraco.Infrastructure/Serialization/CaseInsensitiveDictionaryConverter.cs index 3531d4da12..14de08fa7b 100644 --- a/src/Umbraco.Infrastructure/Serialization/CaseInsensitiveDictionaryConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/CaseInsensitiveDictionaryConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using Newtonsoft.Json.Converters; @@ -14,12 +14,24 @@ namespace Umbraco.Cms.Infrastructure.Serialization /// public class CaseInsensitiveDictionaryConverter : CustomCreationConverter { + private readonly StringComparer _comparer; + + public CaseInsensitiveDictionaryConverter() + : this(StringComparer.OrdinalIgnoreCase) + { + } + + public CaseInsensitiveDictionaryConverter(StringComparer comparer) + { + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + } + public override bool CanWrite => false; public override bool CanRead => true; public override bool CanConvert(Type objectType) => typeof(IDictionary).IsAssignableFrom(objectType); - public override IDictionary Create(Type objectType) => new Dictionary(StringComparer.OrdinalIgnoreCase); + public override IDictionary Create(Type objectType) => new Dictionary(_comparer); } } diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs index 4073366fea..667c5262d5 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeBulkSqlInsertProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Data; using System.Data.SqlServerCe; +using System.Data.SqlTypes; using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence; @@ -64,6 +65,17 @@ namespace Umbraco.Cms.Persistence.SqlCe if (NPocoDatabaseExtensions.IncludeColumn(pocoData, columns[i])) { var val = columns[i].Value.GetValue(record); + + if (val is byte[]) + { + var bytes = val as byte[]; + updatableRecord.SetSqlBinary(i, new SqlBinary(bytes)); + } + else + { + updatableRecord.SetValue(i, val); + } + updatableRecord.SetValue(i, val); } } diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs b/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs new file mode 100644 index 0000000000..28b2d6a185 --- /dev/null +++ b/src/Umbraco.Persistence.SqlCe/SqlCeImageMapper.cs @@ -0,0 +1,63 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Data.SqlServerCe; +using System.Linq; +using System.Reflection; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence; + +namespace Umbraco.Cms.Persistence.SqlCe +{ + /// + /// Custom NPoco mapper for SqlCe + /// + /// + /// Work arounds to handle special columns + /// + public class SqlCeImageMapper : DefaultMapper + { + private readonly IUmbracoDatabaseFactory _dbFactory; + + public SqlCeImageMapper(IUmbracoDatabaseFactory dbFactory) => _dbFactory = dbFactory; + + public override Func GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) + { + if (sourceMemberInfo.GetMemberInfoType() == typeof(byte[])) + { + return x => + { + var pd = _dbFactory.SqlContext.PocoDataFactory.ForType(sourceMemberInfo.DeclaringType); + if (pd == null) return null; + var col = pd.AllColumns.FirstOrDefault(x => x.MemberInfoData.MemberInfo == sourceMemberInfo); + if (col == null) return null; + + return new SqlCeParameter + { + SqlDbType = SqlDbType.Image, + Value = x ?? Array.Empty() + }; + }; + } + return base.GetToDbConverter(destType, sourceMemberInfo); + } + + public override Func GetParameterConverter(DbCommand dbCommand, Type sourceType) + { + if (sourceType == typeof(byte[])) + { + return x => + { + var param = new SqlCeParameter + { + SqlDbType = SqlDbType.Image, + Value = x + }; + return param; + }; + + } + return base.GetParameterConverter(dbCommand, sourceType); + } + } +} diff --git a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs index 60e456a651..717eada77f 100644 --- a/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs @@ -24,6 +24,11 @@ namespace Umbraco.Cms.Persistence.SqlCe public SqlCeSyntaxProvider(IOptions globalSettings) { _globalSettings = globalSettings; + BlobColumnDefinition = "IMAGE"; + // This is silly to have to do this but the way these inherited classes are structured it's the easiest + // way without an overhaul in type map initialization + DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + } public override string ProviderName => Constants.DatabaseProviders.SqlCe; @@ -278,15 +283,36 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() } } - - public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } + public override string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; + public override string Format(IndexDefinition index) + { + var name = string.IsNullOrEmpty(index.Name) + ? $"IX_{index.TableName}_{index.ColumnName}" + : index.Name; + var columns = index.Columns.Any() + ? string.Join(",", index.Columns.Select(x => GetQuotedColumnName(x.Name))) + : GetQuotedColumnName(index.ColumnName); + + + return string.Format(CreateIndex, GetIndexType(index.IndexType), " ", GetQuotedName(name), + GetQuotedTableName(index.TableName), columns); + } + public override string GetSpecialDbType(SpecialDbTypes dbTypes) { if (dbTypes == SpecialDbTypes.NVARCHARMAX) // SqlCE does not have nvarchar(max) for now return "NTEXT"; return base.GetSpecialDbType(dbTypes); } + public override SqlDbType GetSqlDbType(DbType dbType) + { + if (DbType.Binary == dbType) + { + return SqlDbType.Image; + } + return base.GetSqlDbType(dbType); + } } } diff --git a/src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs b/src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs index c97d312c8f..bb05e14706 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentNodeKit.cs @@ -16,6 +16,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache public bool IsNull => ContentTypeId < 0; + public static ContentNodeKit Empty { get; } = new ContentNodeKit(); public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 }; public void Build( diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs index f0b58d0712..59b73c21b0 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentDataSerializer.cs @@ -3,10 +3,22 @@ using CSharpTest.Net.Serialization; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { - class ContentDataSerializer : ISerializer + /// + /// Serializes/Deserializes data to BTree data source for + /// + internal class ContentDataSerializer : ISerializer { - private static readonly DictionaryOfPropertyDataSerializer PropertiesSerializer = new DictionaryOfPropertyDataSerializer(); - private static readonly DictionaryOfCultureVariationSerializer CultureVariationsSerializer = new DictionaryOfCultureVariationSerializer(); + public ContentDataSerializer(IDictionaryOfPropertyDataSerializer dictionaryOfPropertyDataSerializer = null) + { + _dictionaryOfPropertyDataSerializer = dictionaryOfPropertyDataSerializer; + if(_dictionaryOfPropertyDataSerializer == null) + { + _dictionaryOfPropertyDataSerializer = DefaultPropertiesSerializer; + } + } + private static readonly DictionaryOfPropertyDataSerializer DefaultPropertiesSerializer = new DictionaryOfPropertyDataSerializer(); + private static readonly DictionaryOfCultureVariationSerializer DefaultCultureVariationsSerializer = new DictionaryOfCultureVariationSerializer(); + private readonly IDictionaryOfPropertyDataSerializer _dictionaryOfPropertyDataSerializer; public ContentData ReadFrom(Stream stream) { @@ -19,8 +31,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource VersionDate = PrimitiveSerializer.DateTime.ReadFrom(stream), WriterId = PrimitiveSerializer.Int32.ReadFrom(stream), TemplateId = PrimitiveSerializer.Int32.ReadFrom(stream), - Properties = PropertiesSerializer.ReadFrom(stream), - CultureInfos = CultureVariationsSerializer.ReadFrom(stream) + Properties = _dictionaryOfPropertyDataSerializer.ReadFrom(stream), // TODO: We don't want to allocate empty arrays + CultureInfos = DefaultCultureVariationsSerializer.ReadFrom(stream) // TODO: We don't want to allocate empty arrays }; } @@ -36,8 +48,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { PrimitiveSerializer.Int32.WriteTo(value.TemplateId.Value, stream); } - PropertiesSerializer.WriteTo(value.Properties, stream); - CultureVariationsSerializer.WriteTo(value.CultureInfos, stream); + _dictionaryOfPropertyDataSerializer.WriteTo(value.Properties, stream); + DefaultCultureVariationsSerializer.WriteTo(value.CultureInfos, stream); } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs index bb473fd34d..db0886ce79 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.ContentNodeKitSerializer.cs @@ -5,7 +5,17 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { internal class ContentNodeKitSerializer : ISerializer { - static readonly ContentDataSerializer DataSerializer = new ContentDataSerializer(); + public ContentNodeKitSerializer(ContentDataSerializer contentDataSerializer = null) + { + _contentDataSerializer = contentDataSerializer; + if(_contentDataSerializer == null) + { + _contentDataSerializer = DefaultDataSerializer; + } + } + static readonly ContentDataSerializer DefaultDataSerializer = new ContentDataSerializer(); + private readonly ContentDataSerializer _contentDataSerializer; + //static readonly ListOfIntSerializer ChildContentIdsSerializer = new ListOfIntSerializer(); public ContentNodeKit ReadFrom(Stream stream) @@ -26,10 +36,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource }; var hasDraft = PrimitiveSerializer.Boolean.ReadFrom(stream); if (hasDraft) - kit.DraftData = DataSerializer.ReadFrom(stream); + kit.DraftData = _contentDataSerializer.ReadFrom(stream); var hasPublished = PrimitiveSerializer.Boolean.ReadFrom(stream); if (hasPublished) - kit.PublishedData = DataSerializer.ReadFrom(stream); + kit.PublishedData = _contentDataSerializer.ReadFrom(stream); return kit; } @@ -47,11 +57,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource PrimitiveSerializer.Boolean.WriteTo(value.DraftData != null, stream); if (value.DraftData != null) - DataSerializer.WriteTo(value.DraftData, stream); + _contentDataSerializer.WriteTo(value.DraftData, stream); PrimitiveSerializer.Boolean.WriteTo(value.PublishedData != null, stream); if (value.PublishedData != null) - DataSerializer.WriteTo(value.PublishedData, stream); + _contentDataSerializer.WriteTo(value.PublishedData, stream); } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs index 835fa47e34..1ed603d003 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfCultureVariationSerializer.cs @@ -6,6 +6,9 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { + /// + /// Serializes/Deserializes culture variant data as a dictionary for BTree + /// internal class DictionaryOfCultureVariationSerializer : SerializerBase, ISerializer> { public IReadOnlyDictionary ReadFrom(Stream stream) @@ -18,8 +21,13 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); for (var i = 0; i < pcount; i++) { - var languageId = PrimitiveSerializer.String.ReadFrom(stream); - var cultureVariation = new CultureVariation { Name = ReadStringObject(stream), UrlSegment = ReadStringObject(stream), Date = ReadDateTime(stream) }; + var languageId = string.Intern(PrimitiveSerializer.String.ReadFrom(stream)); + var cultureVariation = new CultureVariation + { + Name = ReadStringObject(stream), + UrlSegment = ReadStringObject(stream), + Date = ReadDateTime(stream) + }; dict[languageId] = cultureVariation; } return dict; diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs index b7c23eca04..31b19ae1ca 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.DictionaryOfPropertyDataSerializer.cs @@ -6,44 +6,47 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { - internal class DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer> + /// + /// Serializes/Deserializes property data as a dictionary for BTree + /// + internal class DictionaryOfPropertyDataSerializer : SerializerBase, ISerializer>, IDictionaryOfPropertyDataSerializer { public IDictionary ReadFrom(Stream stream) { - var dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // read properties count var pcount = PrimitiveSerializer.Int32.ReadFrom(stream); + var dict = new Dictionary(pcount,StringComparer.InvariantCultureIgnoreCase); // read each property for (var i = 0; i < pcount; i++) { // read property alias - var key = PrimitiveSerializer.String.ReadFrom(stream); + var key = string.Intern(PrimitiveSerializer.String.ReadFrom(stream)); // read values count var vcount = PrimitiveSerializer.Int32.ReadFrom(stream); // create pdata and add to the dictionary - var pdatas = new List(); + var pdatas = new PropertyData[vcount]; // for each value, read and add to pdata for (var j = 0; j < vcount; j++) { var pdata = new PropertyData(); - pdatas.Add(pdata); + pdatas[j] =pdata; // everything that can be null is read/written as object // even though - culture and segment should never be null here, as 'null' represents // the 'current' value, and string.Empty should be used to represent the invariant or // neutral values - PropertyData throws when getting nulls, so falling back to // string.Empty here - what else? - pdata.Culture = ReadStringObject(stream) ?? string.Empty; - pdata.Segment = ReadStringObject(stream) ?? string.Empty; + pdata.Culture = ReadStringObject(stream, true) ?? string.Empty; + pdata.Segment = ReadStringObject(stream, true) ?? string.Empty; pdata.Value = ReadObject(stream); } - dict[key] = pdatas.ToArray(); + dict[key] = pdatas; } return dict; } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs index ace3e571d3..1b8089d8ba 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs @@ -7,10 +7,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { internal class BTree { - public static BPlusTree GetTree(string filepath, bool exists, NuCacheSettings settings) + public static BPlusTree GetTree(string filepath, bool exists, NuCacheSettings settings, ContentDataSerializer contentDataSerializer = null) { var keySerializer = new PrimitiveSerializer(); - var valueSerializer = new ContentNodeKitSerializer(); + var valueSerializer = new ContentNodeKitSerializer(contentDataSerializer); var options = new BPlusTree.OptionsV2(keySerializer, valueSerializer) { CreateFile = exists ? CreatePolicy.IfNeeded : CreatePolicy.Always, @@ -37,6 +37,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource //btree. return tree; + } private static int GetBlockSize(NuCacheSettings settings) diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs new file mode 100644 index 0000000000..af3136da54 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Umbraco.Cms.Infrastructure.Serialization; +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + /// + /// The content model stored in the content cache database table serialized as JSON + /// + [DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys + public class ContentCacheDataModel + { + // TODO: We don't want to allocate empty arrays + //dont serialize empty properties + [DataMember(Order = 0)] + [JsonProperty("pd")] + [JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))] + public Dictionary PropertyData { get; set; } + + [DataMember(Order = 1)] + [JsonProperty("cd")] + [JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))] + public Dictionary CultureData { get; set; } + + [DataMember(Order = 2)] + [JsonProperty("us")] + public string UrlSegment { get; set; } + + //Legacy properties used to deserialize existing nucache db entries + [IgnoreDataMember] + [JsonProperty("properties")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + private Dictionary LegacyPropertyData { set => PropertyData = value; } + + [IgnoreDataMember] + [JsonProperty("cultureData")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + private Dictionary LegacyCultureData { set => CultureData = value; } + + [IgnoreDataMember] + [JsonProperty("urlSegment")] + private string LegacyUrlSegment { set => UrlSegment = value; } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs new file mode 100644 index 0000000000..5938927243 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + /// + /// The serialization result from for which the serialized value + /// will be either a string or a byte[] + /// + public struct ContentCacheDataSerializationResult : IEquatable + { + public ContentCacheDataSerializationResult(string stringData, byte[] byteData) + { + StringData = stringData; + ByteData = byteData; + } + + public string StringData { get; } + public byte[] ByteData { get; } + + public override bool Equals(object obj) + => obj is ContentCacheDataSerializationResult result && Equals(result); + + public bool Equals(ContentCacheDataSerializationResult other) + => StringData == other.StringData && + EqualityComparer.Default.Equals(ByteData, other.ByteData); + + public override int GetHashCode() + { + var hashCode = 1910544615; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(StringData); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ByteData); + return hashCode; + } + + public static bool operator ==(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + => left.Equals(right); + + public static bool operator !=(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + => !(left == right); + } + +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs new file mode 100644 index 0000000000..2148ff89fe --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + [Flags] + public enum ContentCacheDataSerializerEntityType + { + Document = 1, + Media = 2, + Member = 4 + } + +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs index f135ee8bb4..a4fd455468 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { - // represents everything that is specific to edited or published version + /// + /// Represents everything that is specific to an edited or published content version + /// public class ContentData { public string Name { get; set; } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs deleted file mode 100644 index 12e289660e..0000000000 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Umbraco.Cms.Infrastructure.Serialization; - -namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource -{ - /// - /// The content item 1:M data that is serialized to JSON - /// - internal class ContentNestedData - { - // dont serialize empty properties - [JsonProperty("pd")] - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - public Dictionary PropertyData { get; set; } - - [JsonProperty("cd")] - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - public Dictionary CultureData { get; set; } - - [JsonProperty("us")] - public string UrlSegment { get; set; } - - // Legacy properties used to deserialize existing nucache db entries - [JsonProperty("properties")] - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - private Dictionary LegacyPropertyData { set { PropertyData = value; } } - - [JsonProperty("cultureData")] - [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] - private Dictionary LegacyCultureData { set { CultureData = value; } } - - [JsonProperty("urlSegment")] - private string LegacyUrlSegment { set { UrlSegment = value; } } - } -} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs index 2c3f8160f5..78a8ea6e81 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentSourceDto.cs @@ -1,12 +1,13 @@ -using System; +using System; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { // read-only dto - internal class ContentSourceDto + internal class ContentSourceDto : IReadOnlyContentBase { public int Id { get; set; } - public Guid Uid { get; set; } + public Guid Key { get; set; } public int ContentTypeId { get; set; } public int Level { get; set; } @@ -27,6 +28,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource public int EditWriterId { get; set; } public int EditTemplateId { get; set; } public string EditData { get; set; } + public byte[] EditDataRaw { get; set; } // published data public int PublishedVersionId { get; set; } @@ -35,5 +37,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource public int PubWriterId { get; set; } public int PubTemplateId { get; set; } public string PubData { get; set; } + public byte[] PubDataRaw { get; set; } + + // Explicit implementation + DateTime IReadOnlyContentBase.UpdateDate => EditVersionDate; + string IReadOnlyContentBase.Name => EditName; + int IReadOnlyContentBase.WriterId => EditWriterId; } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs index fc6da41fe3..17a3b3e4dc 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; using Newtonsoft.Json; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource @@ -6,30 +7,39 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource /// /// Represents the culture variation information on a content item /// + [DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys public class CultureVariation { + [DataMember(Order = 0)] [JsonProperty("nm")] public string Name { get; set; } + [DataMember(Order = 1)] [JsonProperty("us")] public string UrlSegment { get; set; } + [DataMember(Order = 2)] [JsonProperty("dt")] public DateTime Date { get; set; } + [DataMember(Order = 3)] [JsonProperty("isd")] public bool IsDraft { get; set; } //Legacy properties used to deserialize existing nucache db entries + [IgnoreDataMember] [JsonProperty("name")] private string LegacyName { set { Name = value; } } + [IgnoreDataMember] [JsonProperty("urlSegment")] private string LegacyUrlSegment { set { UrlSegment = value; } } + [IgnoreDataMember] [JsonProperty("date")] private DateTime LegacyDate { set { Date = value; } } + [IgnoreDataMember] [JsonProperty("isDraft")] private bool LegacyIsDraft { set { IsDraft = value; } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs new file mode 100644 index 0000000000..65412f10d2 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs @@ -0,0 +1,25 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + + /// + /// Serializes/Deserializes document to the SQL Database as a string + /// + /// + /// Resolved from the . This cannot be resolved from DI. + /// + public interface IContentCacheDataSerializer + { + /// + /// Deserialize the data into a + /// + ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData); + + /// + /// Serializes the + /// + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model); + } + +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs new file mode 100644 index 0000000000..40e29b33cb --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs @@ -0,0 +1,16 @@ +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + public interface IContentCacheDataSerializerFactory + { + /// + /// Gets or creates a new instance of + /// + /// + /// + /// This method may return the same instance, however this depends on the state of the application and if any underlying data has changed. + /// This method may also be used to initialize anything before a serialization/deserialization session occurs. + /// + IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types); + } + +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs new file mode 100644 index 0000000000..bffa66898d --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.IO; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + internal interface IDictionaryOfPropertyDataSerializer + { + IDictionary ReadFrom(Stream stream); + void WriteTo(IDictionary value, Stream stream); + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs new file mode 100644 index 0000000000..8fc953f353 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs @@ -0,0 +1,91 @@ +using Newtonsoft.Json; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + + public class JsonContentNestedDataSerializer : IContentCacheDataSerializer + { + // by default JsonConvert will deserialize our numeric values as Int64 + // which is bad, because they were Int32 in the database - take care + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() }, + + // Explicitly specify date handling so that it's consistent and follows the same date handling as MessagePack + DateParseHandling = DateParseHandling.DateTime, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + DateFormatString = "o" + }; + private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable(); + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + { + if (stringData == null && byteData != null) + throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); + + JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings); + using (JsonTextReader reader = new JsonTextReader(new StringReader(stringData))) + { + // reader will get buffer from array pool + reader.ArrayPool = JsonArrayPool.Instance; + reader.PropertyNameTable = _propertyNameTable; + return serializer.Deserialize(reader); + } + } + + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + { + // note that numeric values (which are Int32) are serialized without their + // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 + + var json = JsonConvert.SerializeObject(model); + return new ContentCacheDataSerializationResult(json, null); + } + } + public class JsonArrayPool : IArrayPool + { + public static readonly JsonArrayPool Instance = new JsonArrayPool(); + + public char[] Rent(int minimumLength) + { + // get char array from System.Buffers shared pool + return ArrayPool.Shared.Rent(minimumLength); + } + + public void Return(char[] array) + { + // return char array to System.Buffers shared pool + ArrayPool.Shared.Return(array); + } + } + public class AutomaticJsonNameTable : DefaultJsonNameTable + { + int nAutoAdded = 0; + int maxToAutoAdd; + + public AutomaticJsonNameTable(int maxToAdd) + { + this.maxToAutoAdd = maxToAdd; + } + + public override string Get(char[] key, int start, int length) + { + var s = base.Get(key, start, length); + + if (s == null && nAutoAdded < maxToAutoAdd) + { + s = new string(key, start, length); + Add(s); + nAutoAdded++; + } + + return s; + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..f5590fd069 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + internal class JsonContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory + { + private readonly Lazy _serializer = new Lazy(); + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) => _serializer.Value; + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs new file mode 100644 index 0000000000..a5edbc2512 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs @@ -0,0 +1,109 @@ +using K4os.Compression.LZ4; +using System; +using System.Diagnostics; +using System.Text; +using Umbraco.Cms.Core.Exceptions; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + /// + /// Lazily decompresses a LZ4 Pickler compressed UTF8 string + /// + [DebuggerDisplay("{Display}")] + internal struct LazyCompressedString + { + private byte[] _bytes; + private string _str; + private readonly object _locker; + + /// + /// Constructor + /// + /// LZ4 Pickle compressed UTF8 String + public LazyCompressedString(byte[] bytes) + { + _locker = new object(); + _bytes = bytes; + _str = null; + } + + public byte[] GetBytes() + { + if (_bytes == null) + { + throw new InvalidOperationException("The bytes have already been expanded"); + } + + return _bytes; + } + + /// + /// Returns the decompressed string from the bytes. This methods can only be called once. + /// + /// + /// Throws if this is called more than once + public string DecompressString() + { + if (_str != null) + { + return _str; + } + + lock (_locker) + { + if (_str != null) + { + // double check + return _str; + } + + if (_bytes == null) + { + throw new InvalidOperationException("Bytes have already been cleared"); + } + + _str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); + _bytes = null; + } + return _str; + } + + /// + /// Used to display debugging output since ToString() can only be called once + /// + private string Display + { + get + { + if (_str != null) + { + return $"Decompressed: {_str}"; + } + + lock (_locker) + { + if (_str != null) + { + // double check + return $"Decompressed: {_str}"; + } + + if (_bytes == null) + { + // This shouldn't happen + throw new PanicException("Bytes have already been cleared"); + } + else + { + return $"Compressed Bytes: {_bytes.Length}"; + } + } + } + } + + public override string ToString() => DecompressString(); + + public static implicit operator string(LazyCompressedString l) => l.ToString(); + } + +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs new file mode 100644 index 0000000000..66c01cf1dc --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs @@ -0,0 +1,126 @@ +using K4os.Compression.LZ4; +using MessagePack; +using MessagePack.Resolvers; +using System; +using System.Linq; +using System.Text; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + + /// + /// Serializes/Deserializes document to the SQL Database as bytes using MessagePack + /// + public class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer + { + private readonly MessagePackSerializerOptions _options; + private readonly IPropertyCacheCompression _propertyOptions; + + public MsgPackContentNestedDataSerializer(IPropertyCacheCompression propertyOptions) + { + _propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions)); + + var defaultOptions = ContractlessStandardResolver.Options; + var resolver = CompositeResolver.Create( + + // TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how + // to do that but it would seem to be with a custom message pack resolver but I haven't quite figured out based on the docs how + // to do that since that is part of the int key -> string mapping operation, might have to see the source code to figure that one out. + // There are docs here on how to build one of these: https://github.com/neuecc/MessagePack-CSharp/blob/master/README.md#low-level-api-imessagepackformattert + // and there are a couple examples if you search on google for them but this will need to be a separate project. + // NOTE: resolver custom types first + // new ContentNestedDataResolver(), + + // finally use standard resolver + defaultOptions.Resolver + ); + + _options = defaultOptions + .WithResolver(resolver) + .WithCompression(MessagePackCompression.Lz4BlockArray); + } + + public string ToJson(byte[] bin) + { + var json = MessagePackSerializer.ConvertToJson(bin, _options); + return json; + } + + public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData) + { + if (byteData != null) + { + var cacheModel = MessagePackSerializer.Deserialize(byteData, _options); + Expand(content, cacheModel); + return cacheModel; + } + else if (stringData != null) + { + // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) + var bin = Convert.FromBase64String(stringData); + var cacheModel = MessagePackSerializer.Deserialize(bin, _options); + Expand(content, cacheModel); + return cacheModel; + } + else + { + return null; + } + } + + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model) + { + Compress(content, model); + var bytes = MessagePackSerializer.Serialize(model, _options); + return new ContentCacheDataSerializationResult(null, bytes); + } + + /// + /// Used during serialization to compress properties + /// + /// + /// + /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed + /// but this will go a step further and double compress property data so that it is stored in the nucache file + /// as compressed bytes and therefore will exist in memory as compressed bytes. That is, until the bytes are + /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant + /// memory savings but could also affect performance of first rendering pages while decompression occurs. + /// + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model) + { + foreach(var propertyAliasToData in model.PropertyData) + { + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + { + foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string)) + { + property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes((string)property.Value), LZ4Level.L00_FAST); + } + } + } + } + + /// + /// Used during deserialization to map the property data as lazy or expand the value + /// + /// + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData) + { + foreach (var propertyAliasToData in nestedData.PropertyData) + { + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key)) + { + foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null)) + { + if (property.Value is byte[] byteArrayValue) + { + property.Value = new LazyCompressedString(byteArrayValue); + } + } + } + } + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..9c3d1fe3c2 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs @@ -0,0 +1,69 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource +{ + internal class MsgPackContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory + { + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly ConcurrentDictionary<(int, string), bool> _isCompressedCache = new ConcurrentDictionary<(int, string), bool>(); + + public MsgPackContentNestedDataSerializerFactory( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + PropertyEditorCollection propertyEditors, + IPropertyCacheCompressionOptions compressionOptions) + { + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _propertyEditors = propertyEditors; + _compressionOptions = compressionOptions; + } + + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) + { + // Depending on which entity types are being requested, we need to look up those content types + // to initialize the compression options. + // We need to initialize these options now so that any data lookups required are completed and are not done while the content cache + // is performing DB queries which will result in errors since we'll be trying to query with open readers. + // NOTE: The calls to GetAll() below should be cached if the data has not been changed. + + var contentTypes = new Dictionary(); + if ((types & ContentCacheDataSerializerEntityType.Document) == ContentCacheDataSerializerEntityType.Document) + { + foreach(var ct in _contentTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + if ((types & ContentCacheDataSerializerEntityType.Media) == ContentCacheDataSerializerEntityType.Media) + { + foreach (var ct in _mediaTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + if ((types & ContentCacheDataSerializerEntityType.Member) == ContentCacheDataSerializerEntityType.Member) + { + foreach (var ct in _memberTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + + var compression = new PropertyCacheCompression(_compressionOptions, contentTypes, _propertyEditors, _isCompressedCache); + var serializer = new MsgPackContentNestedDataSerializer(compression); + + return serializer; + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs index 4320ce89bd..befd1698de 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs @@ -1,14 +1,20 @@ using System; using System.ComponentModel; +using System.Runtime.Serialization; using Newtonsoft.Json; +using Umbraco.Cms.Infrastructure.Serialization; namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { + + [DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys public class PropertyData { private string _culture; private string _segment; + [DataMember(Order = 0)] + [JsonConverter(typeof(AutoInterningStringConverter))] [DefaultValue("")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")] public string Culture @@ -17,6 +23,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource set => _culture = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null } + [DataMember(Order = 1)] + [JsonConverter(typeof(AutoInterningStringConverter))] [DefaultValue("")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")] public string Segment @@ -25,23 +33,26 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource set => _segment = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null } + [DataMember(Order = 2)] [JsonProperty("v")] public object Value { get; set; } - // Legacy properties used to deserialize existing nucache db entries + [IgnoreDataMember] [JsonProperty("culture")] private string LegacyCulture { set => Culture = value; } + [IgnoreDataMember] [JsonProperty("seg")] private string LegacySegment { set => Segment = value; } + [IgnoreDataMember] [JsonProperty("val")] private object LegacyValue { diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs index 48fadce476..fb2f39f711 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/SerializerBase.cs @@ -6,100 +6,218 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource { internal abstract class SerializerBase { + private const char PrefixNull = 'N'; + private const char PrefixString = 'S'; + private const char PrefixInt32 = 'I'; + private const char PrefixUInt16 = 'H'; + private const char PrefixUInt32 = 'J'; + private const char PrefixLong = 'L'; + private const char PrefixFloat = 'F'; + private const char PrefixDouble = 'B'; + private const char PrefixDateTime = 'D'; + private const char PrefixByte = 'O'; + private const char PrefixByteArray = 'A'; + private const char PrefixCompressedStringByteArray = 'C'; + private const char PrefixSignedByte = 'E'; + private const char PrefixBool = 'M'; + private const char PrefixGuid = 'G'; + private const char PrefixTimeSpan = 'T'; + private const char PrefixInt16 = 'Q'; + private const char PrefixChar = 'R'; + protected string ReadString(Stream stream) => PrimitiveSerializer.String.ReadFrom(stream); protected int ReadInt(Stream stream) => PrimitiveSerializer.Int32.ReadFrom(stream); protected long ReadLong(Stream stream) => PrimitiveSerializer.Int64.ReadFrom(stream); protected float ReadFloat(Stream stream) => PrimitiveSerializer.Float.ReadFrom(stream); protected double ReadDouble(Stream stream) => PrimitiveSerializer.Double.ReadFrom(stream); protected DateTime ReadDateTime(Stream stream) => PrimitiveSerializer.DateTime.ReadFrom(stream); + protected byte[] ReadByteArray(Stream stream) => PrimitiveSerializer.Bytes.ReadFrom(stream); - private T? ReadObject(Stream stream, char t, Func read) + private T? ReadStruct(Stream stream, char t, Func read) where T : struct { var type = PrimitiveSerializer.Char.ReadFrom(stream); - if (type == 'N') return null; + if (type == PrefixNull) return null; if (type != t) throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{t}'."); return read(stream); } - protected string ReadStringObject(Stream stream) // required 'cos string is not a struct + protected string ReadStringObject(Stream stream, bool intern = false) // required 'cos string is not a struct { var type = PrimitiveSerializer.Char.ReadFrom(stream); - if (type == 'N') return null; - if (type != 'S') - throw new NotSupportedException($"Cannot deserialize type '{type}', expected 'S'."); - return PrimitiveSerializer.String.ReadFrom(stream); + if (type == PrefixNull) return null; + if (type != PrefixString) + throw new NotSupportedException($"Cannot deserialize type '{type}', expected '{PrefixString}'."); + return intern + ? string.Intern(PrimitiveSerializer.String.ReadFrom(stream)) + : PrimitiveSerializer.String.ReadFrom(stream); } - protected int? ReadIntObject(Stream stream) => ReadObject(stream, 'I', ReadInt); - protected long? ReadLongObject(Stream stream) => ReadObject(stream, 'L', ReadLong); - protected float? ReadFloatObject(Stream stream) => ReadObject(stream, 'F', ReadFloat); - protected double? ReadDoubleObject(Stream stream) => ReadObject(stream, 'B', ReadDouble); - protected DateTime? ReadDateTimeObject(Stream stream) => ReadObject(stream, 'D', ReadDateTime); + protected int? ReadIntObject(Stream stream) => ReadStruct(stream, PrefixInt32, ReadInt); + protected long? ReadLongObject(Stream stream) => ReadStruct(stream, PrefixLong, ReadLong); + protected float? ReadFloatObject(Stream stream) => ReadStruct(stream, PrefixFloat, ReadFloat); + protected double? ReadDoubleObject(Stream stream) => ReadStruct(stream, PrefixDouble, ReadDouble); + protected DateTime? ReadDateTimeObject(Stream stream) => ReadStruct(stream, PrefixDateTime, ReadDateTime); protected object ReadObject(Stream stream) => ReadObject(PrimitiveSerializer.Char.ReadFrom(stream), stream); + /// + /// Reads in a value based on its char type + /// + /// + /// + /// + /// + /// This will incur boxing because the result is an object but in most cases the value will be a struct. + /// When the type is known use the specific methods like instead + /// protected object ReadObject(char type, Stream stream) { switch (type) { - case 'N': + case PrefixNull: return null; - case 'S': + case PrefixString: return PrimitiveSerializer.String.ReadFrom(stream); - case 'I': + case PrefixInt32: return PrimitiveSerializer.Int32.ReadFrom(stream); - case 'L': + case PrefixUInt16: + return PrimitiveSerializer.UInt16.ReadFrom(stream); + case PrefixUInt32: + return PrimitiveSerializer.UInt32.ReadFrom(stream); + case PrefixByte: + return PrimitiveSerializer.Byte.ReadFrom(stream); + case PrefixLong: return PrimitiveSerializer.Int64.ReadFrom(stream); - case 'F': + case PrefixFloat: return PrimitiveSerializer.Float.ReadFrom(stream); - case 'B': + case PrefixDouble: return PrimitiveSerializer.Double.ReadFrom(stream); - case 'D': + case PrefixDateTime: return PrimitiveSerializer.DateTime.ReadFrom(stream); + case PrefixByteArray: + return PrimitiveSerializer.Bytes.ReadFrom(stream); + case PrefixSignedByte: + return PrimitiveSerializer.SByte.ReadFrom(stream); + case PrefixBool: + return PrimitiveSerializer.Boolean.ReadFrom(stream); + case PrefixGuid: + return PrimitiveSerializer.Guid.ReadFrom(stream); + case PrefixTimeSpan: + return PrimitiveSerializer.TimeSpan.ReadFrom(stream); + case PrefixInt16: + return PrimitiveSerializer.Int16.ReadFrom(stream); + case PrefixChar: + return PrimitiveSerializer.Char.ReadFrom(stream); + case PrefixCompressedStringByteArray: + return new LazyCompressedString(PrimitiveSerializer.Bytes.ReadFrom(stream)); default: throw new NotSupportedException($"Cannot deserialize unknown type '{type}'."); } } + /// + /// Writes a value to the stream ensuring it's char type is prefixed to the value for reading later + /// + /// + /// + /// + /// This method will incur boxing if the value is a struct. When the type is known use the + /// to write the value directly. + /// protected void WriteObject(object value, Stream stream) { if (value == null) { - PrimitiveSerializer.Char.WriteTo('N', stream); + PrimitiveSerializer.Char.WriteTo(PrefixNull, stream); } else if (value is string stringValue) { - PrimitiveSerializer.Char.WriteTo('S', stream); + PrimitiveSerializer.Char.WriteTo(PrefixString, stream); PrimitiveSerializer.String.WriteTo(stringValue, stream); } else if (value is int intValue) { - PrimitiveSerializer.Char.WriteTo('I', stream); + PrimitiveSerializer.Char.WriteTo(PrefixInt32, stream); PrimitiveSerializer.Int32.WriteTo(intValue, stream); } + else if (value is byte byteValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixByte, stream); + PrimitiveSerializer.Byte.WriteTo(byteValue, stream); + } + else if (value is ushort ushortValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixUInt16, stream); + PrimitiveSerializer.UInt16.WriteTo(ushortValue, stream); + } else if (value is long longValue) { - PrimitiveSerializer.Char.WriteTo('L', stream); + PrimitiveSerializer.Char.WriteTo(PrefixLong, stream); PrimitiveSerializer.Int64.WriteTo(longValue, stream); } else if (value is float floatValue) { - PrimitiveSerializer.Char.WriteTo('F', stream); + PrimitiveSerializer.Char.WriteTo(PrefixFloat, stream); PrimitiveSerializer.Float.WriteTo(floatValue, stream); } else if (value is double doubleValue) { - PrimitiveSerializer.Char.WriteTo('B', stream); + PrimitiveSerializer.Char.WriteTo(PrefixDouble, stream); PrimitiveSerializer.Double.WriteTo(doubleValue, stream); } else if (value is DateTime dateValue) { - PrimitiveSerializer.Char.WriteTo('D', stream); + PrimitiveSerializer.Char.WriteTo(PrefixDateTime, stream); PrimitiveSerializer.DateTime.WriteTo(dateValue, stream); } + else if (value is uint uInt32Value) + { + PrimitiveSerializer.Char.WriteTo(PrefixUInt32, stream); + PrimitiveSerializer.UInt32.WriteTo(uInt32Value, stream); + } + else if (value is byte[] byteArrayValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixByteArray, stream); + PrimitiveSerializer.Bytes.WriteTo(byteArrayValue, stream); + } + else if (value is LazyCompressedString lazyCompressedString) + { + PrimitiveSerializer.Char.WriteTo(PrefixCompressedStringByteArray, stream); + PrimitiveSerializer.Bytes.WriteTo(lazyCompressedString.GetBytes(), stream); + } + else if (value is sbyte signedByteValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixSignedByte, stream); + PrimitiveSerializer.SByte.WriteTo(signedByteValue, stream); + } + else if (value is bool boolValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixBool, stream); + PrimitiveSerializer.Boolean.WriteTo(boolValue, stream); + } + else if (value is Guid guidValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixGuid, stream); + PrimitiveSerializer.Guid.WriteTo(guidValue, stream); + } + else if (value is TimeSpan timespanValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixTimeSpan, stream); + PrimitiveSerializer.TimeSpan.WriteTo(timespanValue, stream); + } + else if (value is short int16Value) + { + PrimitiveSerializer.Char.WriteTo(PrefixInt16, stream); + PrimitiveSerializer.Int16.WriteTo(int16Value, stream); + } + else if (value is char charValue) + { + PrimitiveSerializer.Char.WriteTo(PrefixChar, stream); + PrimitiveSerializer.Char.WriteTo(charValue, stream); + } else throw new NotSupportedException("Value type " + value.GetType().FullName + " cannot be serialized."); } diff --git a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs index b68662644e..7ef655b2a8 100644 --- a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,12 +1,17 @@ +using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.PublishedCache; +using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; namespace Umbraco.Extensions @@ -49,6 +54,23 @@ namespace Umbraco.Extensions builder.AddNuCacheNotifications(); + builder.AddNotificationHandler(); + builder.Services.AddSingleton(s => + { + IOptions options = s.GetRequiredService>(); + switch (options.Value.NuCacheSerializerType) + { + case NuCacheSerializerType.JSON: + return new JsonContentNestedDataSerializerFactory(); + case NuCacheSerializerType.MessagePack: + return ActivatorUtilities.CreateInstance(s); + default: + throw new IndexOutOfRangeException(); + } + }); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(s => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); + // add the NuCache health check (hidden from type finder) // TODO: no NuCache health check yet // composition.HealthChecks().Add(); @@ -61,6 +83,7 @@ namespace Umbraco.Extensions builder .AddNotificationHandler() .AddNotificationHandler() +#pragma warning disable CS0618 // Type or member is obsolete .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() @@ -68,6 +91,7 @@ namespace Umbraco.Extensions .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() +#pragma warning restore CS0618 // Type or member is obsolete ; return builder; diff --git a/src/Umbraco.PublishedCache.NuCache/NuCacheStartupHandler.cs b/src/Umbraco.PublishedCache.NuCache/NuCacheStartupHandler.cs new file mode 100644 index 0000000000..403b83803b --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/NuCacheStartupHandler.cs @@ -0,0 +1,67 @@ +using System.Configuration; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.PublishedCache +{ + /// + /// Rebuilds the database cache if required when the serializer changes + /// + public class NuCacheStartupHandler : INotificationHandler + { + // TODO: Eventually we should kill this since at some stage we shouldn't even support JSON since we know + // this is faster. + + internal const string Nucache_Serializer_Key = "Umbraco.Web.PublishedCache.NuCache.Serializer"; + private const string JSON_SERIALIZER_VALUE = "JSON"; + private readonly IPublishedSnapshotService _service; + private readonly IKeyValueService _keyValueService; + private readonly IProfilingLogger _profilingLogger; + private readonly ILogger _logger; + + public NuCacheStartupHandler( + IPublishedSnapshotService service, + IKeyValueService keyValueService, + IProfilingLogger profilingLogger, + ILogger logger) + { + _service = service; + _keyValueService = keyValueService; + _profilingLogger = profilingLogger; + _logger = logger; + } + + public void Handle(UmbracoApplicationStartingNotification notification) + => RebuildDatabaseCacheIfSerializerChanged(); + + private void RebuildDatabaseCacheIfSerializerChanged() + { + var serializer = ConfigurationManager.AppSettings[Nucache_Serializer_Key]; + var currentSerializer = _keyValueService.GetValue(Nucache_Serializer_Key); + + if (currentSerializer == null) + { + currentSerializer = JSON_SERIALIZER_VALUE; + } + if (serializer == null) + { + serializer = JSON_SERIALIZER_VALUE; + } + + if (serializer != currentSerializer) + { + _logger.LogWarning("Database NuCache was serialized using {CurrentSerializer}. Currently configured NuCache serializer {Serializer}. Rebuilding Nucache", currentSerializer, serializer); + + using (_profilingLogger.TraceDuration($"Rebuilding NuCache database with {currentSerializer} serializer")) + { + _service.Rebuild(); + _keyValueService.SetValue(Nucache_Serializer_Key, serializer); + } + } + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs index ab15515d55..dd04a63a66 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentRepository.cs @@ -21,19 +21,22 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence void RefreshContent(IContent content); /// - /// Refreshes the nucache database row for the (used for media/members) + /// Refreshes the nucache database row for the /// - void RefreshEntity(IContentBase content); + void RefreshMedia(IMedia content); + + /// + /// Refreshes the nucache database row for the + /// + void RefreshMember(IMember content); /// /// Rebuilds the caches for content, media and/or members based on the content type ids specified /// - /// The operation batch size to process the items /// If not null will process content for the matching content types, if empty will process all content /// If not null will process content for the matching media types, if empty will process all media /// If not null will process content for the matching members types, if empty will process all members void Rebuild( - int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null); diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs index db9129b08b..6cba88da40 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/INuCacheContentService.cs @@ -71,19 +71,22 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence void RefreshContent(IContent content); /// - /// Refreshes the nucache database row for the (used for media/members) + /// Refreshes the nucache database row for the /// - void RefreshEntity(IContentBase content); + void RefreshMedia(IMedia media); + + /// + /// Refreshes the nucache database row for the + /// + void RefreshMember(IMember member); /// /// Rebuilds the database caches for content, media and/or members based on the content type ids specified /// - /// The operation batch size to process the items /// If not null will process content for the matching content types, if empty will process all content /// If not null will process content for the matching media types, if empty will process all media /// If not null will process content for the matching members types, if empty will process all members void Rebuild( - int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null); diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index a3b37b1d00..fb991a666a 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using NPoco; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; @@ -16,7 +18,6 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; -using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; using Constants = Umbraco.Cms.Core.Constants; @@ -25,13 +26,14 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence { public class NuCacheContentRepository : RepositoryBase, INuCacheContentRepository { - private const int PageSize = 500; private readonly ILogger _logger; private readonly IMemberRepository _memberRepository; private readonly IDocumentRepository _documentRepository; private readonly IMediaRepository _mediaRepository; private readonly IShortStringHelper _shortStringHelper; private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory; + private readonly IOptions _nucacheSettings; /// /// Initializes a new instance of the class. @@ -44,7 +46,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence IDocumentRepository documentRepository, IMediaRepository mediaRepository, IShortStringHelper shortStringHelper, - UrlSegmentProviderCollection urlSegmentProviders) + UrlSegmentProviderCollection urlSegmentProviders, + IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, + IOptions nucacheSettings) : base(scopeAccessor, appCaches) { _logger = logger; @@ -53,6 +57,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence _mediaRepository = mediaRepository; _shortStringHelper = shortStringHelper; _urlSegmentProviders = urlSegmentProviders; + _contentCacheDataSerializerFactory = contentCacheDataSerializerFactory; + _nucacheSettings = nucacheSettings; } public void DeleteContentItem(IContentBase item) @@ -60,8 +66,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence public void RefreshContent(IContent content) { + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + // always refresh the edited data - OnRepositoryRefreshed(content, false); + OnRepositoryRefreshed(serializer, content, false); if (content.PublishedState == PublishedState.Unpublishing) { @@ -71,24 +79,36 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence else if (content.PublishedState == PublishedState.Publishing) { // if publishing, refresh the published data - OnRepositoryRefreshed(content, true); + OnRepositoryRefreshed(serializer, content, true); } } - public void RefreshEntity(IContentBase content) - => OnRepositoryRefreshed(content, false); + public void RefreshMedia(IMedia media) + { + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); - private void OnRepositoryRefreshed(IContentBase content, bool published) + OnRepositoryRefreshed(serializer, media, false); + } + + public void RefreshMember(IMember member) + { + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Member); + + OnRepositoryRefreshed(serializer, member, false); + } + + private void OnRepositoryRefreshed(IContentCacheDataSerializer serializer, IContentBase content, bool published) { // use a custom SQL to update row version on each update // db.InsertOrUpdate(dto); - ContentNuDto dto = GetDto(content, published); + ContentNuDto dto = GetDto(content, published, serializer); Database.InsertOrUpdate( dto, - "SET data=@data, rv=rv+1 WHERE nodeId=@id AND published=@published", + "SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published", new { + dataRaw = dto.RawData ?? Array.Empty(), data = dto.Data, id = dto.NodeId, published = dto.Published @@ -96,18 +116,22 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence } public void Rebuild( - int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null) { - RebuildContentDbCache(groupSize, contentTypeIds); - RebuildContentDbCache(groupSize, mediaTypeIds); - RebuildContentDbCache(groupSize, memberTypeIds); + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create( + ContentCacheDataSerializerEntityType.Document + | ContentCacheDataSerializerEntityType.Media + | ContentCacheDataSerializerEntityType.Member); + + RebuildContentDbCache(serializer, _nucacheSettings.Value.SqlPageSize, contentTypeIds); + RebuildMediaDbCache(serializer, _nucacheSettings.Value.SqlPageSize, mediaTypeIds); + RebuildMemberDbCache(serializer, _nucacheSettings.Value.SqlPageSize, memberTypeIds); } // assumes content tree lock - private void RebuildContentDbCache(int groupSize, IReadOnlyCollection contentTypeIds) + private void RebuildContentDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection contentTypeIds) { Guid contentObjectType = Constants.ObjectTypes.Document; @@ -156,12 +180,12 @@ WHERE cmsContentNu.nodeId IN ( foreach (IContent c in descendants) { // always the edited version - items.Add(GetDto(c, false)); + items.Add(GetDto(c, false, serializer)); // and also the published version if it makes any sense if (c.Published) { - items.Add(GetDto(c, true)); + items.Add(GetDto(c, true, serializer)); } count++; @@ -173,7 +197,7 @@ WHERE cmsContentNu.nodeId IN ( } // assumes media tree lock - private void RebuildMediaDbCache(int groupSize, IReadOnlyCollection contentTypeIds) + private void RebuildMediaDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection contentTypeIds) { var mediaObjectType = Constants.ObjectTypes.Media; @@ -217,14 +241,14 @@ WHERE cmsContentNu.nodeId IN ( { // the tree is locked, counting and comparing to total is safe var descendants = _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - var items = descendants.Select(m => GetDto(m, false)).ToList(); + var items = descendants.Select(m => GetDto(m, false, serializer)).ToList(); Database.BulkInsertRecords(items); processed += items.Count; } while (processed < total); } // assumes member tree lock - private void RebuildMemberDbCache(int groupSize, IReadOnlyCollection contentTypeIds) + private void RebuildMemberDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection contentTypeIds) { Guid memberObjectType = Constants.ObjectTypes.Member; @@ -267,7 +291,7 @@ WHERE cmsContentNu.nodeId IN ( do { IEnumerable descendants = _memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); - ContentNuDto[] items = descendants.Select(m => GetDto(m, false)).ToArray(); + ContentNuDto[] items = descendants.Select(m => GetDto(m, false, serializer)).ToArray(); Database.BulkInsertRecords(items); processed += items.Length; } while (processed < total); @@ -327,7 +351,7 @@ AND cmsContentNu.nodeId IS NULL return count == 0; } - private ContentNuDto GetDto(IContentBase content, bool published) + private ContentNuDto GetDto(IContentBase content, bool published, IContentCacheDataSerializer serializer) { // should inject these in ctor // BUT for the time being we decide not to support ConvertDbToXml/String @@ -382,32 +406,31 @@ AND cmsContentNu.nodeId IS NULL } // the dictionary that will be serialized - var nestedData = new ContentNestedData + var contentCacheData = new ContentCacheDataModel { PropertyData = propertyData, CultureData = cultureData, UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders) }; + var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData); + var dto = new ContentNuDto { NodeId = content.Id, Published = published, - - // note that numeric values (which are Int32) are serialized without their - // type (eg "value":1234) and JsonConvert by default deserializes them as Int64 - Data = JsonConvert.SerializeObject(nestedData) + Data = serialized.StringData, + RawData = serialized.ByteData }; return dto; } // we want arrays, we want them all loaded, not an enumerable - private Sql ContentSourcesSelect(Func, Sql> joins = null) + private Sql SqlContentSourcesSelect(Func> joins = null) { - var sql = Sql() - - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + var sqlTemplate = SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect, tsql => + tsql.Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Key"), x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) @@ -422,12 +445,17 @@ AND cmsContentNu.nodeId IS NULL .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) .AndSelect("nuPub", x => Alias(x.Data, "PubData")) - .From(); + .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) + .AndSelect("nuPub", x => Alias(x.RawData, "PubDataRaw")) + + .From()); + + var sql = sqlTemplate.Sql(); + + // TODO: I'm unsure how we can format the below into SQL templates also because right.Current and right.Published end up being parameters if (joins != null) - { - sql = joins(sql); - } + sql = sql.Append(joins(sql.SqlContext)); sql = sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId) @@ -437,94 +465,118 @@ AND cmsContentNu.nodeId IS NULL .InnerJoin().On((left, right) => left.Id == right.Id) .LeftJoin(j => - j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published == true, "pcver", "pdver"), "pcver") .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver") - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit") - .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published, aliasRight: "nuPub"); + .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && right.Published == false, aliasRight: "nuEdit") + .LeftJoin("nuPub").On((left, right) => left.NodeId == right.NodeId && right.Published == true, aliasRight: "nuPub"); return sql; } - public ContentNodeKit GetContentSource(int id) + private Sql SqlContentSourcesSelectUmbracoNodeJoin(ISqlContext sqlContext) { - var sql = ContentSourcesSelect() - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var syntax = sqlContext.SqlSyntax; - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? new ContentNodeKit() : CreateContentNodeKit(dto); + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, builder => + builder.InnerJoin("x") + .On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")); + + var sql = sqlTemplate.Sql(); + return sql; } - public IEnumerable GetAllContentSources() + private Sql SqlWhereNodeId(ISqlContext sqlContext, int id) { - var sql = ContentSourcesSelect() - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var syntax = sqlContext.SqlSyntax; - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId, builder => + builder.Where(x => x.NodeId == SqlTemplate.Arg("id"))); - foreach (var row in Database.QueryPaged(PageSize, sql)) - { - yield return CreateContentNodeKit(row); - } + var sql = sqlTemplate.Sql(id); + return sql; } - public IEnumerable GetBranchContentSources(int id) + private Sql SqlWhereNodeIdX(ISqlContext sqlContext, int id) { - var syntax = SqlSyntax; - var sql = ContentSourcesSelect( - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var syntax = sqlContext.SqlSyntax; - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeIdX, s => + s.Where(x => x.NodeId == SqlTemplate.Arg("id"), "x")); - foreach (var row in Database.QueryPaged(PageSize, sql)) - { - yield return CreateContentNodeKit(row); - } + var sql = sqlTemplate.Sql(id); + return sql; } - public IEnumerable GetTypeContentSources(IEnumerable ids) + private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext) { - if (!ids.Any()) - yield break; + var syntax = sqlContext.SqlSyntax; - var sql = ContentSourcesSelect() - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s => + s.OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder)); - // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. - // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - - foreach (var row in Database.QueryPaged(PageSize, sql)) - { - yield return CreateContentNodeKit(row); - } + var sql = sqlTemplate.Sql(); + return sql; } - private Sql MediaSourcesSelect(Func, Sql> joins = null) + private Sql SqlObjectTypeNotTrashed(ISqlContext sqlContext, Guid nodeObjectType) { - var sql = Sql() + var syntax = sqlContext.SqlSyntax; - .Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Uid"), + var sqlTemplate = sqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => + s.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.Trashed == SqlTemplate.Arg("trashed"))); + + var sql = sqlTemplate.Sql(nodeObjectType, false); + return sql; + } + + /// + /// Returns a slightly more optimized query to use for the document counting when paging over the content sources + /// + /// + /// + private Sql SqlContentSourcesCount(Func> joins = null) + { + var sqlTemplate = SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql => + tsql.Select(x => Alias(x.NodeId, "Id")) + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId)); + + var sql = sqlTemplate.Sql(); + + if (joins != null) + sql = sql.Append(joins(sql.SqlContext)); + + // TODO: We can't use a template with this one because of the 'right.Current' and 'right.Published' ends up being a parameter so not sure how we can do that + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin().On((left, right) => left.Id == right.Id) + .LeftJoin(j => + j.InnerJoin("pdver").On((left, right) => left.Id == right.Id && right.Published, "pcver", "pdver"), "pcver") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver"); + + return sql; + } + + private Sql SqlMediaSourcesSelect(Func> joins = null) + { + var sqlTemplate = SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesSelect, tsql => + tsql.Select(x => Alias(x.NodeId, "Id"), x => Alias(x.UniqueId, "Key"), x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .From(); + .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) + .From()); + + var sql = sqlTemplate.Sql(); if (joins != null) - { - sql = joins(sql); - } + sql = sql.Append(joins(sql.SqlContext)); + // TODO: We can't use a template with this one because of the 'right.Published' ends up being a parameter so not sure how we can do that sql = sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId) .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) @@ -533,78 +585,226 @@ AND cmsContentNu.nodeId IS NULL return sql; } - public ContentNodeKit GetMediaSource(int id) + private Sql SqlMediaSourcesCount(Func> joins = null) { - var sql = MediaSourcesSelect() - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && x.NodeId == id && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sqlTemplate = SqlContext.Templates.Get(Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesCount, tsql => + tsql.Select(x => Alias(x.NodeId, "Id")).From()); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? new ContentNodeKit() : CreateMediaNodeKit(dto); + var sql = sqlTemplate.Sql(); + + if (joins != null) + sql = sql.Append(joins(sql.SqlContext)); + + // TODO: We can't use a template with this one because of the 'right.Current' ends up being a parameter so not sure how we can do that + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current); + + return sql; } - public IEnumerable GetAllMediaSources() + public ContentNodeKit GetContentSource(int id) { - var sql = MediaSourcesSelect() - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sql = SqlContentSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeId(SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) return ContentNodeKit.Empty; + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + return CreateContentNodeKit(dto, serializer); + } + + public IEnumerable GetAllContentSources() + { + var sql = SqlContentSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + // Use a more efficient COUNT query + var sqlCountQuery = SqlContentSourcesCount() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)); + + var sqlCount = SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in Database.QueryPaged(PageSize, sql)) + foreach (var row in Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount)) { - yield return CreateMediaNodeKit(row); + yield return CreateContentNodeKit(row, serializer); + } + } + + public IEnumerable GetBranchContentSources(int id) + { + var sql = SqlContentSourcesSelect(SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeIdX(SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + // Use a more efficient COUNT query + var sqlCountQuery = SqlContentSourcesCount(SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeIdX(SqlContext, id)); + var sqlCount = SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount)) + { + yield return CreateContentNodeKit(row, serializer); + } + } + + public IEnumerable GetTypeContentSources(IEnumerable ids) + { + if (!ids.Any()) + yield break; + + var sql = SqlContentSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .WhereIn(x => x.ContentTypeId, ids) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + // Use a more efficient COUNT query + var sqlCountQuery = SqlContentSourcesCount() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .WhereIn(x => x.ContentTypeId, ids); + var sqlCount = SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount)) + { + yield return CreateContentNodeKit(row, serializer); + } + } + + public ContentNodeKit GetMediaSource(IScope scope, int id) + { + var sql = SqlMediaSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeId(SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(scope.SqlContext)); + + var dto = scope.Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return ContentNodeKit.Empty; + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + return CreateMediaNodeKit(dto, serializer); + } + + public ContentNodeKit GetMediaSource(int id) + { + var sql = SqlMediaSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeId(SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return ContentNodeKit.Empty; + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + return CreateMediaNodeKit(dto, serializer); + } + + public IEnumerable GetAllMediaSources() + { + var sql = SqlMediaSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + // Use a more efficient COUNT query + var sqlCountQuery = SqlMediaSourcesCount() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)); + var sqlCount = SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount)) + { + yield return CreateMediaNodeKit(row, serializer); } } public IEnumerable GetBranchMediaSources(int id) { - var syntax = SqlSyntax; - var sql = MediaSourcesSelect( - s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + var sql = SqlMediaSourcesSelect(SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeIdX(SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + // Use a more efficient COUNT query + var sqlCountQuery = SqlMediaSourcesCount(SqlContentSourcesSelectUmbracoNodeJoin) + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeIdX(SqlContext, id)); + var sqlCount = SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in Database.QueryPaged(PageSize, sql)) + foreach (var row in Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount)) { - yield return CreateMediaNodeKit(row); + yield return CreateMediaNodeKit(row, serializer); } } public IEnumerable GetTypeMediaSources(IEnumerable ids) { if (!ids.Any()) - { yield break; - } - var sql = MediaSourcesSelect() - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) + var sql = SqlMediaSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) .WhereIn(x => x.ContentTypeId, ids) - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + // Use a more efficient COUNT query + var sqlCountQuery = SqlMediaSourcesCount() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .WhereIn(x => x.ContentTypeId, ids); + var sqlCount = SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - foreach (var row in Database.QueryPaged(PageSize, sql)) + foreach (var row in Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount)) { - yield return CreateMediaNodeKit(row); + yield return CreateMediaNodeKit(row, serializer); } } - private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) + private ContentNodeKit CreateContentNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) { ContentData d = null; ContentData p = null; if (dto.Edited) { - if (dto.EditData == null) + if (dto.EditData == null && dto.EditDataRaw == null) { if (Debugger.IsAttached) { @@ -615,7 +815,7 @@ AND cmsContentNu.nodeId IS NULL } else { - var nested = DeserializeNestedData(dto.EditData); + var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); d = new ContentData { @@ -625,16 +825,16 @@ AND cmsContentNu.nodeId IS NULL VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, WriterId = dto.EditWriterId, - Properties = nested.PropertyData, - CultureInfos = nested.CultureData, - UrlSegment = nested.UrlSegment + Properties = deserializedContent.PropertyData, // TODO: We don't want to allocate empty arrays + CultureInfos = deserializedContent.CultureData, + UrlSegment = deserializedContent.UrlSegment }; } } if (dto.Published) { - if (dto.PubData == null) + if (dto.PubData == null && dto.PubDataRaw == null) { if (Debugger.IsAttached) { @@ -645,24 +845,24 @@ AND cmsContentNu.nodeId IS NULL } else { - var nested = DeserializeNestedData(dto.PubData); + var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw); p = new ContentData { Name = dto.PubName, - UrlSegment = nested.UrlSegment, + UrlSegment = deserializedContent.UrlSegment, Published = true, TemplateId = dto.PubTemplateId, VersionId = dto.VersionId, VersionDate = dto.PubVersionDate, WriterId = dto.PubWriterId, - Properties = nested.PropertyData, - CultureInfos = nested.CultureData + Properties = deserializedContent.PropertyData, // TODO: We don't want to allocate empty arrays + CultureInfos = deserializedContent.CultureData }; } } - var n = new ContentNode(dto.Id, dto.Uid, + var n = new ContentNode(dto.Id, dto.Key, dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); var s = new ContentNodeKit @@ -676,12 +876,12 @@ AND cmsContentNu.nodeId IS NULL return s; } - private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) + private ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) { - if (dto.EditData == null) + if (dto.EditData == null && dto.EditDataRaw == null) throw new InvalidOperationException("No data for media " + dto.Id); - var nested = DeserializeNestedData(dto.EditData); + var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw); var p = new ContentData { @@ -691,11 +891,11 @@ AND cmsContentNu.nodeId IS NULL VersionId = dto.VersionId, VersionDate = dto.EditVersionDate, WriterId = dto.CreatorId, // what-else? - Properties = nested.PropertyData, - CultureInfos = nested.CultureData + Properties = deserializedMedia.PropertyData, // TODO: We don't want to allocate empty arrays + CultureInfos = deserializedMedia.CultureData }; - var n = new ContentNode(dto.Id, dto.Uid, + var n = new ContentNode(dto.Id, dto.Key, dto.Level, dto.Path, dto.SortOrder, dto.ParentId, dto.CreateDate, dto.CreatorId); var s = new ContentNodeKit @@ -707,19 +907,5 @@ AND cmsContentNu.nodeId IS NULL return s; } - - private static readonly JsonSerializerSettings NestedContentDataJsonSerializerSettings = new JsonSerializerSettings - { - Converters = new List { new ForceInt32Converter() } - }; - - private static ContentNestedData DeserializeNestedData(string data) - { - // by default JsonConvert will deserialize our numeric values as Int64 - // which is bad, because they were Int32 in the database - take care - - return JsonConvert.DeserializeObject(data, NestedContentDataJsonSerializerSettings - ); - } } } diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs index 32f7c83f9d..0c28b8a6e4 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs @@ -12,7 +12,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence { private readonly INuCacheContentRepository _repository; - public NuCacheContentService(INuCacheContentRepository repository, IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory) + public NuCacheContentService( + INuCacheContentRepository repository, + IScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory) : base(provider, loggerFactory, eventMessagesFactory) { _repository = repository; @@ -67,12 +71,15 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence => _repository.RefreshContent(content); /// - public void RefreshEntity(IContentBase content) - => _repository.RefreshEntity(content); + public void RefreshMedia(IMedia media) + => _repository.RefreshMedia(media); + + /// + public void RefreshMember(IMember member) + => _repository.RefreshMember(member); /// public void Rebuild( - int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null) @@ -84,7 +91,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.Persistence scope.ReadLock(Constants.Locks.MediaTree); scope.ReadLock(Constants.Locks.MemberTree); - _repository.Rebuild(groupSize, contentTypeIds, mediaTypeIds, memberTypeIds); + _repository.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); scope.Complete(); } } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs b/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs index bf34fca4e7..e6552b7df3 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs @@ -274,7 +274,18 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache throw new PanicException($"failed to get content with id={id}"); } - id = UnwrapIPublishedContent(content)._contentNode.NextSiblingContentId; + var next = UnwrapIPublishedContent(content)._contentNode.NextSiblingContentId; + +#if DEBUG + // I've seen this happen but I think that may have been due to corrupt DB data due to my own + // bugs, but I'm leaving this here just in case we encounter it again while we're debugging. + if (next == id) + { + throw new PanicException($"The current content id {id} is the same as it's next sibling id {next}"); + } +#endif + + id = next; } } } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index b236f8ccd0..f73b5b505e 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -46,6 +46,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache private readonly IPublishedModelFactory _publishedModelFactory; private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly IHostingEnvironment _hostingEnvironment; + private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory; + private readonly ContentDataSerializer _contentDataSerializer; private readonly NuCacheSettings _config; private bool _isReady; @@ -90,7 +92,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache IOptions globalSettings, IPublishedModelFactory publishedModelFactory, IHostingEnvironment hostingEnvironment, - IOptions config) + IOptions config, + IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, + ContentDataSerializer contentDataSerializer) { _options = options; _syncBootStateAccessor = syncBootStateAccessor; @@ -107,6 +111,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _defaultCultureAccessor = defaultCultureAccessor; _globalSettings = globalSettings.Value; _hostingEnvironment = hostingEnvironment; + _contentCacheDataSerializerFactory = contentCacheDataSerializerFactory; + _contentDataSerializer = contentDataSerializer; _config = config.Value; _publishedModelFactory = publishedModelFactory; } @@ -164,8 +170,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _localMediaDbExists = File.Exists(localMediaDbPath); // if both local databases exist then GetTree will open them, else new databases will be created - _localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists, _config); - _localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists, _config); + _localContentDb = BTree.GetTree(localContentDbPath, _localContentDbExists, _config, _contentDataSerializer); + _localMediaDb = BTree.GetTree(localMediaDbPath, _localMediaDbExists, _config, _contentDataSerializer); _logger.LogInformation("Registered with MainDom, localContentDbExists? {LocalContentDbExists}, localMediaDbExists? {LocalMediaDbExists}", _localContentDbExists, _localMediaDbExists); } @@ -345,10 +351,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache // contentStore is wlocked (1 thread) // content (and types) are read-locked - var contentTypes = _serviceContext.ContentTypeService.GetAll() - .Select(x => _publishedContentTypeFactory.CreateContentType(x)); + var contentTypes = _serviceContext.ContentTypeService.GetAll().ToList(); - _contentStore.SetAllContentTypesLocked(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes.Select(x => _publishedContentTypeFactory.CreateContentType(x))); using (_profilingLogger.TraceDuration("Loading content from database")) { @@ -1117,11 +1122,10 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// public void Rebuild( - int groupSize = 5000, IReadOnlyCollection contentTypeIds = null, IReadOnlyCollection mediaTypeIds = null, IReadOnlyCollection memberTypeIds = null) - => _publishedContentService.Rebuild(groupSize, contentTypeIds, mediaTypeIds, memberTypeIds); + => _publishedContentService.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); public async Task CollectAsync() { diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs index 898f22e989..81c9710ad6 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs @@ -83,9 +83,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache public void Handle(MemberDeletingNotification notification) => _publishedContentService.DeleteContentItems(notification.DeletedEntities); - public void Handle(MemberRefreshNotification notification) => _publishedContentService.RefreshEntity(notification.Entity); + public void Handle(MemberRefreshNotification notification) => _publishedContentService.RefreshMember(notification.Entity); - public void Handle(MediaRefreshNotification notification) => _publishedContentService.RefreshEntity(notification.Entity); + public void Handle(MediaRefreshNotification notification) => _publishedContentService.RefreshMedia(notification.Entity); public void Handle(ContentRefreshNotification notification) => _publishedContentService.RefreshContent(notification.Entity); diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj index cf3ad2018f..bedc6b7137 100644 --- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj +++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj @@ -1,51 +1,53 @@ - - netstandard2.0 - Umbraco.Cms.Infrastructure.PublishedCache - 8 - Umbraco.Cms.PublishedCache.NuCache - Umbraco CMS Published Cache - Contains the Published Cache assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco - + + netstandard2.0 + Umbraco.Cms.Infrastructure.PublishedCache + 8 + Umbraco.Cms.PublishedCache.NuCache + Umbraco CMS Published Cache + Contains the Published Cache assembly needed to run Umbraco Cms. This package only contains the assembly, and can be used for package development. Use the template in the Umbraco.Templates package to setup Umbraco + - - bin\Release\Umbraco.PublishedCache.NuCache.xml - + + bin\Release\Umbraco.PublishedCache.NuCache.xml + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + + - - - - + + + + - - - <_Parameter1>Umbraco.Tests - - - <_Parameter1>Umbraco.Tests.UnitTests - - - <_Parameter1>Umbraco.Tests.Integration - - - <_Parameter1>Umbraco.Tests.Benchmarks - - - <_Parameter1>DynamicProxyGenAssembly2 - - + + + <_Parameter1>Umbraco.Tests + + + <_Parameter1>Umbraco.Tests.UnitTests + + + <_Parameter1>Umbraco.Tests.Integration + + + <_Parameter1>Umbraco.Tests.Benchmarks + + + <_Parameter1>DynamicProxyGenAssembly2 + + diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 5f02db21b9..7f9be247b4 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -213,7 +213,8 @@ namespace Umbraco.TestData var docType = GetOrCreateContentType(); var parent = Services.ContentService.Create(company, -1, docType.Alias); - parent.SetValue("review", faker.Rant.Review()); + // give it some reasonable data (100 reviews) + parent.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review()))); parent.SetValue("desc", company); parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); Services.ContentService.Save(parent); @@ -223,7 +224,8 @@ namespace Umbraco.TestData return CreateHierarchy(parent, count, depth, currParent => { var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); - content.SetValue("review", faker.Rant.Review()); + // give it some reasonable data (100 reviews) + content.SetValue("review", string.Join(" ", Enumerable.Range(0, 100).Select(x => faker.Rant.Review()))); content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index dafc8c6af0..4dd1086348 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -33,4 +33,6 @@ - \ No newline at end of file + + + diff --git a/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs b/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs index b53e55a323..344a616e76 100644 --- a/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs +++ b/src/Umbraco.Tests.Integration/Testing/TestUmbracoDatabaseFactoryProvider.cs @@ -23,6 +23,7 @@ namespace Umbraco.Cms.Tests.Integration.Testing private readonly Lazy _mappers; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly NPocoMapperCollection _npocoMappers; public TestUmbracoDatabaseFactoryProvider( ILoggerFactory loggerFactory, @@ -30,7 +31,8 @@ namespace Umbraco.Cms.Tests.Integration.Testing IOptions connectionStrings, Lazy mappers, IDbProviderFactoryCreator dbProviderFactoryCreator, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory) + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + NPocoMapperCollection npocoMappers) { _loggerFactory = loggerFactory; _globalSettings = globalSettings; @@ -38,19 +40,18 @@ namespace Umbraco.Cms.Tests.Integration.Testing _mappers = mappers; _dbProviderFactoryCreator = dbProviderFactoryCreator; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; + _npocoMappers = npocoMappers; } public IUmbracoDatabaseFactory Create() - { - // ReSharper disable once ArrangeMethodOrOperatorBody - return new UmbracoDatabaseFactory( + => new UmbracoDatabaseFactory( _loggerFactory.CreateLogger(), _loggerFactory, _globalSettings, _connectionStrings, _mappers, _dbProviderFactoryCreator, - _databaseSchemaCreatorFactory); - } + _databaseSchemaCreatorFactory, + _npocoMappers); } } diff --git a/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs b/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs index dce144803f..db739d9369 100644 --- a/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs +++ b/src/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs @@ -41,7 +41,10 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers composition.Services.AddUnique(_ => SqlContext); IServiceProvider factory = composition.CreateServiceProvider(); - var pocoMappers = new NPoco.MapperCollection { new PocoMapper() }; + var pocoMappers = new NPoco.MapperCollection + { + new NullableDateMapper() + }; var pocoDataFactory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, pocoMappers).Init()); var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); SqlContext = new SqlContext(sqlSyntax, DatabaseType.SqlServer2012, pocoDataFactory, new Lazy(() => factory.GetRequiredService())); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index 14ed2ca6eb..11fee778e2 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -47,8 +47,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components ILogger logger = loggerFactory.CreateLogger("GenericLogger"); var globalSettings = new GlobalSettings(); var connectionStrings = new ConnectionStrings(); - var f = new UmbracoDatabaseFactory(loggerFactory.CreateLogger(), loggerFactory, Options.Create(globalSettings), Options.Create(connectionStrings), new Lazy(() => new MapperCollection(Enumerable.Empty())), TestHelper.DbProviderFactoryCreator, - new DatabaseSchemaCreatorFactory(loggerFactory.CreateLogger(), loggerFactory, new UmbracoVersion(), Mock.Of())); + var mapperCollection = new NPocoMapperCollection(new[] { new NullableDateMapper() }); + var f = new UmbracoDatabaseFactory( + loggerFactory.CreateLogger(), + loggerFactory, + Options.Create(globalSettings), + Options.Create(connectionStrings), + new Lazy(() => new MapperCollection(Enumerable.Empty())), + TestHelper.DbProviderFactoryCreator, + new DatabaseSchemaCreatorFactory(loggerFactory.CreateLogger(), loggerFactory, new UmbracoVersion(), Mock.Of()), + mapperCollection); + var fs = new FileSystems(loggerFactory, IOHelper, Options.Create(globalSettings), Mock.Of()); var coreDebug = new CoreDebugSettings(); MediaFileManager mediaFileManager = new MediaFileManager(Mock.Of(), diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs index cc152bed66..ac880c2b3b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs @@ -41,7 +41,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.NPocoTe [Test] public void SqlTemplateArgs() { - var mappers = new NPoco.MapperCollection { new PocoMapper() }; + var mappers = new NPoco.MapperCollection { new NullableDateMapper() }; var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); var sqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTests.cs index 93d5e6f908..103f9207d0 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTests.cs @@ -86,9 +86,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.NPocoTe Sql sql = Sql().SelectAll().From() .Where(x => x.Trashed == false); - Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE (NOT ([umbracoNode].[trashed] = @0))", sql.SQL.Replace("\n", " ")); + Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE ([umbracoNode].[trashed] = @0)", sql.SQL.Replace("\n", " ")); Assert.AreEqual(1, sql.Arguments.Length); - Assert.AreEqual(true, sql.Arguments[0]); + Assert.AreEqual(false, sql.Arguments[0]); } [Test] @@ -96,9 +96,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.NPocoTe { Sql sql = Sql().SelectAll().From().Where(x => x.Trashed == false); - Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE (NOT ([umbracoNode].[trashed] = @0))", sql.SQL.Replace("\n", " ")); + Assert.AreEqual("SELECT * FROM [umbracoNode] WHERE ([umbracoNode].[trashed] = @0)", sql.SQL.Replace("\n", " ")); Assert.AreEqual(1, sql.Arguments.Length); - Assert.AreEqual(true, sql.Arguments[0]); + Assert.AreEqual(false, sql.Arguments[0]); } [Test] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/QueryBuilderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/QueryBuilderTests.cs index 473a832100..7e224251f9 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/QueryBuilderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/Querying/QueryBuilderTests.cs @@ -115,7 +115,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.Queryin Assert.AreEqual("-1,1046,1076,1089%", result.Arguments[0]); Assert.AreEqual(1046, result.Arguments[1]); Assert.AreEqual(true, result.Arguments[2]); - Assert.AreEqual(true, result.Arguments[3]); + Assert.AreEqual(false, result.Arguments[3]); } } } diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 2781babfbe..4a20c1c1bf 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -1,58 +1,58 @@ - - + + - - - - - - - - - - - + + + + + + + + + + + - - - + + + - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + @@ -97,11 +97,15 @@ + + + + - - + + diff --git a/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs new file mode 100644 index 0000000000..b3543dad1a --- /dev/null +++ b/src/Umbraco.Tests/PublishedContent/ContentSerializationTests.cs @@ -0,0 +1,104 @@ +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PublishedCache.NuCache.DataSource; + +namespace Umbraco.Tests.PublishedContent +{ + [TestFixture] + public class ContentSerializationTests + { + [Test] + public void GivenACacheModel_WhenItsSerializedAndDeserializedWithAnySerializer_TheResultsAreTheSame() + { + var jsonSerializer = new JsonContentNestedDataSerializer(); + var msgPackSerializer = new MsgPackContentNestedDataSerializer(Mock.Of()); + + var now = DateTime.Now; + var cacheModel = new ContentCacheDataModel + { + PropertyData = new Dictionary + { + ["propertyOne"] = new[] + { + new PropertyData + { + Culture = "en-US", + Segment = "test", + Value = "hello world" + } + }, + ["propertyTwo"] = new[] + { + new PropertyData + { + Culture = "en-US", + Segment = "test", + Value = "Lorem ipsum" + } + } + }, + CultureData = new Dictionary + { + ["en-US"] = new CultureVariation + { + Date = now, + IsDraft = false, + Name = "Home", + UrlSegment = "home" + } + }, + UrlSegment = "home" + }; + + var content = Mock.Of(x => x.ContentTypeId == 1); + + var json = jsonSerializer.Serialize(content, cacheModel).StringData; + var msgPack = msgPackSerializer.Serialize(content, cacheModel).ByteData; + + Console.WriteLine(json); + Console.WriteLine(msgPackSerializer.ToJson(msgPack)); + + var jsonContent = jsonSerializer.Deserialize(content, json, null); + var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack); + + + CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys); + CollectionAssert.AreEqual(jsonContent.PropertyData.Keys, msgPackContent.PropertyData.Keys); + CollectionAssert.AreEqual(jsonContent.CultureData.Values, msgPackContent.CultureData.Values, new CultureVariationComparer()); + CollectionAssert.AreEqual(jsonContent.PropertyData.Values, msgPackContent.PropertyData.Values, new PropertyDataComparer()); + Assert.AreEqual(jsonContent.UrlSegment, msgPackContent.UrlSegment); + } + + public class CultureVariationComparer : Comparer + { + public override int Compare(CultureVariation x, CultureVariation y) + { + if (x == null && y == null) return 0; + if (x == null && y != null) return -1; + if (x != null && y == null) return 1; + + return x.Date.CompareTo(y.Date) | x.IsDraft.CompareTo(y.IsDraft) | x.Name.CompareTo(y.Name) | x.UrlSegment.CompareTo(y.UrlSegment); + } + } + + public class PropertyDataComparer : Comparer + { + public override int Compare(PropertyData x, PropertyData y) + { + if (x == null && y == null) return 0; + if (x == null && y != null) return -1; + if (x != null && y == null) return 1; + + var xVal = x.Value?.ToString() ?? string.Empty; + var yVal = y.Value?.ToString() ?? string.Empty; + + return x.Culture.CompareTo(y.Culture) | x.Segment.CompareTo(y.Segment) | xVal.CompareTo(yVal); + } + } + + } +} diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 2f74483cc6..aa06724d75 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -43,6 +43,7 @@ namespace Umbraco.Tests.PublishedContent private ContentType _contentTypeInvariant; private ContentType _contentTypeVariant; private TestDataSource _source; + private IContentCacheDataSerializerFactory _contentNestedDataSerializerFactory; [TearDown] public void Teardown() @@ -135,6 +136,7 @@ namespace Umbraco.Tests.PublishedContent // create a data source for NuCache _source = new TestDataSource(kits()); + _contentNestedDataSerializerFactory = new JsonContentNestedDataSerializerFactory(); var typeFinder = TestHelper.GetTypeFinder(); @@ -159,7 +161,9 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), PublishedModelFactory, hostingEnvironment, - Options.Create(nuCacheSettings)); + Options.Create(nuCacheSettings), + _contentNestedDataSerializerFactory); + // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index e2fe48f1cb..3f1bd09639 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -34,6 +34,7 @@ namespace Umbraco.Tests.PublishedContent { private IPublishedSnapshotService _snapshotService; private IVariationContextAccessor _variationAccesor; + private IContentCacheDataSerializerFactory _contentNestedDataSerializerFactory; private ContentType _contentType; private PropertyType _propertyType; @@ -108,6 +109,7 @@ namespace Umbraco.Tests.PublishedContent // create a data source for NuCache var dataSource = new TestDataSource(kit); + _contentNestedDataSerializerFactory = new JsonContentNestedDataSerializerFactory(); var runtime = Mock.Of(); Mock.Get(runtime).Setup(x => x.Level).Returns(RuntimeLevel.Run); @@ -200,7 +202,8 @@ namespace Umbraco.Tests.PublishedContent Mock.Of(), publishedModelFactory, TestHelper.GetHostingEnvironment(), - Microsoft.Extensions.Options.Options.Create(nuCacheSettings)); + Microsoft.Extensions.Options.Options.Create(nuCacheSettings), + _contentNestedDataSerializerFactory); // invariant is the current default _variationAccesor.VariationContext = new VariationContext(); diff --git a/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs b/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs new file mode 100644 index 0000000000..f83ea940c9 --- /dev/null +++ b/src/Umbraco.Tests/Serialization/AutoInterningStringConverterTests.cs @@ -0,0 +1,67 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; +using Umbraco.Core.Serialization; + +namespace Umbraco.Tests.Serialization +{ + [TestFixture] + public class AutoInterningStringConverterTests + { + [Test] + public void Intern_Property_String() + { + var str1 = "Hello"; + var obj = new Test + { + Name = str1 + Guid.NewGuid() + }; + + // ensure the raw value is not interned + Assert.IsNull(string.IsInterned(obj.Name)); + + var serialized = JsonConvert.SerializeObject(obj); + obj = JsonConvert.DeserializeObject(serialized); + + Assert.IsNotNull(string.IsInterned(obj.Name)); + } + + [Test] + public void Intern_Property_Dictionary() + { + var str1 = "key"; + var obj = new Test + { + Values = new Dictionary + { + [str1 + Guid.NewGuid()] = 0, + [str1 + Guid.NewGuid()] = 1 + } + }; + + // ensure the raw value is not interned + Assert.IsNull(string.IsInterned(obj.Values.Keys.First())); + Assert.IsNull(string.IsInterned(obj.Values.Keys.Last())); + + var serialized = JsonConvert.SerializeObject(obj); + obj = JsonConvert.DeserializeObject(serialized); + + Assert.IsNotNull(string.IsInterned(obj.Values.Keys.First())); + Assert.IsNotNull(string.IsInterned(obj.Values.Keys.Last())); + } + + public class Test + { + [JsonConverter(typeof(AutoInterningStringConverter))] + public string Name { get; set; } + + [JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))] + public Dictionary Values = new Dictionary(); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index a47825a815..5b1797ebe3 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -120,6 +120,8 @@ + + @@ -159,6 +161,13 @@ + + + + + + + @@ -166,6 +175,23 @@ + + + + + + + + + + + + + + + + + @@ -298,4 +324,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 02f6a04ef9..764ba49cdc 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -348,15 +348,17 @@ namespace Umbraco.Extensions var dllPath = Path.Combine(binFolder, "Umbraco.Persistence.SqlCe.dll"); var umbSqlCeAssembly = Assembly.LoadFrom(dllPath); - var sqlCeSyntaxProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider"); - var sqlCeBulkSqlInsertProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeBulkSqlInsertProvider"); - var sqlCeEmbeddedDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeEmbeddedDatabaseCreator"); + Type sqlCeSyntaxProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeSyntaxProvider"); + Type sqlCeBulkSqlInsertProviderType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeBulkSqlInsertProvider"); + Type sqlCeEmbeddedDatabaseCreatorType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeEmbeddedDatabaseCreator"); + Type sqlCeImageMapperType = umbSqlCeAssembly.GetType("Umbraco.Cms.Persistence.SqlCe.SqlCeImageMapper"); if (!(sqlCeSyntaxProviderType is null || sqlCeBulkSqlInsertProviderType is null || sqlCeEmbeddedDatabaseCreatorType is null)) { builder.Services.AddSingleton(typeof(ISqlSyntaxProvider), sqlCeSyntaxProviderType); builder.Services.AddSingleton(typeof(IBulkSqlInsertProvider), sqlCeBulkSqlInsertProviderType); builder.Services.AddSingleton(typeof(IEmbeddedDatabaseCreator), sqlCeEmbeddedDatabaseCreatorType); + builder.NPocoMappers().Add(sqlCeImageMapperType); } var sqlCeAssembly = Assembly.LoadFrom(Path.Combine(binFolder, "System.Data.SqlServerCe.dll")); diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs index 8b5942780f..82a1513658 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyImageCropperTemplateExtensions.cs @@ -1,11 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.DependencyInjection; -using Umbraco.Core.Models; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs index cbeb489beb..01c4ca1413 100644 --- a/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ImageCropperTemplateCoreExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Media; @@ -6,7 +6,6 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Routing; -using Umbraco.Core.Models; namespace Umbraco.Extensions { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index 1554c136b6..c6f4c79ea2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -12,7 +12,7 @@
i, // legacy icon + .umb-icon { font-size: 36px; - line-height: 28px; + line-height: 1; } } -.umb-card-grid .umb-card-grid-item { - position: relative; - display: block; - width: 100%; - height: 100%; - padding: 10px 5px; - border-radius: @baseBorderRadius * 2; - transition: background-color 120ms; - font-size: 13px; - line-height: 1.3em; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - - > span { +.umb-card-grid { + .umb-card-grid-item { position: relative; + display: block; + width: 100%; + height: 100%; + padding: 10px 5px; + border-radius: @baseBorderRadius * 2; + transition: background-color 120ms; + font-size: 13px; + line-height: 1.3em; display: flex; - align-items: center; - justify-content: center; flex-direction: column; - background-color: transparent; - word-break: break-word; + align-items: center; + justify-content: flex-start; + + &__loading { + position: absolute; + background-color: rgba(255,255,255,0.8); + top: 0; + right: 0; + bottom: 0; + left: 0; + } + + a { + color: @ui-option-type; + text-decoration: none; + } + + > span { + position: relative; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: transparent; + word-break: break-word; + } + + &:hover { + background-color: @ui-option-hover; + color: @ui-option-type-hover; + } + + &:focus { + color: @ui-option-type-hover; + } } -} - -.umb-card-grid .umb-card-grid-item:hover { - background-color: @ui-option-hover; - color: @ui-option-type-hover; -} -.umb-card-grid .umb-card-grid-item:focus { - color: @ui-option-type-hover; + span > i, // legacy icon + .umb-icon { + font-size: 30px; + line-height: 1; + margin-top: 6px; + margin-bottom: 10px; + display: block; + } } .umb-card-grid .umb-card-grid-item.--creator { @@ -189,7 +213,6 @@ width: 100%; padding-top: 100%; border-radius: @baseBorderRadius * 2; - box-sizing: border-box; transition: background-color 120ms; @@ -217,30 +240,6 @@ } } - -.umb-card-grid a { - color: @ui-option-type; - text-decoration: none; -} - -.umb-card-grid i { - font-size: 30px; - line-height: 20px; - margin-top: 6px; - margin-bottom: 10px; - display: block; -} - - .umb-card-grid .umb-card-grid-item__loading { - position: absolute; - background-color: rgba(255,255,255,0.8); - top: 0; - right: 0; - bottom: 0; - left: 0; - } - - //Round icon-like button - this should be somewhere else .umb-btn-round { padding: 4px 6px 4px 6px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index a96c59de84..9be50b877a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -14,13 +14,12 @@ margin-left: 30px; position: relative; } - - &.-small-text{ + &.-small-text { font-size: 13px; } - &.-bold{ + &.-bold { font-weight: 700; } @@ -38,12 +37,15 @@ &:hover ~ .umb-form-check__state .umb-form-check__check { border-color: @inputBorderFocus; } + &:checked ~ .umb-form-check__state .umb-form-check__check { border-color: @ui-option-type; } + &[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check { background-color: @ui-option-type; } + &:checked:hover ~ .umb-form-check__state .umb-form-check__check { &::before { background: @ui-option-type-hover; @@ -80,16 +82,22 @@ border: 2px solid @inputBorderTabFocus; margin: -1px; } + .tabbing-active &.umb-form-check--checkbox &__input:focus ~ .umb-form-check__state .umb-form-check__check { outline: 2px solid @inputBorderTabFocus; } + .tabbing-active &.umb-form-check--checkbox &__input:checked:focus ~ .umb-form-check__state .umb-form-check__check { border-color: @white; } - // add spacing between when flexed/inline, equal to the width of the input .flex & + & { - margin-left:@checkboxWidth; + margin-left: @checkboxWidth; + } + + .icon, + .umb-icon { + font-size: 1.2rem; } &__state { @@ -98,10 +106,8 @@ width: 20px; position: absolute; top: 0; - } - &__check { display: flex; position: relative; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 281284a5ca..033fe8fc0e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -404,12 +404,12 @@ box-sizing: border-box; } -.umb-grid .umb-editor-placeholder i { +.umb-grid .umb-editor-placeholder .icon { color: @gray-8; font-size: 85px; - line-height: 85px; + line-height: 1; display: block; - margin-bottom: 10px; + margin: 10px auto; } .umb-grid .umb-editor-preview { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index 87e46f5d85..a4a8388861 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -546,6 +546,7 @@ input.umb-group-builder__group-sort-value { .icon { font-size: 32px; + line-height: 1; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less index ba46c68a57..9eb00d4437 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less @@ -66,11 +66,8 @@ padding-left: 15px; } - .ui-sortable-handle { - min-height: 37px; - display: flex; - width:0; - align-items: center; + input[type="text"] { + margin-bottom: 0; } } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less index 6c1980a6e4..42a13c7dda 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less @@ -15,6 +15,11 @@ height: 20px; top: -6px; } +.umb-range-slider .noUi-connect { + background-color: @purple-washed; + border: 1px solid @purple-l3; +} + .umb-range-slider .noUi-tooltip { padding: 2px 6px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index 582da12804..9321577c15 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -222,63 +222,67 @@ /* ---------- LAYOUTS ---------- */ .list-view-layout { - display: flex; - align-items: center; - padding: 10px 15px; - background: @gray-10; - margin-bottom: 1px; -} + display: flex; + align-items: center; + padding: 10px 15px; + background: @gray-10; + margin-bottom: 1px; -.list-view-layout__sort-handle { - font-size: 14px; - color: @gray-8; - margin-right: 15px; -} + &__sort-handle { + font-size: 14px; + color: @gray-8; + margin-right: 15px; + } -.list-view-layout__name { - flex: 5; - font-weight: bold; - margin-right: 15px; - display: flex; - align-content: center; - flex-wrap: wrap; - line-height: 1.2em; -} + &__name { + flex: 5; + font-weight: bold; + margin-right: 15px; + display: flex; + align-content: center; + flex-wrap: wrap; + line-height: 1.2em; + } -.list-view-layout__name-text { - margin-right: 3px; -} - -.list-view-layout__system { - font-size: 10px; - font-weight: normal; -} + &__name-text { + margin-right: 3px; + } -.list-view-layout__path { - flex: 10; - margin-right: 15px; -} + &__system { + font-size: 10px; + font-weight: normal; + } -.list-view-layout__icon-wrapper { - margin-right: 10px; -} + &__path { + flex: 10; + margin-right: 15px; + } -.list-view-layout__icon { - font-size: 18px; - vertical-align: middle; - border: 1px solid @gray-8; - background: @white; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; -} + &__icon-wrapper { + margin-right: 10px; + } -.list-view-layout__remove { - position: relative; - cursor: pointer; + &__icon { + font-size: 18px; + vertical-align: middle; + border: 1px solid @gray-8; + background: @white; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + } + + &__remove { + position: relative; + cursor: pointer; + } + + input[type="text"] { + margin-bottom: 0; + } } .list-view-add-layout { diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 328ba2229b..11d11c7e3a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -950,6 +950,12 @@ .umb-linkpicker__url { width: 50%; padding-right: 5px; + + // when the anchor input is hidden by config + // the URL input should be full-width + &:only-child { + width: 100%; + } } .umb-linkpicker__anchor { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html index 3d8e1d4d0b..d19f537354 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html @@ -22,8 +22,7 @@ text="Type to search" on-change="vm.searchTermChanged()" css-class="w-100" - auto-focus="true" - > + auto-focus="true"> @@ -33,14 +32,13 @@

{{key | umbCmsTitleCase}}

  • -
  • @@ -62,14 +60,13 @@
    - @@ -84,14 +81,13 @@

    {{result.group | umbCmsTitleCase}}

    • -
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html index 760c5331b7..7155edf553 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html @@ -24,8 +24,7 @@ text="Type to filter..." css-class="w-100" on-change="vm.filterItems()" - auto-focus="true" - > + auto-focus="true"> @@ -36,14 +35,13 @@
      • - @@ -63,17 +61,16 @@
        -
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index 7074497a98..2a31fbd6c4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -1,19 +1,19 @@
- - - + +
    + + +
@@ -51,26 +53,27 @@ - +
- - +
    + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html index f038b8c4aa..61448f163e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html @@ -3,10 +3,10 @@
- +
- {{ parentName }} + {{parentName}} () @@ -19,13 +19,13 @@
- +
{{selectedChild.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html index 0276ae2a98..5f6fd2d485 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html @@ -12,8 +12,8 @@ ng-href="{{'#' + item.editPath}}" ng-click="clickItemName(item, $event, $index)" ng-class="{'-light': !item.published && item.updater != null}"> - - {{ item.name }} + + {{item.name}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-folder-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-folder-grid.html index 6796b7d64b..4c365a8335 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-folder-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-folder-grid.html @@ -7,12 +7,10 @@ ng-click="clickFolder(folder, $event, $index)">
    - -
    {{ folder.name }}
    + +
    {{folder.name}}
    - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html index ae3bbbf699..7f09177707 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html @@ -3,33 +3,42 @@
+
- +
{{ defaultItem.name }}
(Default {{itemLabel}}) -
- +
+ + +
+
- + +
{{ selectedItem.name }}
- + + +
- diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 9754056267..5a5116225d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -59,8 +59,7 @@
@@ -68,8 +67,7 @@
@@ -79,13 +77,12 @@
-
- - + + +
- {{item.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html index 3b6d82f73c..0c88dedee3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -11,11 +11,9 @@
- @@ -24,11 +22,10 @@ Status
- @@ -39,15 +36,16 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html b/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html index 8a915be57b..ca3df1d4eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html @@ -49,7 +49,7 @@ -
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/create.html b/src/Umbraco.Web.UI.Client/src/views/member/create.html index 5f4ad77f04..1762308a2c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/create.html @@ -6,8 +6,8 @@
  • - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js index 5e66684ac5..486bdab044 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js @@ -2,6 +2,8 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MacroController", function ($scope, $timeout, editorService, macroResource, macroService, localizationService, $routeParams) { + $scope.control.icon = $scope.control.icon || 'icon-settings-alt'; + localizationService.localize("grid_clickToInsertMacro").then(function(label) { $scope.title = label; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html index c07d29d89c..300ec91bcc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html @@ -2,9 +2,9 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 983644767d..21f6354c62 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -318,7 +318,6 @@ angular.module("umbraco") // Add items overlay menu // ********************************************* $scope.openEditorOverlay = function (event, area, index, key) { - const dialog = { view: "itempicker", filter: area.$allowedEditors.length > 15, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html index aa9f50b7df..cb6d9e5e26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html @@ -42,7 +42,7 @@ id="{{vm.model.alias}}" type="button" class="btn-reset umb-media-card-grid__create-button umb-outline" - disbled="!vm.allowAdd" + ng-disabled="!vm.allowAdd" ng-click="vm.addMediaAt(vm.model.value.length, $event)">
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index aaebb5d07e..22897e3ca2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -6,9 +6,9 @@
-
+
- +
@@ -18,7 +18,13 @@ ng-hide="vm.singleMode" umb-auto-focus="{{vm.focusOnNode && vm.currentNode.key === node.key ? 'true' : 'false'}}"> -
+
+ + + +
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 57c87807fc..57c1128151 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -150,7 +150,7 @@ diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml index 43acad00eb..0a9ba8acaa 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml @@ -31,6 +31,7 @@ Udgiv Afpublicér Genindlæs elementer + Fjern Genudgiv hele sitet Omdøb Gendan @@ -55,6 +56,7 @@ Sæt rettigheder Lås op Opret indholdsskabelon + Gensend Invitation Standard værdi @@ -141,18 +143,21 @@ Gem og send til udgivelse Gem listevisning Planlæg + Se side Forhåndsvisning Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt Vælg formattering Vis koder Indsæt tabel + Generer modeller og luk Gem og generer modeller Fortryd Genskab + Rul tilbage Slet tag Fortryd Bekræft - Flere publiseringsmuligheder + Flere publiseringsmuligheder Indsæt Indsæt og luk @@ -160,6 +165,7 @@ For Brugeren har slettet indholdet Brugeren har afpubliceret indholdet + Brugeren har afpubliceret indholdet for sprogene: %0% Brugeren har gemt og udgivet indholdet Brugeren har gemt og udgivet indholdet for sprogene: %0% Brugeren har gemt indholdet @@ -168,8 +174,10 @@ Brugeren har kopieret indholdet Brugeren har tilbagerullet indholdet til en tidligere tilstand Brugeren har sendt indholdet til udgivelse + Brugeren har sendt indholdet til udgivelse for sprogene: %0% Brugeren har sendt indholdet til oversættelse Brugeren har sorteret de underliggende sider + %0% Kopieret Udgivet Udgivet @@ -178,10 +186,13 @@ Gemt Slettet Afpubliceret + Afpubliceret Indhold tilbagerullet Sendt til udgivelse + Sendt til udgivelse Sendt til oversættelse Sorteret + Brugerdefineret Historik (alle sprog) @@ -241,6 +252,7 @@ Ingen dato valgt Sidetitel Dette medie har ikke noget link + Intet indhold kan tilføjes for dette element Egenskaber Dette dokument er udgivet, men ikke synligt da den overliggende side '%0%' ikke er udgivet! Dette sprog er udgivet, men ikke synligt, da den overliggende side '%0%' ikke er udgivet! @@ -263,6 +275,7 @@ Statistik Titel (valgfri) Alternativ tekst (valgfri) + Overskrift (valgfri) Type Hvilke varianter vil du udgive? Vælg hvilke varianter, der skal gemmes. @@ -272,6 +285,8 @@ Sidst redigeret Tidspunkt for seneste redigering Fjern fil + Klik her for at fjerne billedet fra medie filen + Klik her for at fjerne filen fra medie filen Link til dokument Medlem af grupper(ne) Ikke medlem af grupper(ne) @@ -283,6 +298,9 @@ Er du sikker på, at du vil slette alle elementer? Egenskaben %0% anvender editoren %1% som ikke er understøttet af Nested Content. Der er ikke konfigureret nogen indholdstyper for denne egenskab. + Tilføj element type + Vælg element type + Vælg gruppen, hvis værdier skal vises. Hvis dette er efterladt blankt vil den første gruppe på element typen bruges. %0% fra %1% Tilføj en ny tekstboks Fjern denne tekstboks @@ -301,6 +319,14 @@ Ikke-udgivne sprog Uændrede sprog Disse sprog er ikke blevet oprettet + Alle nye varianter vil blive gemt. + Hvilke varianter skal udgives? + Vælg, hvilke varianter skal gemmes. + Vælg varianter som skal sendes til gennemgang. + Sæt udgivnings tidspunkt... + Vælg varianterne som skal afpubliceres. Afpublicering af et krævet sprog vil afpublicere alle varianter. + De følgende varianter er krævet for at en udgivelse kan finde sted: + Vi er ikke klar til at udgive Klar til at udgive? Klar til at gemme? Send til godkendelse @@ -326,10 +352,12 @@ Maks filstørrelse er Medie rod Flytning af mediet fejlede + Overordnet og destinations mappe kan ikke være den samme Kopiering af mediet fejlede Oprettelse af mappen under parent med id %0% fejlede Omdøbning af mappen med id %0% fejlede Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteket. + Upload er ikke tiladt på denne lokation Opret et nyt medlem @@ -337,16 +365,16 @@ Medlemgrupper har ingen yderligere egenskaber til redigering. - Kopiering af indholdstypen fejlede - Flytning af indholdstypen fejlede + Kopiering af indholdstypen fejlede + Flytning af indholdstypen fejlede - Kopiering af medietypen fejlede - Flytning af medietypen fejlede - Auto vælg + Kopiering af medietypen fejlede + Flytning af medietypen fejlede + Auto vælg - Kopiering af medlemstypen fejlede + Kopiering af medlemstypen fejlede Hvor ønsker du at oprette den nye %0% @@ -359,6 +387,7 @@ Den valgte side i træet tillader ikke at sider oprettes under den. Rediger tilladelser for denne dokumenttype. Opret en ny dokumenttype + Dokumenttyper inde i Indstillinger sektionen, ved at ændre Tillad på rodniveau indestillingen under Permissions.]]> "media typer".]]> Det valgte medie i træet tillader ikke at medier oprettes under det. Rediger tilladelser for denne medietype. @@ -435,6 +464,9 @@ Luk denne dialog Er du sikker på at du vil slette Er du sikker på du vil deaktivere + Er du sikker på at du vil fjerne + %0%]]> + %0%]]> Er du sikker på at du vil forlade Umbraco? Er du sikker? Klip @@ -449,6 +481,7 @@ Indsæt makro Indsæt tabel Dette vil slette sproget + Ændring af kulturen for et sprog kan forsage en krævende opration og vil resultere i indholds cache og indeksering vil blive genlavet Sidst redigeret Link Internt link: @@ -513,6 +546,9 @@ Vælg konfiguration Vælg snippet Dette vil slette noden og alle dets sprog. Hvis du kun vil slette et sprog, så afpublicér det i stedet. + %0%]]> + %0% fra gruppen]]> + Ja, fjern Der er ingen ordbogselementer. @@ -574,6 +610,9 @@ #value eller ?key=value Indtast alias... Genererer alias... + Opret element + Rediger + Navn Opret brugerdefineret listevisning @@ -638,6 +677,7 @@ Denne egenskab er ugyldig + Valgmuligheder Om Handling Muligheder @@ -655,6 +695,7 @@ Ryd Luk Luk vindue + Luk vindue Kommentar Bekræft Proportioner @@ -663,6 +704,7 @@ Fortsæt Kopiér Opret + Beskær sektion Database Dato Standard @@ -789,6 +831,8 @@ Andet Artikler Videoer + installere + Avatar til Blå @@ -806,7 +850,7 @@ Vis genveje Brug listevisning Tillad på rodniveau - Lommentér/Udkommentér linjer + Kommentér/Udkommentér linjer Slet linje Kopiér linjer op Kopiér linjer ned @@ -1035,6 +1079,7 @@ Mange hilsner fra Umbraco robotten Bemærk: at dokumenter og medier som afhænger af denne pakke vil muligvis holde op med at virke, så vær forsigtig. Hvis i tvivl, kontakt personen som har udviklet pakken.]]> Pakke version + Opgraderer fra version Pakke allerede installeret Denne pakke kan ikke installeres, den kræver en minimum Umbraco version af Afinstallerer... @@ -1072,8 +1117,13 @@ Mange hilsner fra Umbraco robotten Hvis du ønsker at give adgang til enkelte medlemmer + Utilstrækkelige bruger adgang til a udgive alle under dokumenter Udgivelsen kunne ikke udgives da publiceringsdato er sat - + + Sortering udført Træk de forskellige sider op eller ned for at indstille hvordan de skal arrangeres, eller klik på kolonnehovederne for at sortere hele rækken af sider + Denne node har ingen under noder at sortere Validering Valideringsfejl skal rettes før elementet kan gemmes Fejlet Gemt + Gemt. For at se ændringerne skal du genindlæse din browser Utilstrækkelige brugerrettigheder, kunne ikke fuldføre handlingen Annulleret Handlingen blev annulleret af et 3. part tilføjelsesprogram @@ -1215,10 +1267,16 @@ Mange hilsner fra Umbraco robotten Udgivelse fejlede da overliggende side ikke er udgivet Indhold publiceret og nu synligt for besøgende + %0% dokumenter udgivet og synlige på hjemmesiden + %0% udgivet og synligt på hjemmesiden + %0% dokumenter udgivet for sprogene %1% og synlige på hjemmesiden Indhold gemt Husk at publicere for at gøre det synligt for besøgende + En planlægning for udgivelse er blevet opdateret + %0% gemt Send til Godkendelse Rettelser er blevet sendt til godkendelse + %0% rettelser er blevet sendt til godkendelse Medie gemt Medie gemt uden problemer Medlem gemt @@ -1245,6 +1303,8 @@ Mange hilsner fra Umbraco robotten Skabelon gemt Skabelon gemt uden fejl! Indhold fjernet fra udgivelse + Indhold variation %0% afpubliceret + Det krævet sprog '%0%' var afpubliceret. Alle sprog for dette indholds element er nu afpubliceret. Partial view gemt Partial view gemt uden fejl! Partial view ikke gemt @@ -1259,11 +1319,20 @@ Mange hilsner fra Umbraco robotten Brugergrupper er blevet indstillet Låste %0% brugere op %0% er nu låst op + Medlem blev exportet til fil + Der skete en fejl under exporteringen af medlemmet Brugeren %0% blev slettet Invitér bruger Invitationen blev gensendt til %0% + Kan ikke udgive dokumentet da det krævet '%0%' ikke er udgivet + Validering fejlede for sproget '%0%' Dokumenttypen blev eksporteret til en fil Der skete en fejl under eksport af en dokumenttype + Udgivelses datoen kan ikke ligge i fortiden + Kan ikke planlægge dokumentes udgivelse da det krævet '%0%' ikke er udgivet + Kan ikke planlægge dokumentes udgivelse da det krævet '%0%' har en senere udgivelses dato end et ikke krævet sprog + Afpubliceringsdatoen kan ikke ligge i fortiden + Afpubliceringsdatoen kan ikke være før udgivelsesdatoen Tilføj style @@ -1334,6 +1403,7 @@ Mange hilsner fra Umbraco robotten ]]> Query builder sider returneret, på + Kopier til udkilpsholder Returner alt indhold indhold af typen "%0%" @@ -1382,10 +1452,12 @@ Mange hilsner fra Umbraco robotten Grid layout Et layout er det overordnede arbejdsområde til dit grid - du vil typisk kun behøve ét eller to Tilføj grid layout + Rediger grid layout Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner Rækkekonfigurationer Rækker er foruddefinerede celler, der arrangeres vandret Tilføj rækkekonfiguration + Rediger rækkekonfiguration Juster rækken ved at indstille cellebredder og tilføje yderligere celler Ingen yderligere konfiguration tilgængelig Kolonner @@ -1400,6 +1472,9 @@ Mange hilsner fra Umbraco robotten Vælg ekstra Vælg standard er tilføjet + Advarsel + Du sletter en rækkekonfiguration + Sletning af et rækkekonfigurations navn vil resultere i et tab af data for alle eksiterende indhold som bruger dens konfiguration. Maksimalt emner Efterlad blank eller sæt til 0 for ubegrænset @@ -1467,6 +1542,7 @@ Mange hilsner fra Umbraco robotten Element-type Er en Element-type En Element-type er tiltænkt brug i f.eks. Nested Content, ikke i indholdstræet. + En Dokumenttype kan ikke ændres til en Element-type efter den er blevet brugt til at oprette en eller flere indholds elementer. Dette benyttes ikke for en Element-type Du har lavet ændringer til denne egenskab. Er du sikker på at du vil kassere dem? Visning @@ -1510,21 +1586,27 @@ Mange hilsner fra Umbraco robotten Casing Kodning Felt som skal indsættes - Konvertér linieskift - Erstatter et linieskift med html-tag'et &lt;br&gt; + Konvertér linjeskift + Ja, konverter linjeskift + Erstatter et linjeskift med html-tag'et &lt;br&gt; Custom felter Ja, kun dato Format og kodning Formatér som dato + Formater værdien som en dato eller en dato med tid, i forhold til den aktive kultur HTML indkod Vil erstatte specielle karakterer med deres HTML jævnbyrdige. Denne tekst vil blive sat ind lige efter værdien af feltet Denne tekst vil blive sat ind lige før værdien af feltet Lowercase + Ændre udskrift Ingen + Udskrift eksempel Indsæt efter felt Indsæt før felt Rekursivt + Ja, lav det rekursivt + Separator Fjern paragraf-tags Fjerner eventuelle &lt;P&gt; omkring teksten Standard felter @@ -1610,6 +1692,8 @@ Mange hilsner fra Umbraco robotten Skift dit kodeord Skift billede Nyt kodeord + Minium %0% karakterer tilbage! + Der skal som minium være %0% specielle karakterer. er ikke blevet låst ude Kodeordet er ikke blevet ændret Gentag dit nye kodeord @@ -1644,8 +1728,10 @@ Mange hilsner fra Umbraco robotten Adgangskode Nulstil kodeord Dit kodeord er blevet ændret! + Kodeord ændret Bekræft venligst dit nye kodeord Indtast dit nye kodeord + Dit nye kodeord kan ikke være blankt! Nuværende kodeord ugyldig nuværende kodeord Dit nye kodeord må ikke være tomt! @@ -1665,6 +1751,7 @@ Mange hilsner fra Umbraco robotten Vælg brugergrupper Ingen startnode valgt Ingen startnoder valgt + Indhold startnode Begræns indholdstræet til en bestemt startnode Indhold startnoder Begræns indholdstræet til bestemte startnoder @@ -1678,7 +1765,7 @@ Mange hilsner fra Umbraco robotten er blevet inviteret En invitation er blevet sendt til den nye bruger med oplysninger om, hvordan man logger ind i Umbraco. Hej og velkommen til Umbraco! På bare 1 minut vil du være klar til at komme i gang, vi skal bare have dig til at oprette en adgangskode og tilføje et billede til din avatar. - Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen. + Velkommen til Umbraco! Desværre er din invitation udløbet. Kontakt din administrator og bed om at gensende invitationen. Hvis du uploader et billede af dig selv, gør du det nemt for andre brugere at genkende dig. Klik på cirklen ovenfor for at uploade et billede. Forfatter Skift @@ -1691,6 +1778,10 @@ Mange hilsner fra Umbraco robotten Tilbage til brugere Umbraco: Invitation

Hej %0%, du er blevet inviteret af %1% til Umbraco backoffice.

Besked fra %1%: %2%

Klik på dette link for acceptere invitationen

Hvis du ikke kan klikke på linket, så kopier og indsæt denne URL i dit browservindue

%3%

]]> + Inviter + Gensender invitation... + Slet bruger + Er du sikker på du ønsker at slette denne brugers konto? Alle Aktiv Deaktiveret @@ -1702,6 +1793,7 @@ Mange hilsner fra Umbraco robotten Nyeste Ældste Sidst logget ind + Ingen brugere er blevet tilføjet Validering @@ -1710,7 +1802,9 @@ Mange hilsner fra Umbraco robotten Valider som URL ...eller indtast din egen validering Feltet er påkrævet + Indtast en selvvalgt validerings fejlbesked (valgfrit) Indtast et regulært udtryk + Indtast en selvvalgt validerings fejlbesked (valgfrit) Du skal tilføje mindst Du kan kun have Tilføj op til @@ -1725,14 +1819,18 @@ Mange hilsner fra Umbraco robotten Værdien kan ikke være tom Værdien kan ikke være tom Værdien er ugyldig, som ikke matcher det korrekte format + Selvvalgt validering %1% mere.]]> %1% for mange.]]> Slå URL tracker fra Slå URL tracker til + Kultur Original URL Viderestillet til + Viderestil URL håndtering + De følgende URLs viderestiller til dette indholds element Der er ikke lavet nogen viderestillinger Når en udgivet side bliver omdøbt eller flyttet, vil en viderestilling automatisk blive lavet til den nye side. Er du sikker på at du vil fjerne viderestillingen fra '%0%' til '%1%'? @@ -1807,12 +1905,34 @@ Mange hilsner fra Umbraco robotten Opsæt rettigheder på %0% Juster soterings rækkefølgen for %0% Opret indholds skabelon baseret på %0% + Åben kontext menu for Aktivt sprog Skift sprog til Opret ny mappe + Delvist View + Delvist View Macro + Medlem + Data type + Søg i viderestillings dashboardet + Søg i brugergruppe sektionen + Søg i bruger sektionen Opret element + Opret Rediger Navn + Tilføj ny række + Vis flere muligheder + Søg i Umbraco backoffice + Søg efter indholdsnoder, medienoder osv. i backoffice + Når autoudfyldnings resultaterne er klar, tryk op og ned pilene, eller benyt tab knappen og brug enter knappen til at vælge. + Vej + Fundet i + Har oversættelse + Mangler oversættelse + Ordbogs elementer + Udfør handling %0% på %1% noden + Tilføj billede overskrift + Søg i indholdstræet Referencer @@ -1824,12 +1944,17 @@ Mange hilsner fra Umbraco robotten Brugt i Medlems Typer Ingen referencer til Medlems Typer. Brugt af + Brugt i Dokumenter + Brugt i Medlemmer + Brugt i Medier Slet gemte søgning Log type + Gemte søgninger Gem søgning Indtast et navn for din søgebetingelse + Filter søgning Samlet resultat Dato Type @@ -1854,6 +1979,17 @@ Mange hilsner fra Umbraco robotten Find logs med Namespace Find logs med maskin navn Åben + Henter + Hver 2 sekunder + Hver 5 sekunder + Hver 10 sekunder + Hver 20 sekunder + Hver 30 sekunder + Henter hver 2s + Henter hver 5s + Henter hver 10s + Henter hver 20s + Henter hver 30s Kopier %0% @@ -1864,6 +2000,7 @@ Mange hilsner fra Umbraco robotten Åben egenskabshandlinger + Luk egenskabshandlinger Vælg elementtype @@ -1910,11 +2047,11 @@ Mange hilsner fra Umbraco robotten Feltet %0% bruger editor %1% som ikke er supporteret for blokke. - Hvad er Indholdsskabeloner? - Indholdsskabeloner er foruddefineret indhold der kan vælges når der oprettes nye indholdselementer. - Hvordan opretter jeg en Indholdsskabelon? - - Hvad er Indholdsskabeloner? + Indholdsskabeloner er foruddefineret indhold der kan vælges når der oprettes nye indholdselementer. + Hvordan opretter jeg en Indholdsskabelon? + + Der er to måder at oprette Indholdsskabeloner på:

  • Højreklik på en indholdsnode og vælg "Opret indholdsskabelon" for at oprette en ny Indholdsskabelon.
  • @@ -1923,9 +2060,9 @@ Mange hilsner fra Umbraco robotten

    Når indholdsskabelonen har fået et navn, kan redaktører begynde at bruge indholdsskabelonen som udgangspunkt for deres nye side.

    ]]> - Hvordan vedligeholder jeg Indholdsskabeloner? - Du kan redigere og slette Indholdsskabeloner fra "Indholdsskabeloner" i sektionen Indstillinger. Fold dokumenttypen som Indholdsskabelonen er baseret på ud og klik på den for at redigere eller slette den. - + Hvordan vedligeholder jeg Indholdsskabeloner? + Du kan redigere og slette Indholdsskabeloner fra "Indholdsskabeloner" i sektionen Indstillinger. Fold dokumenttypen som Indholdsskabelonen er baseret på ud og klik på den for at redigere eller slette den. + Afslut Afslut forhåndsvisning diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml index 2f89060ac6..63b7a2fff8 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/it.xml @@ -1,401 +1,437 @@ - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Gestisci hostnames - Audit Trail - Sfoglia - Copia - Crea - Crea pacchetto - Cancella - Disabilita - Svuota il cestino - Esporta il tipo di documento - Importa il tipo di documento - Importa il pacchetto - Modifica in Area di Lavoro - Uscita - Sposta - Notifiche - Accesso pubblico - Pubblica - Aggiorna nodi - Ripubblica intero sito - Permessi - Annulla ultima modifica - Invia per la pubblicazione - Invia per la traduzione - Ordina - Invia la pubblicazione - Traduci - Annulla pubblicazione - Aggiorna - - - Aggiungi nuovo dominio - Dominio - - - - - Hostname non valido - Modifica il dominio corrente - - - Visualizzazione per - - - Grassetto - Cancella rientro paragrafo - Inserisci dal file - Inserisci intestazione grafica - Modifica Html - Inserisci rientro paragrafo - Corsivo - Centra - Allinea testo a sinistra - Allinea testo a destra - Inserisci Link - Inserisci local link (ancora) - Elenco puntato - Elenco numerato - Inserisci macro - Inserisci immagine - Modifica relazioni - Salva - Salva e pubblica - Salva e invia per approvazione - Anteprima - - Scegli lo stile - Mostra gli stili - Inserisci tabella - - - Informazioni su questa pagina - Link alternativo - - Links alternativi - Clicca per modificare questo elemento - Creato da - Creato il - Tipo di documento - Modifica - Attivo fino al - - - Ultima pubblicazione - Link ai media - Tipo di media - Gruppo di membri - Ruolo - Tipologia Membro - - Titolo della Pagina - - - Pubblicato - Stato della pubblicazione - Pubblicato il - Rimuovi data - Ordinamento dei nodi aggiornato - - Statistiche - Titolo (opzionale) - Tipo - Non pubblicare - Ultima modifica - Rimuovi il file - Link al documento - - - - Crea al - Scegli il tipo ed il titolo - - - - - - hai aperto una nuova finestra - Riavvia - Visita - Benvenuto - - - Rimani - Scarta le modifiche - Hai delle modifiche non salvate - Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate - - - Fatto - Elimianto %0% elemento - Elimianto %0% elementi - Eliminato %0% su %1% elemento - Eliminato %0% su %1% elementi - Pubblicato %0% elemento - Pubblicato %0% elementi - Pubblicato %0% su %1% elemento - Pubblicato %0% su %1% elementi - %0% elemento non pubblicato - %0% elementi non pubblicati - Elementi non pubblicati - %0% su %1% - Elementi non pubblicati - %0% su %1% - Spostato %0% elemento - Spsotato %0% elementi - Spostato %0% su %1% elemento - Spostato %0% su %1% elementi - Copiato %0% elemento - Copiato %0% elementi - Copiato %0% su %1% elemento - Copiato %0% su %1% elementi - - - Titolo del Link - Link - Nome - Gestione alias Hostnames - Chiudi questa finestra - - - - - Taglia - Modifica elemento Dictionary - Modifica il linguaggio - Inserisci il link locale - Inserisci carattere - - - Inserisci link - Inserisci macro - Inserisci tabella - Ultima modifica - Link - - - - - Incolla - Modifica il Permesso per - - - - regexlib.com ha attualmente qualche problema, di cui non abbiamo il controllo. Siamo spiacevoli dell'inconveniente.]]> - - Elimina Macro - Campo obbligatorio - - - - Numero di colonne - Numero di righe - - Seleziona elemento - Visualizza gli elementi in cache - - - - - - - - - - - - Rendering controllo - Bottoni - Abilita impostazioni avanzate per - Abilita menu contestuale - Dimensione massima delle immagini inserite - Fogli di stile collegati - Visualizza etichetta - Larghezza e altezza - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Info - Azione - Aggiungi - Alias - - Bordo - o - Annulla - - Scegli - Chiudi - Chiudi la finestra - Commento - Conferma - Blocca le proporzioni - Continua - Copia - Crea - Base di dati - Data - Default - Elimina - Eliminato - Elimina... - Design - Dimensioni - - Scarica - Modifica - Modificato - Elementi - Email - Errore - Trova - Cartella - Altezza - Guida - Icona - Importa - - Inserisci - Installa - Giustificato - Lingua - Layout - Caricamento - Bloccato - Login - Log off - Logout - Macro - Sposta - Nome - Nuovo - Successivo - No - di - Ok - Apri - o - Password - Percorso - - Precedente - - - Cestino - Rimangono - Rinomina - Rinnova - Riprova - Permessi - Cerca - Server - Mostra - Mostra la pagina inviata - Dimensione - Ordina - Invia - Tipo - - Su - Aggiorna - Aggiornamento - Carica - URL - Utente - - Valore - Vedi - Benvenuto... - Larghezza - Si - Riordina - Ho finito di ordinare - - - Colore di sfondo - Grassetto - Colore del testo - Carattere - Testo - - - Pagina - - - - - - - installa per installare il database Umbraco %0% ]]> - Avanti per proseguire.]]> - Database non trovato! Perfavore, controlla che le informazioni della stringa di connessione nel file "web.config" siano corrette.

    -

    Per procedere, edita il file "web.config" (utilizzando Visual Studio o l'editor di testo che preferisci), scorri in basso, aggiungi la stringa di connessione per il database chiamato "umbracoDbDSN" e salva il file.

    Clicca il tasto riprova quando hai finito.
    Maggiori dettagli per la modifica del file web.config qui.

    ]]>
    - - Premi il tasto aggiorna per aggiornare il database ad Umbraco %0%

    Non preoccuparti, il contenuto non verrà perso e tutto continuerà a funzionare dopo l'aggiornamento!

    ]]>
    - Premi il tasto Avanti per continuare.]]> - Avanti per continuare la configurazione.]]> - La password predefinita per l'utente di default deve essere cambiata!]]> - L'utente di default è stato disabilitato o non ha accesso ad Umbraco!

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> - La password è stata modificata con successo

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> - - - - - - - - Le impostazioni dei permessi sono perfette!

    Puoi eseguire Umbraco senza problemi, ma potresti non poter installare i pacchetti che sono consigliati per sfruttare tutti i vantaggi offerti da Umbraco.]]>
    - - - video tutorial su come impostare i permessi delle cartelle per Umbraco o leggi la versione testuale.]]> - Le impostazioni dei permessi potrebbero avere dei problemi!

    Puoi eseguire Umbraco, ma potresti non essere in grado di creare cartelle o installare pacchetti che sono raccomandati per sfruttare tutti i vantaggi di Umbraco.]]>
    - Le impostazioni dei permessi non sono corrette per Umbraco!

    Per eseguire Umbraco, devi aggiornare le impostazioni dei permessi.]]>
    - La configurazione dei permessi è perfetta!

    Sei pronto per avviare Umbraco e installare i pacchetti!]]>
    - - - - - - Guarda come) Puoi anche installare eventuali Runway in un secondo momento. Vai nella sezione Developer e scegli Pacchetti.]]> - - Runway è installato - + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Gestisci hostnames + Audit Trail + Sfoglia + Copia + Crea + Crea pacchetto + Cancella + Disabilita + Svuota il cestino + Esporta il tipo di documento + Importa il tipo di documento + Importa il pacchetto + Modifica in Area di Lavoro + Uscita + Sposta + Notifiche + Accesso pubblico + Pubblica + Aggiorna nodi + Ripubblica intero sito + Permessi + Annulla ultima modifica + Invia per la pubblicazione + Invia per la traduzione + Ordina + Invia la pubblicazione + Traduci + Annulla pubblicazione + Aggiorna + Rimuovi + Ripristina + Crea Content Template + Crea gruppo + + + Aggiungi nuovo dominio + Dominio + + + + + Hostname non valido + Modifica il dominio corrente + + + Visualizzazione per + Contenuto pubblicato + Contenuto salvato + + + Grassetto + Cancella rientro paragrafo + Inserisci dal file + Inserisci intestazione grafica + Modifica Html + Inserisci rientro paragrafo + Corsivo + Centra + Allinea testo a sinistra + Allinea testo a destra + Inserisci Link + Inserisci local link (ancora) + Elenco puntato + Elenco numerato + Inserisci macro + Inserisci immagine + Modifica relazioni + Salva + Salva e pubblica + Salva e invia per approvazione + Anteprima + + Scegli lo stile + Mostra gli stili + Inserisci tabella + Altre azioni + Pubblica con i discendenti + Pianifica + Seleziona + Annulla selezione + + + Informazioni su questa pagina + Link alternativo + + Links alternativi + Clicca per modificare questo elemento + Creato da + Creato il + Tipo di documento + Modifica + Attivo fino al + + + Ultima pubblicazione + Link ai media + Tipo di media + Gruppo di membri + Ruolo + Tipologia Membro + + Titolo della Pagina + + + Pubblicato + Stato della pubblicazione + Pubblicato il + Rimuovi data + Ordinamento dei nodi aggiornato + + Statistiche + Titolo (opzionale) + Tipo + Non pubblicare + Ultima modifica + Rimuovi il file + Link al documento + Elementi + Pubblicato + Seleziona da data e l'ora in cui pubblicare/depubblicare il contenuto. + Imposta data + Depubblicato il + + + + Crea un elemento sotto + Scegli il tipo ed il titolo + Cartella + + + + + + hai aperto una nuova finestra + Riavvia + Visita + Benvenuto + + + Rimani + Scarta le modifiche + Hai delle modifiche non salvate + Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate + + + Fatto + Elimianto %0% elemento + Elimianto %0% elementi + Eliminato %0% su %1% elemento + Eliminato %0% su %1% elementi + Pubblicato %0% elemento + Pubblicato %0% elementi + Pubblicato %0% su %1% elemento + Pubblicato %0% su %1% elementi + %0% elemento non pubblicato + %0% elementi non pubblicati + Elementi non pubblicati - %0% su %1% + Elementi non pubblicati - %0% su %1% + Spostato %0% elemento + Spsotato %0% elementi + Spostato %0% su %1% elemento + Spostato %0% su %1% elementi + Copiato %0% elemento + Copiato %0% elementi + Copiato %0% su %1% elemento + Copiato %0% su %1% elementi + + + Titolo del Link + Link + Nome + Gestione alias Hostnames + Chiudi questa finestra + + + + + Taglia + Modifica elemento Dictionary + Modifica il linguaggio + Inserisci il link locale + Inserisci carattere + + + Inserisci link + Inserisci macro + Inserisci tabella + Ultima modifica + Link + + + + + Incolla + Modifica il Permesso per + + + + regexlib.com ha attualmente qualche problema, di cui non abbiamo il controllo. Siamo spiacevoli dell'inconveniente.]]> + + Elimina Macro + Campo obbligatorio + + + + Numero di colonne + Numero di righe + + Seleziona elemento + Visualizza gli elementi in cache + Seleziona contenuto + + + + + + + + + + + + Rendering controllo + Bottoni + Abilita impostazioni avanzate per + Abilita menu contestuale + Dimensione massima delle immagini inserite + Fogli di stile collegati + Visualizza etichetta + Larghezza e altezza + + + + + + + + + + + + + + + + + + + + + + + + + + + Questa proprietà non è valida + + + Info + Azione + Aggiungi + Alias + + Bordo + o + Annulla + + Scegli + Chiudi + Chiudi la finestra + Commento + Conferma + Blocca le proporzioni + Continua + Copia + Crea + Base di dati + Data + Default + Elimina + Eliminato + Elimina... + Design + Dimensioni + + Scarica + Modifica + Modificato + Elementi + Email + Errore + Trova + Cartella + Altezza + Guida + Icona + Importa + + Inserisci + Installa + Giustificato + Lingua + Layout + Caricamento + Bloccato + Login + Log off + Logout + Macro + Sposta + Nome + Nuovo + Successivo + No + di + Ok + Apri + o + Password + Percorso + + Precedente + + + Cestino + Rimangono + Rinomina + Rinnova + Riprova + Permessi + Cerca + Server + Mostra + Mostra la pagina inviata + Dimensione + Ordina + Conferma + Tipo + Digita per cercare... + Su + Aggiorna + Aggiornamento + Carica + URL + Utente + + Valore + Vedi + Benvenuto... + Larghezza + Si + Riordina + Ho finito di ordinare + Richiesto + Contenuti + Azioni + Cerca solo in questa cartella + Pianifica pubblicazione + selezionato + Annulla + Cambia password + Cronologia + Generale + Rimuovi + Gruppi + + + Colore di sfondo + Grassetto + Colore del testo + Carattere + Testo + + + Pagina + + + + + + + installa per installare il database Umbraco %0% ]]> + Avanti per proseguire.]]> + + Database non trovato! Perfavore, controlla che le informazioni della stringa di connessione nel file "web.config" siano corrette.

    +

    Per procedere, edita il file "web.config" (utilizzando Visual Studio o l'editor di testo che preferisci), scorri in basso, aggiungi la stringa di connessione per il database chiamato "umbracoDbDSN" e salva il file.

    Clicca il tasto riprova quando hai finito.
    Maggiori dettagli per la modifica del file web.config qui.

    ]]> +
    + + Premi il tasto aggiorna per aggiornare il database ad Umbraco %0%

    Non preoccuparti, il contenuto non verrà perso e tutto continuerà a funzionare dopo l'aggiornamento!

    ]]>
    + Premi il tasto Avanti per continuare.]]> + Avanti per continuare la configurazione.]]> + La password predefinita per l'utente di default deve essere cambiata!]]> + L'utente di default è stato disabilitato o non ha accesso ad Umbraco!

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + La password è stata modificata con successo

    Non è necessario eseguire altre operazioni. Clicca il tasto Avanti per continuare.]]> + + + + + + + + Le impostazioni dei permessi sono perfette!

    Puoi eseguire Umbraco senza problemi, ma potresti non poter installare i pacchetti che sono consigliati per sfruttare tutti i vantaggi offerti da Umbraco.]]>
    + + + video tutorial su come impostare i permessi delle cartelle per Umbraco o leggi la versione testuale.]]> + Le impostazioni dei permessi potrebbero avere dei problemi!

    Puoi eseguire Umbraco, ma potresti non essere in grado di creare cartelle o installare pacchetti che sono raccomandati per sfruttare tutti i vantaggi di Umbraco.]]>
    + Le impostazioni dei permessi non sono corrette per Umbraco!

    Per eseguire Umbraco, devi aggiornare le impostazioni dei permessi.]]>
    + La configurazione dei permessi è perfetta!

    Sei pronto per avviare Umbraco e installare i pacchetti!]]>
    + + + + + + Guarda come) Puoi anche installare eventuali Runway in un secondo momento. Vai nella sezione Developer e scegli Pacchetti.]]> + + Runway è installato + + Questa è la lista dei nostri moduli raccomandati, seleziona quali vorresti installare, o vedi l'intera lista di moduli - ]]> - Raccommandato solo per utenti esperti - Vorrei iniziare da un sito semplice - + + Raccommandato solo per utenti esperti + Vorrei iniziare da un sito semplice + + "Runway" è un semplice sito web contenente alcuni tipi di documento e alcuni templates di base. L'installer configurerà Runway per te automaticamente, ma tu potrai facilmente modificarlo, estenderlo o eliminarlo. Non è necessario installarlo e potrai usare Umbraco anche senza di esso, ma @@ -406,63 +442,85 @@ Inclusi in Runway: Home page, pagina Guida introduttiva, pagina Installazione moduli
    Moduli opzionali: Top Navigation, Sitemap, Contatti, Gallery.
    - ]]> - Cosa è Runway - Passo 1/5 Accettazione licenza - Passo 2/5: Configurazione database - Passo 3/5: Controllo permessi dei file - Passo 4/5: Controllo impostazioni sicurezza - Passo 5/5: Umbraco è pronto per iniziare - Grazie per aver scelto Umbraco - Naviga per il tuo nuovo sito -Hai installato Runway, quindi perché non dare uno sguardo al vostro nuovo sito web.]]> - Ulteriori informazioni e assistenza -Fatti aiutare dalla nostra community, consulta la documentazione o guarda alcuni video gratuiti su come costruire un semplice sito web, come usare i pacchetti e una guida rapida alla terminologia Umbraco]]> - - /web.config e aggiornare la chiave AppSetting UmbracoConfigurationStatus impostando il valore '%0%'.]]> - iniziare immediatamente cliccando sul bottone "Avvia Umbraco".
    Se sei nuovo a Umbraco, si possono trovare un sacco di risorse sulle nostre pagine Getting Started.]]>
    - Avvia Umbraco -Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e iniziare ad aggiungere i contenuti, aggiornando i modelli e i fogli di stile o aggiungere nuove funzionalità]]> - Connessione al database non riuscita. - Umbraco Versione 3 - Umbraco Versione 4 - Guarda - Umbraco %0% per una nuova installazione o per l'aggiornamento dalla versione 3.0. + ]]> + + Cosa è Runway + Passo 1/5 Accettazione licenza + Passo 2/5: Configurazione database + Passo 3/5: Controllo permessi dei file + Passo 4/5: Controllo impostazioni sicurezza + Passo 5/5: Umbraco è pronto per iniziare + Grazie per aver scelto Umbraco + + Naviga per il tuo nuovo sito +Hai installato Runway, quindi perché non dare uno sguardo al vostro nuovo sito web.]]> + + + Ulteriori informazioni e assistenza +Fatti aiutare dalla nostra community, consulta la documentazione o guarda alcuni video gratuiti su come costruire un semplice sito web, come usare i pacchetti e una guida rapida alla terminologia Umbraco]]> + + + /web.config e aggiornare la chiave AppSetting UmbracoConfigurationStatus impostando il valore '%0%'.]]> + iniziare immediatamente cliccando sul bottone "Avvia Umbraco".
    Se sei nuovo a Umbraco, si possono trovare un sacco di risorse sulle nostre pagine Getting Started.]]>
    + + Avvia Umbraco +Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e iniziare ad aggiungere i contenuti, aggiornando i modelli e i fogli di stile o aggiungere nuove funzionalità]]> + + Connessione al database non riuscita. + Umbraco Versione 3 + Umbraco Versione 4 + Guarda + + Umbraco %0% per una nuova installazione o per l'aggiornamento dalla versione 3.0.

    - Clicca "avanti" per avviare la procedura.]]>
    - - - Codice cultura - Nome cultura - - - - Riconnetti adesso per salvare il tuo lavoro - - - © 2001 - %0%
    umbraco.com

    ]]> - - - Dashboard - Sezioni - Contenuto - - - Scegli la pagina sopra... - - Seleziona dove il documento %0% deve essere copiato - - Seleziona dove il documento %0% deve essere spostato - - - - - - - - - - "avanti" per avviare la procedura.]]> + + + + Codice cultura + Nome cultura + + + + Riconnetti adesso per salvare il tuo lavoro + + + © 2001 - %0%
    umbraco.com

    ]]>
    + Buona domenica + Buon lunedì + Buon martedì + Buon mercoledì + Buon giovedì + Buon venerdì + Buon sabato + Mostra password + Nascondi password + Password dimenticata? + Una email verrà inviata all'indirizzo specificato con un link per il reset della password + Ritorna alla finestra di login + + + Dashboard + Sezioni + Contenuto + + + Scegli la pagina sopra... + + Seleziona dove il documento %0% deve essere copiato + + Seleziona dove il documento %0% deve essere spostato + + + + + + + + + + + - Salve %0%

    + ]]> +
    + + Salve %0%

    Questa è un'email automatica per informare che l'azione '%1%' è stata eseguita sulla pagina '%2%' @@ -501,257 +561,266 @@ Per gestire il tuo sito web, è sufficiente aprire il backoffice di Umbraco e in

    Buona giornata!

    Grazie da Umbraco -

    ]]>
    - [%0%] Notifica per %1% eseguita su %2% - Notifiche - - - ]]> + + [%0%] Notifica per %1% eseguita su %2% + Notifiche + + + + e selezionando il pacchetto. I pacchetti Umbraco generalmente hanno l'estensione ".umb" o ".zip". - ]]> - Autore - Documentazione - Meta dati pacchetto - Nome del pacchetto - Il pacchetto non contiene tutti gli elementi -
    - E' possibile rimuovere questo pacchetto dal sistema cliccando "rimuovi pacchetto" in basso.]]>
    - Opzioni pacchetto - Pacchetto leggimi - Pacchetto repository - Conferma eliminazione - - - Disinstalla pacchetto - + ]]> + + Autore + Documentazione + Meta dati pacchetto + Nome del pacchetto + Il pacchetto non contiene tutti gli elementi + +
    + E' possibile rimuovere questo pacchetto dal sistema cliccando "rimuovi pacchetto" in basso.]]> +
    + Opzioni pacchetto + Pacchetto leggimi + Pacchetto repository + Conferma eliminazione + + + Disinstalla pacchetto + + Avviso: tutti i documenti, i media, etc a seconda degli elementi che rimuoverai, smetteranno di funzionare, e potrebbero portare a un'instabilità del sistema, - perciò disinstalla con cautela. In caso di dubbio contattare l'autore del pacchetto.]]> - Versione del pacchetto - - - - - - - - - - usando i gruppi di membri di Umbraco.]]> - Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli - - - - - - - - - - - - - - - - - - - - - - - - ok per pubblicare %0% e rendere questo contenuto accessibile al pubblico.

    Puoi pubblicare questa pagina e tutte le sue sottopagine selezionando pubblica tutti i figli qui sotto.]]>
    - - - - - - - - - - - - - - - - Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> - - - - - - - - - - - Concierge - Contenuto - Courier - Sviluppo - Configurazione guidata Umbraco - Media - Membri - Newsletters - Impostazioni - Statistiche - Traduzione - Utenti - - - Tipo di contenuto master abilitato - Questo tipo di contenuto usa - - - - - Tipo - Foglio di stile - Tab - Titolo tab - Tabs - - - Ordinamento - Data creazione - - - - - - - - - Tipo di dati: %1%]]> - - Tipo di documento salvato - Tab creata - Tab eliminata - Tab con id: %0% eliminata - Contenuto non pubblicare - - - - Tipo di dato salvato - - - - - - - - - - - - - - - Tipo utente salvato - - - - - - Partial view salvata - Partial view salvata senza errori! - Partial view non salvata - Errore durante il salvataggio del file. - - - - - - - - - - - Anteprima - Stili - - - - - - - - - Master template - - Template - - - Image - Macro - Seleziona il tipo di contenuto - Seleziona un layout - Aggiungi una riga - Aggiungi contenuto - Elimina contenuto - Impostazioni applicati - Questo contenuto non è consentito qui - Questo contenuto è consentito qui - Clicca per incorporare - Clicca per inserire l'immagine - Didascalia dell'immagine... - Scrivi qui... - I Grid Layout - I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti - Aggiungi un Grid Layout - Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni - Configurazioni della riga - Le righe sono le colonne predefinite disposte orizzontalmente - Aggiungi configurazione della riga - Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne - Colonne - Totale combinazioni delle colonne nel grid layout - Impostazioni - Configura le impostazioni che possono essere cambiate dai editori - Stili - Configura i stili che possono essere cambiati dai editori - Permetti tutti i editor - Permetti tutte le configurazioni della riga - - - - - - Scegli il campo - Converte le interruzioni di linea - - Campi Personalizzati - - - - - - - Minuscolo - Nessuno - - - Ricorsivo - - - Campi Standard - Maiuscolo - - - - - - - - Dettagli - Scarica xml DTD - Campi - Includi le sottopagine - + + Versione del pacchetto + + + + + + + + + + usando i gruppi di membri di Umbraco.]]> + Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli + + + + + + + + + + + + + + + + + + + + + + + + ok per pubblicare %0% e rendere questo contenuto accessibile al pubblico.

    Puoi pubblicare questa pagina e tutte le sue sottopagine selezionando pubblica tutti i figli qui sotto.]]>
    + + + + + + + + + + + + + + + + Il testo in rosso non verrà mostrato nella versione selezionata, quello in verde verrà aggiunto]]> + + + + + + + + + + + Concierge + Contenuto + Courier + Sviluppo + Configurazione guidata Umbraco + Media + Membri + Newsletters + Impostazioni + Statistiche + Traduzione + Utenti + + + Tipo di contenuto master abilitato + Questo tipo di contenuto usa + + + + + Tipo + Foglio di stile + Tab + Titolo tab + Tabs + + + Ordinamento + Data creazione + + + + + + + + + Tipo di dati: %1%]]> + + Tipo di documento salvato + Tab creata + Tab eliminata + Tab con id: %0% eliminata + Contenuto non pubblicare + + + + Tipo di dato salvato + + + + + + + + + + + + + + + Tipo utente salvato + + + + + + Partial view salvata + Partial view salvata senza errori! + Partial view non salvata + Errore durante il salvataggio del file. + + + + + + + + + + + Anteprima + Stili + + + + + + + + + Master template + + Template + Data creazione + + + Immagine + Macro + Seleziona il tipo di contenuto + Seleziona un layout + Aggiungi una riga + Aggiungi contenuto + Elimina contenuto + Impostazioni applicati + Questo contenuto non è consentito qui + Questo contenuto è consentito qui + Clicca per incorporare + Clicca per inserire l'immagine + Didascalia dell'immagine... + Scrivi qui... + I Grid Layout + I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti + Aggiungi un Grid Layout + Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni + Configurazioni della riga + Le righe sono le colonne predefinite disposte orizzontalmente + Aggiungi configurazione della riga + Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne + Colonne + Totale combinazioni delle colonne nel grid layout + Impostazioni + Configura le impostazioni che possono essere cambiate dai editori + Stili + Configura i stili che possono essere cambiati dai editori + Permetti tutti i editor + Permetti tutte le configurazioni della riga + + + + + + Scegli il campo + Converte le interruzioni di linea + + Campi Personalizzati + + + + + + + Minuscolo + Nessuno + + + Ricorsivo + + + Campi Standard + Maiuscolo + + + + + + + + Dettagli + Scarica xml DTD + Campi + Includi le sottopagine + + - - - - - - - - - - Traduttore - - - - Cache Browser - Cestino - Pacchetti creati - Tipi di dato - Dizionario - Pacchetti installati - Installare skin - Installare starter kit - Lingue - Installa un pacchetto locale - Macros - Tipi di media - Membri - Gruppi di Membri - Ruoli - Tipologia Membri - Tipi di documento - Pacchetti - Pacchetti - Installa dal repository - Installa Runway - Moduli Runway - Files di scripting - Scripts - Fogli di stile - Templates - Permessi Utente - Tipi di Utente - Utenti - - - - - - - - - Amministratore - Campo Categoria - Cambia la tua password - - Conferma la nuova password - Contenuto del canale - Campo Descrizione - Disabilita l'utente - Tipo di Documento - Editor - Campo Eccezione - Lingua - Login - - Sezioni - Modifica la tua password - - Password - - - Password attuale - - - - - - - - - - - Username - - - - Autore - + ]]> + + + + + + + + + + + Traduttore + + + + Cache Browser + Cestino + Pacchetti creati + Tipi di dato + Dizionario + Pacchetti installati + Installare skin + Installare starter kit + Lingue + Installa un pacchetto locale + Macros + Tipi di media + Membri + Gruppi di Membri + Ruoli + Tipologia Membri + Tipi di documento + Pacchetti + Pacchetti + Installa dal repository + Installa Runway + Moduli Runway + Files di scripting + Scripts + Fogli di stile + Templates + Permessi Utente + Tipi di Utente + Utenti + Contenuti + + + + + + + + + Amministratore + Campo Categoria + Cambia la tua password + + Conferma la nuova password + Contenuto del canale + Campo Descrizione + Disabilita l'utente + Tipo di Documento + Editor + Campo Eccezione + Lingua + Login + + Sezioni + Modifica la tua password + + Password + + + Password attuale + + + + + + + + + + + Username + + + + Autore + Il tuo profilo + La tua storia recente + Crea utente + Crea nuovi utenti e dai loro accesso ad Umbraco. Quando un nuovo utente viene creato viene generata una password che potrai condividere con l'utente. + Aggiungi gruppi per assegnare accessi e permessi + Torna agli utenti + Gestione utenti + + + Devi aggiungere almeno + elementi + + + Digita per cercare... + Inserisci la tua email + Inserisci la tua password + Inserisci un nome... + Inserisci una email... + + + o clicca qui per scegliere i file + Trascina i tuoi file all'interno di quest'area + + + Contenuti + Info + Elementi + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 2ead7665e0..a6a9e7188c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -203,6 +203,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v16.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v17.0 @@ -212,8 +213,7 @@ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll - - $(ProgramFiles32)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v17.0\Web\Microsoft.Web.Publishing.Tasks.dll @@ -322,4 +322,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 7d30bbed41..67d9d4aa19 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -100,6 +100,13 @@ + + + + + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 437e6c60a5..474c6edf57 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -48,6 +48,7 @@ +