Files
Umbraco-CMS/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs

1076 lines
49 KiB
C#
Raw Normal View History

using System;
2017-12-07 16:45:25 +01:00
using System.Collections.Generic;
2021-09-14 22:13:39 +02:00
using System.Globalization;
2017-12-07 16:45:25 +01:00
using System.Linq;
using System.Text.RegularExpressions;
2020-09-17 09:42:55 +02:00
using Microsoft.Extensions.Logging;
2017-12-07 16:45:25 +01:00
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
using Umbraco.Cms.Infrastructure.Persistence.Querying;
2022-01-13 17:44:11 +00:00
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;
using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics;
2017-12-07 16:45:25 +01:00
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
2017-12-07 16:45:25 +01:00
{
internal sealed class ContentRepositoryBase
{
/// <summary>
/// This is used for unit tests ONLY
/// </summary>
public static bool ThrowOnWarning { get; set; } = false;
2017-12-07 16:45:25 +01:00
}
2020-12-22 10:30:16 +11:00
public abstract class ContentRepositoryBase<TId, TEntity, TRepository> : EntityRepositoryBase<TId, TEntity>, IContentRepository<TId, TEntity>
where TEntity : class, IContentBase
2017-12-07 16:45:25 +01:00
where TRepository : class, IRepository
{
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
private readonly IEventAggregator _eventAggregator;
/// <summary>
///
/// </summary>
/// <param name="scopeAccessor"></param>
/// <param name="cache"></param>
/// <param name="logger"></param>
/// <param name="languageRepository"></param>
/// <param name="propertyEditors">
/// Lazy property value collection - must be lazy because we have a circular dependency since some property editors require services, yet these services require property editors
/// </param>
Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/move-files # Conflicts: # src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs # src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs # src/Umbraco.Tests/Published/NestedContentTests.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs # src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs # src/Umbraco.Tests/Services/ContentServiceTests.cs # src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs # src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs
2019-12-11 08:13:51 +01:00
protected ContentRepositoryBase(
IScopeAccessor scopeAccessor,
2020-09-17 09:42:55 +02:00
AppCaches cache,
2020-12-22 10:30:16 +11:00
ILogger<EntityRepositoryBase<TId, TEntity>> logger,
Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/move-files # Conflicts: # src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs # src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs # src/Umbraco.Tests/Published/NestedContentTests.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs # src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs # src/Umbraco.Tests/Services/ContentServiceTests.cs # src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs # src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs
2019-12-11 08:13:51 +01:00
ILanguageRepository languageRepository,
IRelationRepository relationRepository,
IRelationTypeRepository relationTypeRepository,
PropertyEditorCollection propertyEditors,
Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/move-files # Conflicts: # src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs # src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs # src/Umbraco.Tests/Published/NestedContentTests.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs # src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs # src/Umbraco.Tests/Services/ContentServiceTests.cs # src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs # src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs
2019-12-11 08:13:51 +01:00
DataValueReferenceFactoryCollection dataValueReferenceFactories,
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
IDataTypeService dataTypeService,
IEventAggregator eventAggregator)
2017-12-14 17:04:44 +01:00
: base(scopeAccessor, cache, logger)
2018-04-21 09:57:28 +02:00
{
DataTypeService = dataTypeService;
2018-04-21 09:57:28 +02:00
LanguageRepository = languageRepository;
RelationRepository = relationRepository;
RelationTypeRepository = relationTypeRepository;
PropertyEditors = propertyEditors;
_dataValueReferenceFactories = dataValueReferenceFactories;
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
_eventAggregator = eventAggregator;
2018-04-21 09:57:28 +02:00
}
2017-12-07 16:45:25 +01:00
protected abstract TRepository This { get; }
/// <summary>
/// Gets the node object type for the repository's entity
/// </summary>
protected abstract Guid NodeObjectTypeId { get; }
2018-04-21 09:57:28 +02:00
protected ILanguageRepository LanguageRepository { get; }
protected IDataTypeService DataTypeService { get; }
protected IRelationRepository RelationRepository { get; }
protected IRelationTypeRepository RelationTypeRepository { get; }
2018-04-21 09:57:28 +02:00
protected PropertyEditorCollection PropertyEditors { get; }
2017-12-07 16:45:25 +01:00
#region Versions
// gets a specific version
public abstract TEntity? GetVersion(int versionId);
2017-12-07 16:45:25 +01:00
// gets all versions, current first
public abstract IEnumerable<TEntity> GetAllVersions(int nodeId);
2018-10-22 08:45:30 +02:00
// gets all versions, current first
public virtual IEnumerable<TEntity> GetAllVersionsSlim(int nodeId, int skip, int take)
=> GetAllVersions(nodeId).Skip(skip).Take(take);
2017-12-07 16:45:25 +01:00
// gets all version ids, current first
public virtual IEnumerable<int> GetVersionIds(int nodeId, int maxRows)
{
var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql =>
2017-12-07 16:45:25 +01:00
tsql.Select<ContentVersionDto>(x => x.Id)
.From<ContentVersionDto>()
.Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId"))
.OrderByDescending<ContentVersionDto>(x => x.Current) // current '1' comes before others '0'
.AndByDescending<ContentVersionDto>(x => x.VersionDate) // most recent first
);
return Database.Fetch<int>(SqlSyntax.SelectTop(template.Sql(nodeId), maxRows));
}
// deletes a specific version
public virtual void DeleteVersion(int versionId)
{
// TODO: test object node type?
2017-12-07 16:45:25 +01:00
// get the version we want to delete
var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersion, tsql =>
2017-12-07 16:45:25 +01:00
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.Id == SqlTemplate.Arg<int>("versionId"))
);
var versionDto = Database.Fetch<ContentVersionDto>(template.Sql(new { versionId })).FirstOrDefault();
// nothing to delete
if (versionDto == null)
return;
// don't delete the current version
if (versionDto.Current)
throw new InvalidOperationException("Cannot delete the current version.");
PerformDeleteVersion(versionDto.NodeId, versionId);
}
// deletes all versions of an entity, older than a date.
public virtual void DeleteVersions(int nodeId, DateTime versionDate)
{
// TODO: test object node type?
2017-12-07 16:45:25 +01:00
// get the versions we want to delete, excluding the current one
var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersions, tsql =>
2017-12-07 16:45:25 +01:00
tsql.Select<ContentVersionDto>().From<ContentVersionDto>().Where<ContentVersionDto>(x => x.NodeId == SqlTemplate.Arg<int>("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg<DateTime>("versionDate"))
);
var versionDtos = Database.Fetch<ContentVersionDto>(template.Sql(new { nodeId, versionDate }));
foreach (var versionDto in versionDtos)
PerformDeleteVersion(versionDto.NodeId, versionDto.Id);
}
// actually deletes a version
protected abstract void PerformDeleteVersion(int id, int versionId);
#endregion
#region Count
/// <summary>
/// Count descendants of an item.
/// </summary>
public int CountDescendants(int parentId, string? contentTypeAlias = null)
2017-12-07 16:45:25 +01:00
{
var pathMatch = parentId == -1
? "-1,"
: "," + parentId + ",";
var sql = SqlContext.Sql()
.SelectCount()
.From<NodeDto>();
if (contentTypeAlias.IsNullOrWhiteSpace())
{
sql
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.Where<NodeDto>(x => x.Path.Contains(pathMatch));
}
else
{
sql
.InnerJoin<ContentDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<ContentTypeDto>()
.On<ContentTypeDto, ContentDto>(left => left.NodeId, right => right.ContentTypeId)
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.Where<NodeDto>(x => x.Path.Contains(pathMatch))
.Where<ContentTypeDto>(x => x.Alias == contentTypeAlias);
}
return Database.ExecuteScalar<int>(sql);
}
/// <summary>
/// Count children of an item.
/// </summary>
public int CountChildren(int parentId, string? contentTypeAlias = null)
2017-12-07 16:45:25 +01:00
{
var sql = SqlContext.Sql()
.SelectCount()
.From<NodeDto>();
if (contentTypeAlias.IsNullOrWhiteSpace())
{
sql
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.Where<NodeDto>(x => x.ParentId == parentId);
}
else
{
sql
.InnerJoin<ContentDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<ContentTypeDto>()
.On<ContentTypeDto, ContentDto>(left => left.NodeId, right => right.ContentTypeId)
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.Where<NodeDto>(x => x.ParentId == parentId)
.Where<ContentTypeDto>(x => x.Alias == contentTypeAlias);
}
return Database.ExecuteScalar<int>(sql);
}
/// <summary>
/// Count items.
/// </summary>
public int Count(string? contentTypeAlias = null)
2017-12-07 16:45:25 +01:00
{
var sql = SqlContext.Sql()
.SelectCount()
.From<NodeDto>();
if (contentTypeAlias.IsNullOrWhiteSpace())
{
sql
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId);
}
else
{
sql
.InnerJoin<ContentDto>()
.On<NodeDto, ContentDto>(left => left.NodeId, right => right.NodeId)
.InnerJoin<ContentTypeDto>()
.On<ContentTypeDto, ContentDto>(left => left.NodeId, right => right.ContentTypeId)
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.Where<ContentTypeDto>(x => x.Alias == contentTypeAlias);
}
return Database.ExecuteScalar<int>(sql);
}
#endregion
#region Tags
/// <summary>
/// Updates tags for an item.
2017-12-07 16:45:25 +01:00
/// </summary>
protected void SetEntityTags(IContentBase entity, ITagRepository tagRepo, IJsonSerializer serializer)
2017-12-07 16:45:25 +01:00
{
foreach (var property in entity.Properties)
{
Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/move-files # Conflicts: # src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs # src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs # src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs # src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs # src/Umbraco.Tests/Published/NestedContentTests.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs # src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs # src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs # src/Umbraco.Tests/Services/ContentServiceTests.cs # src/Umbraco.Tests/Web/TemplateUtilitiesTests.cs # src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs # src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs
2019-12-11 08:13:51 +01:00
var tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService);
2018-11-20 13:24:06 +01:00
if (tagConfiguration == null) continue; // not a tags property
if (property.PropertyType.VariesByCulture())
{
var tags = new List<ITag>();
foreach (var pvalue in property.Values)
{
var tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture);
2018-11-20 13:24:06 +01:00
var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture);
var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId });
tags.AddRange(cultureTags);
}
tagRepo.Assign(entity.Id, property.PropertyTypeId, tags);
}
else
{
var tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings
2018-11-20 13:24:06 +01:00
var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x });
tagRepo.Assign(entity.Id, property.PropertyTypeId, tags);
}
}
2017-12-07 16:45:25 +01:00
}
// TODO: should we do it when un-publishing? or?
2017-12-07 16:45:25 +01:00
/// <summary>
/// Clears tags for an item.
2017-12-07 16:45:25 +01:00
/// </summary>
protected void ClearEntityTags(IContentBase entity, ITagRepository tagRepo)
2017-12-07 16:45:25 +01:00
{
tagRepo.RemoveAll(entity.Id);
2017-12-07 16:45:25 +01:00
}
#endregion
private Sql<ISqlContext> PreparePageSql(Sql<ISqlContext> sql, Sql<ISqlContext>? filterSql, Ordering ordering)
2017-12-07 16:45:25 +01:00
{
// non-filtering, non-ordering = nothing to do
if (filterSql == null && ordering.IsEmpty) return sql;
2017-12-07 16:45:25 +01:00
// preserve original
var psql = new Sql<ISqlContext>(sql.SqlContext, sql.SQL, sql.Arguments);
// apply filter
if (filterSql != null)
psql.Append(filterSql);
// non-sorting, we're done
if (ordering.IsEmpty)
2017-12-07 16:45:25 +01:00
return psql;
// else apply ordering
ApplyOrdering(ref psql, ordering);
2017-12-07 16:45:25 +01:00
// no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique.
// if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column
// is empty for many nodes) - see: http://issues.umbraco.org/issue/U4-8831
2018-10-18 14:16:54 +02:00
var (dbfield, _) = SqlContext.VisitDto<NodeDto>(x => x.NodeId);
if (ordering.IsCustomField || !ordering.OrderBy.InvariantEquals("id"))
2017-12-07 16:45:25 +01:00
{
2019-01-21 15:39:19 +01:00
psql.OrderBy(GetAliasedField(dbfield, sql));
2017-12-07 16:45:25 +01:00
}
// create prepared sql
// ensure it's single-line as NPoco PagingHelper has issues with multi-lines
psql = Sql(psql.SQL.ToSingleLine(), psql.Arguments);
// replace the magic culture parameter (see DocumentRepository.GetBaseQuery())
if (!ordering.Culture.IsNullOrWhiteSpace())
{
for (var i = 0; i < psql.Arguments.Length; i++)
{
if (psql.Arguments[i] is string s && s == "[[[ISOCODE]]]")
{
psql.Arguments[i] = ordering.Culture;
}
}
}
2017-12-07 16:45:25 +01:00
return psql;
}
private void ApplyOrdering(ref Sql<ISqlContext> sql, Ordering ordering)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
var orderBy = ordering.IsCustomField
? ApplyCustomOrdering(ref sql, ordering)
: ApplySystemOrdering(ref sql, ordering);
2018-09-19 17:50:43 +02:00
// beware! NPoco paging code parses the query to isolate the ORDER BY fragment,
// using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything
// else in orderBy is going to break NPoco / not be detected
// beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar]
// to [bar] only, so we MUST use aliases, cannot use [table].[field]
// beware! pre-2012 SqlServer is using a convoluted syntax for paging, which
// includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...",
// so anything added here MUST also be part of the inner SELECT statement, ie
// the original statement, AND must be using the proper alias, as the inner SELECT
// will hide the original table.field names entirely
if (ordering.Direction == Direction.Ascending)
sql.OrderBy(orderBy);
else
sql.OrderByDescending(orderBy);
}
protected virtual string ApplySystemOrdering(ref Sql<ISqlContext> sql, Ordering ordering)
2017-12-07 16:45:25 +01:00
{
// id is invariant
if (ordering.OrderBy.InvariantEquals("id"))
2018-09-19 17:50:43 +02:00
return GetAliasedField(SqlSyntax.GetFieldName<NodeDto>(x => x.NodeId), sql);
// sort order is invariant
if (ordering.OrderBy.InvariantEquals("sortOrder"))
2018-09-19 17:50:43 +02:00
return GetAliasedField(SqlSyntax.GetFieldName<NodeDto>(x => x.SortOrder), sql);
// path is invariant
if (ordering.OrderBy.InvariantEquals("path"))
2018-09-19 17:50:43 +02:00
return GetAliasedField(SqlSyntax.GetFieldName<NodeDto>(x => x.Path), sql);
// note: 'owner' is the user who created the item as a whole,
// we don't have an 'owner' per culture (should we?)
if (ordering.OrderBy.InvariantEquals("owner"))
{
var joins = Sql()
.InnerJoin<UserDto>("ownerUser").On<NodeDto, UserDto>((node, user) => node.UserId == user.Id, aliasRight: "ownerUser");
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be selected + aliased
sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName<UserDto>(x => x.UserName, "ownerUser") + " AS ordering "), sql.Arguments);
sql = InsertJoins(sql, joins);
2018-09-19 17:50:43 +02:00
return "ordering";
}
// note: each version culture variation has a date too,
// maybe we would want to use it instead?
if (ordering.OrderBy.InvariantEquals("versionDate") || ordering.OrderBy.InvariantEquals("updateDate"))
2018-09-19 17:50:43 +02:00
return GetAliasedField(SqlSyntax.GetFieldName<ContentVersionDto>(x => x.VersionDate), sql);
// create date is invariant (we don't keep each culture's creation date)
if (ordering.OrderBy.InvariantEquals("createDate"))
2018-09-19 17:50:43 +02:00
return GetAliasedField(SqlSyntax.GetFieldName<NodeDto>(x => x.CreateDate), sql);
// name is variant
if (ordering.OrderBy.InvariantEquals("name"))
{
// no culture = can only work on the invariant name
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be aliased
if (ordering.Culture.IsNullOrWhiteSpace())
return GetAliasedField(SqlSyntax.GetFieldName<NodeDto>(x => x.Text!), sql);
2018-10-18 14:16:54 +02:00
// "variantName" alias is defined in DocumentRepository.GetBaseQuery
// TODO: what if it is NOT a document but a ... media or whatever?
2018-10-18 14:16:54 +02:00
// previously, we inserted the join+select *here* so we were sure to have it,
// but now that's not the case anymore!
return "variantName";
}
// content type alias is invariant
2020-04-07 01:02:08 +10:00
if (ordering.OrderBy.InvariantEquals("contentTypeAlias"))
{
var joins = Sql()
.InnerJoin<ContentTypeDto>("ctype").On<ContentDto, ContentTypeDto>((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype");
// see notes in ApplyOrdering: the field MUST be selected + aliased
sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName<ContentTypeDto>(x => x.Alias!, "ctype") + " AS ordering "), sql.Arguments);
sql = InsertJoins(sql, joins);
return "ordering";
}
// previously, we'd accept anything and just sanitize it - not anymore
throw new NotSupportedException($"Ordering by {ordering.OrderBy} not supported.");
2017-12-07 16:45:25 +01:00
}
private string ApplyCustomOrdering(ref Sql<ISqlContext> sql, Ordering ordering)
2017-12-07 16:45:25 +01:00
{
// sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through value
// from 'current' content version for the given order by field
var sortedInt = string.Format(SqlContext.SqlSyntax.ConvertIntegerToOrderableString, "intValue");
var sortedDecimal = string.Format(SqlContext.SqlSyntax.ConvertDecimalToOrderableString, "decimalValue");
2017-12-07 16:45:25 +01:00
var sortedDate = string.Format(SqlContext.SqlSyntax.ConvertDateToOrderableString, "dateValue");
var sortedString = "COALESCE(varcharValue,'')"; // assuming COALESCE is ok for all syntaxes
// needs to be an outer join since there's no guarantee that any of the nodes have values for this property
var innerSql = Sql().Select($@"CASE
WHEN intValue IS NOT NULL THEN {sortedInt}
WHEN decimalValue IS NOT NULL THEN {sortedDecimal}
WHEN dateValue IS NOT NULL THEN {sortedDate}
ELSE {sortedString}
END AS customPropVal,
cver.nodeId AS customPropNodeId")
.From<ContentVersionDto>("cver")
.InnerJoin<PropertyDataDto>("opdata")
.On<ContentVersionDto, PropertyDataDto>((version, pdata) => version.Id == pdata.VersionId, "cver", "opdata")
.InnerJoin<PropertyTypeDto>("optype").On<PropertyDataDto, PropertyTypeDto>((pdata, ptype) => pdata.PropertyTypeId == ptype.Id, "opdata", "optype")
.LeftJoin<LanguageDto>().On<PropertyDataDto, LanguageDto>((pdata, lang) => pdata.LanguageId == lang.Id, "opdata")
2017-12-07 16:45:25 +01:00
.Where<ContentVersionDto>(x => x.Current, "cver") // always query on current (edit) values
.Where<PropertyTypeDto>(x => x.Alias == ordering.OrderBy, "optype")
.Where<PropertyDataDto, LanguageDto>((opdata, lang) => opdata.LanguageId == null || lang.IsoCode == ordering.Culture, "opdata");
// merge arguments
var argsList = sql.Arguments.ToList();
var innerSqlString = ParameterHelper.ProcessParams(innerSql.SQL, innerSql.Arguments, argsList);
2017-12-07 16:45:25 +01:00
// create the outer join complete sql fragment
2017-12-07 16:45:25 +01:00
var outerJoinTempTable = $@"LEFT OUTER JOIN ({innerSqlString}) AS customPropData
ON customPropData.customPropNodeId = {Cms.Core.Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important!
2017-12-07 16:45:25 +01:00
// insert this just above the first WHERE
var newSql = InsertBefore(sql.SQL, "WHERE", outerJoinTempTable);
2017-12-07 16:45:25 +01:00
2018-09-19 17:50:43 +02:00
// see notes in ApplyOrdering: the field MUST be selected + aliased
newSql = InsertBefore(newSql, "FROM", ", customPropData.customPropVal AS ordering "); // trailing space is important!
2017-12-07 16:45:25 +01:00
// create the new sql
sql = Sql(newSql, argsList.ToArray());
2017-12-07 16:45:25 +01:00
// and order by the custom field
// this original code means that an ascending sort would first expose all NULL values, ie items without a value
2018-09-19 17:50:43 +02:00
return "ordering";
// note: adding an extra sorting criteria on
// "(CASE WHEN customPropData.customPropVal IS NULL THEN 1 ELSE 0 END")
// would ensure that items without a value always come last, both in ASC and DESC-ending sorts
2017-12-07 16:45:25 +01:00
}
public abstract IEnumerable<TEntity> GetPage(IQuery<TEntity>? query,
long pageIndex, int pageSize, out long totalRecords,
IQuery<TEntity>? filter,
Ordering? ordering);
public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options)
2020-04-07 01:02:08 +10:00
{
var report = new Dictionary<int, ContentDataIntegrityReportEntry>();
2020-04-07 01:02:08 +10:00
var sql = SqlContext.Sql()
.Select<NodeDto>()
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == NodeObjectTypeId)
.OrderBy<NodeDto>(x => x.Level, x => x.ParentId, x => x.SortOrder);
var nodesToRebuild = new Dictionary<int, List<NodeDto>>();
var validNodes = new Dictionary<int, NodeDto>();
var rootIds = new[] {Cms.Core.Constants.System.Root, Cms.Core.Constants.System.RecycleBinContent, Cms.Core.Constants.System.RecycleBinMedia};
2020-04-07 17:38:40 +10:00
var currentParentIds = new HashSet<int>(rootIds);
2020-04-07 01:02:08 +10:00
var prevParentIds = currentParentIds;
var lastLevel = -1;
// use a forward cursor (query)
foreach (var node in Database.Query<NodeDto>(sql))
{
if (node.Level != lastLevel)
{
// changing levels
prevParentIds = currentParentIds;
currentParentIds = null;
lastLevel = node.Level;
}
if (currentParentIds == null)
{
// we're reset
currentParentIds = new HashSet<int>();
}
currentParentIds.Add(node.NodeId);
2020-04-07 17:38:40 +10:00
// paths parts without the roots
2021-09-14 22:13:39 +02:00
var pathParts = node.Path.Split(Constants.CharArrays.Comma).Where(x => !rootIds.Contains(int.Parse(x, CultureInfo.InvariantCulture))).ToArray();
2020-04-07 01:02:08 +10:00
if (!prevParentIds.Contains(node.ParentId))
{
// invalid, this will be because the level is wrong (which prob means path is wrong too)
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId));
AppendNodeToFix(nodesToRebuild, node);
2020-04-07 01:02:08 +10:00
}
2020-04-07 17:38:40 +10:00
else if (pathParts.Length == 0)
2020-04-07 01:02:08 +10:00
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty));
AppendNodeToFix(nodesToRebuild, node);
2020-04-07 01:02:08 +10:00
}
2020-04-07 17:38:40 +10:00
else if (pathParts.Length != node.Level)
2020-04-07 01:02:08 +10:00
{
// invalid, either path or level is wrong
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch));
AppendNodeToFix(nodesToRebuild, node);
2020-04-07 01:02:08 +10:00
}
else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString())
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById));
AppendNodeToFix(nodesToRebuild, node);
2020-04-07 01:02:08 +10:00
}
2020-04-07 17:38:40 +10:00
else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString())
2020-04-07 01:02:08 +10:00
{
// invalid path
report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId));
AppendNodeToFix(nodesToRebuild, node);
2020-04-07 01:02:08 +10:00
}
else
{
// it's valid!
2020-04-07 01:02:08 +10:00
// don't track unless we are configured to fix
if (options.FixIssues)
validNodes.Add(node.NodeId, node);
}
}
2020-04-07 01:02:08 +10:00
var updated = new List<NodeDto>();
if (options.FixIssues)
2020-04-07 01:02:08 +10:00
{
// iterate all valid nodes to see if these are parents for invalid nodes
foreach (var (nodeId, node) in validNodes)
2020-04-07 01:02:08 +10:00
{
if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue;
2020-04-07 01:02:08 +10:00
// now we can try to rebuild the invalid paths.
2020-04-07 01:02:08 +10:00
foreach (var invalidNode in invalidNodes)
2020-04-07 01:02:08 +10:00
{
invalidNode.Level = (short)(node.Level + 1);
invalidNode.Path = node.Path + "," + invalidNode.NodeId;
updated.Add(invalidNode);
2020-04-07 01:02:08 +10:00
}
}
foreach (var node in updated)
{
Database.Update(node);
2020-04-08 10:38:02 +10:00
if (report.TryGetValue(node.NodeId, out var entry))
entry.Fixed = true;
}
2020-04-07 01:02:08 +10:00
}
return new ContentDataIntegrityReport(report);
2020-04-07 16:53:54 +10:00
}
2020-04-07 16:53:54 +10:00
private static void AppendNodeToFix(IDictionary<int, List<NodeDto>> nodesToRebuild, NodeDto node)
{
if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds))
childIds.Add(node);
else
nodesToRebuild[node.ParentId] = new List<NodeDto> { node };
2020-04-07 01:02:08 +10:00
}
// here, filter can be null and ordering cannot
protected IEnumerable<TEntity> GetPage<TDto>(IQuery<TEntity>? query,
2017-12-07 16:45:25 +01:00
long pageIndex, int pageSize, out long totalRecords,
Func<List<TDto>, IEnumerable<TEntity>> mapDtos,
Sql<ISqlContext>? filter,
Ordering? ordering)
2017-12-07 16:45:25 +01:00
{
if (ordering == null) throw new ArgumentNullException(nameof(ordering));
2017-12-07 16:45:25 +01:00
// start with base query, and apply the supplied IQuery
if (query == null) query = Query<TEntity>();
2017-12-07 16:45:25 +01:00
var sql = new SqlTranslator<TEntity>(GetBaseQuery(QueryType.Many), query).Translate();
// sort and filter
sql = PreparePageSql(sql, filter, ordering);
2017-12-07 16:45:25 +01:00
// get a page of DTOs and the total count
var pagedResult = Database.Page<TDto>(pageIndex + 1, pageSize, sql);
totalRecords = Convert.ToInt32(pagedResult.TotalItems);
// map the DTOs and return
return mapDtos(pagedResult.Items);
}
protected IDictionary<int, PropertyCollection> GetPropertyCollections<T>(List<TempContent<T>> temps)
where T : class, IContentBase
{
var versions = new List<int>();
foreach (var temp in temps)
{
versions.Add(temp.VersionId);
if (temp.PublishedVersionId > 0)
versions.Add(temp.PublishedVersionId);
}
if (versions.Count == 0) return new Dictionary<int, PropertyCollection>();
Merge commit '94d525d88f713b36419f28bfda4d82ee68637d83' into v9/dev # Conflicts: # build/NuSpecs/UmbracoCms.Web.nuspec # src/Umbraco.Core/Composing/Current.cs # src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs # src/Umbraco.Core/Runtime/CoreRuntime.cs # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs # src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs # src/Umbraco.Persistence.SqlCe/SqlCeSyntaxProvider.cs # src/Umbraco.PublishedCache.NuCache/DataSource/BTree.cs # src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs # src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializationResult.cs # src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataSerializerEntityType.cs # src/Umbraco.PublishedCache.NuCache/DataSource/ContentData.cs # src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs # src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs # src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializer.cs # src/Umbraco.PublishedCache.NuCache/DataSource/IContentCacheDataSerializerFactory.cs # src/Umbraco.PublishedCache.NuCache/DataSource/IDictionaryOfPropertyDataSerializer.cs # src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializer.cs # src/Umbraco.PublishedCache.NuCache/DataSource/JsonContentNestedDataSerializerFactory.cs # src/Umbraco.PublishedCache.NuCache/DataSource/LazyCompressedString.cs # src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializer.cs # src/Umbraco.PublishedCache.NuCache/DataSource/MsgPackContentNestedDataSerializerFactory.cs # src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs # src/Umbraco.PublishedCache.NuCache/NuCacheSerializerComponent.cs # src/Umbraco.PublishedCache.NuCache/NuCacheSerializerComposer.cs # src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs # src/Umbraco.Tests/App.config # src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs # src/Umbraco.Tests/PublishedContent/NuCacheTests.cs # src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI/web.Template.Debug.config # src/Umbraco.Web.UI/web.Template.config # src/Umbraco.Web/Composing/ModuleInjector.cs # src/Umbraco.Web/Editors/NuCacheStatusController.cs # src/Umbraco.Web/PublishedCache/NuCache/DataSource/ContentNestedData.cs # src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs # src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs # src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs # src/Umbraco.Web/Runtime/WebRuntime.cs
2021-06-24 09:43:57 -06:00
// 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?
2017-12-07 16:45:25 +01:00
// get all PropertyDataDto for all definitions / versions
var allPropertyDataDtos = Database.FetchByGroups<PropertyDataDto, int>(versions, Constants.Sql.MaxParameterCount, batch =>
2017-12-07 16:45:25 +01:00
SqlContext.Sql()
.Select<PropertyDataDto>()
.From<PropertyDataDto>()
.WhereIn<PropertyDataDto>(x => x.VersionId, batch))
.ToList();
// get PropertyDataDto distinct PropertyTypeDto
var allPropertyTypeIds = allPropertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList();
var allPropertyTypeDtos = Database.FetchByGroups<PropertyTypeDto, int>(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch =>
2017-12-07 16:45:25 +01:00
SqlContext.Sql()
.Select<PropertyTypeDto>(r => r.Select(x => x.DataTypeDto))
2017-12-07 16:45:25 +01:00
.From<PropertyTypeDto>()
.InnerJoin<DataTypeDto>().On<PropertyTypeDto, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId)
2017-12-07 16:45:25 +01:00
.WhereIn<PropertyTypeDto>(x => x.Id, batch));
// index the types for perfs, and assign to PropertyDataDto
var indexedPropertyTypeDtos = allPropertyTypeDtos.ToDictionary(x => x.Id, x => x);
foreach (var a in allPropertyDataDtos)
a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId];
// now we have
2019-01-22 18:03:39 -05:00
// - the definitions
2017-12-07 16:45:25 +01:00
// - all property data dtos
2019-07-30 19:08:37 +10:00
// - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems)
2017-12-07 16:45:25 +01:00
// and we need to build the proper property collections
2019-07-30 19:08:37 +10:00
return GetPropertyCollections(temps, allPropertyDataDtos);
2017-12-07 16:45:25 +01:00
}
2019-07-30 19:08:37 +10:00
private IDictionary<int, PropertyCollection> GetPropertyCollections<T>(List<TempContent<T>> temps, IEnumerable<PropertyDataDto> allPropertyDataDtos)
2017-12-07 16:45:25 +01:00
where T : class, IContentBase
{
var result = new Dictionary<int, PropertyCollection>();
var compositionPropertiesIndex = new Dictionary<int, IPropertyType[]>();
2017-12-07 16:45:25 +01:00
// index PropertyDataDto per versionId for perfs
// merge edited and published dtos
var indexedPropertyDataDtos = new Dictionary<int, List<PropertyDataDto>>();
foreach (var dto in allPropertyDataDtos)
{
var versionId = dto.VersionId;
if (indexedPropertyDataDtos.TryGetValue(versionId, out var list) == false)
indexedPropertyDataDtos[versionId] = list = new List<PropertyDataDto>();
list.Add(dto);
}
foreach (var temp in temps)
{
// compositionProperties is the property types for the entire composition
// use an index for perfs
if (temp.ContentType is null)
{
continue;
}
2017-12-07 16:45:25 +01:00
if (compositionPropertiesIndex.TryGetValue(temp.ContentType.Id, out var compositionProperties) == false)
compositionPropertiesIndex[temp.ContentType.Id] = compositionProperties = temp.ContentType.CompositionPropertyTypes.ToArray();
// map the list of PropertyDataDto to a list of Property
var propertyDataDtos = new List<PropertyDataDto>();
if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out var propertyDataDtos1))
{
propertyDataDtos.AddRange(propertyDataDtos1);
if (temp.VersionId == temp.PublishedVersionId) // dirty corner case
2018-04-21 09:57:28 +02:00
propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1)));
2017-12-07 16:45:25 +01:00
}
if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out var propertyDataDtos2))
propertyDataDtos.AddRange(propertyDataDtos2);
2018-04-21 09:57:28 +02:00
var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList();
2017-12-07 16:45:25 +01:00
if (result.ContainsKey(temp.VersionId))
{
if (ContentRepositoryBase.ThrowOnWarning)
throw new InvalidOperationException($"The query returned multiple property sets for content {temp.Id}, {temp.ContentType.Name}");
Logger.LogWarning("The query returned multiple property sets for content {ContentId}, {ContentTypeName}", temp.Id, temp.ContentType.Name);
2017-12-07 16:45:25 +01:00
}
result[temp.VersionId] = new PropertyCollection(properties);
}
return result;
}
2018-09-19 17:50:43 +02:00
protected string InsertBefore(Sql<ISqlContext> s, string atToken, string insert)
=> InsertBefore(s.SQL, atToken, insert);
protected string InsertBefore(string s, string atToken, string insert)
2017-12-07 16:45:25 +01:00
{
var pos = s.InvariantIndexOf(atToken);
if (pos < 0) throw new Exception($"Could not find token \"{atToken}\".");
return s.Insert(pos, insert);
}
protected Sql<ISqlContext> InsertJoins(Sql<ISqlContext> sql, Sql<ISqlContext> joins)
{
var joinsSql = joins.SQL;
var args = sql.Arguments;
2017-12-07 16:45:25 +01:00
// merge args if any
if (joins.Arguments.Length > 0)
2017-12-07 16:45:25 +01:00
{
var argsList = args.ToList();
joinsSql = ParameterHelper.ProcessParams(joinsSql, joins.Arguments, argsList);
args = argsList.ToArray();
2017-12-07 16:45:25 +01:00
}
return Sql(InsertBefore(sql.SQL, "WHERE", joinsSql), args);
2017-12-07 16:45:25 +01:00
}
2018-09-19 17:50:43 +02:00
private string GetAliasedField(string field, Sql sql)
{
// get alias, if aliased
//
// regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)"
// and, if found & a group's field matches the field name, returns the alias
//
// so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]"
// then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]"
var matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL);
var match = matches.Cast<Match>().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field));
return match == null ? field : match.Groups[2].Value;
}
protected string GetQuotedFieldName(string tableName, string fieldName)
2017-12-07 16:45:25 +01:00
{
return SqlContext.SqlSyntax.GetQuotedTableName(tableName) + "." + SqlContext.SqlSyntax.GetQuotedColumnName(fieldName);
}
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
#region UnitOfWork Notification
2017-12-07 16:45:25 +01:00
/*
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
* The reason why EntityRefreshNotification is published from the repository and not the service is because
* the published state of the IContent must be "Publishing" when the event is raised for the cache to handle it correctly.
* This state is changed half way through the repository method, meaning that if we publish the notification
* after the state will be "Published" and the cache won't handle it correctly,
* It wont call OnRepositoryRefreshed with the published flag set to true, the same is true for unpublishing
* where it wont remove the entity from the nucache.
* We can't publish the notification before calling Save method on the repository either,
* because that method sets certain fields such as create date, update date, etc...
*/
/// <summary>
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
/// Publishes a notification, used to publish <see cref="EntityRefreshNotification{T}"/> for caching purposes.
/// </summary>
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
protected void OnUowRefreshedEntity(INotification notification) => _eventAggregator.Publish(notification);
2018-04-28 09:55:36 +02:00
2017-12-07 16:45:25 +01:00
Netcore: Migrate RepositoryBase and ContentTypeServiceBase events (#10141) * Remove ScopeEntityRemove from ContentRepositoryBase and rely on "ing" notifications * Remove old event handler from ContentEventsTests * Remove ScopedVersionRemove from ContentRepositoryBase and rely on service notifications instead * Remove unused ScopedVersionEventArgs from ContentRepositoryBase * Migrate ScopeEntityRefresh to notification pattern Unfortunately it's still published from the repository base * Add simple content type notifications * Publish Notifications instead of events in ContentTypeServiceBase for simple events * Switch OnChanged to use Notifications for ContentTypeServices * Publish notifications instead of raising ScopedRefreshedEntity on ContentTypeServiceBase * Hook up to the new ContentType notifications * Remove DistributedCacheBinderTests There are no longer any events to really test on. * Remove ContentTypeChange EventArgs * Remove ContentService_Copied from DistributedCacheBinder It's no longer used * Cleanup * Cleanup * Removed uncommented code * Fixed issue with unattented installs * Re-add ContentTreeChangeNotification to DistributedCache * Add new notification for ScopedEntityRemove Marked as obsolete/hidden in editor, since this should only be used for nucache for now, and should really be changed in the future * Mark Refresh notifications as obsolete/hidden These should not be used anywhere outside Nucache, and should be changed to tree change at some point. * Raise ScopedEntityRemoveNotification on repos and use in nucache Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 12:17:11 +02:00
protected void OnUowRemovingEntity(IContentBase entity) => _eventAggregator.Publish(new ScopedEntityRemoveNotification(entity, new EventMessages()));
2017-12-07 16:45:25 +01:00
#endregion
#region Classes
protected class TempContent
{
public TempContent(int id, int versionId, int publishedVersionId, IContentTypeComposition? contentType)
2017-12-07 16:45:25 +01:00
{
Id = id;
VersionId = versionId;
PublishedVersionId = publishedVersionId;
ContentType = contentType;
}
/// <summary>
/// Gets or sets the identifier of the content.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the version identifier of the content.
/// </summary>
public int VersionId { get; set; }
/// <summary>
/// Gets or sets the published version identifier of the content.
/// </summary>
public int PublishedVersionId { get; set; }
/// <summary>
/// Gets or sets the content type.
/// </summary>
public IContentTypeComposition? ContentType { get; set; }
2017-12-07 16:45:25 +01:00
/// <summary>
/// Gets or sets the identifier of the template 1 of the content.
/// </summary>
public int? Template1Id { get; set; }
/// <summary>
/// Gets or sets the identifier of the template 2 of the content.
/// </summary>
public int? Template2Id { get; set; }
}
protected class TempContent<T> : TempContent
where T : class, IContentBase
{
public TempContent(int id, int versionId, int publishedVersionId, IContentTypeComposition? contentType, T? content = null)
2017-12-07 16:45:25 +01:00
: base(id, versionId, publishedVersionId, contentType)
{
Content = content;
}
/// <summary>
/// Gets or sets the associated actual content.
/// </summary>
public T? Content { get; set; }
2017-12-07 16:45:25 +01:00
}
/// <summary>
/// For Paging, repositories must support returning different query for the query type specified
/// </summary>
/// <param name="queryType"></param>
/// <returns></returns>
protected abstract Sql<ISqlContext> GetBaseQuery(QueryType queryType);
#endregion
#region Utilities
protected virtual string? EnsureUniqueNodeName(int parentId, string nodeName, int id = 0)
2017-12-07 16:45:25 +01:00
{
var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql
.Select<NodeDto>(x => Alias(x.NodeId, "id"), x => Alias(x.Text!, "name"))
2017-12-07 16:45:25 +01:00
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType") && x.ParentId == SqlTemplate.Arg<int>("parentId"))
);
2017-12-07 16:45:25 +01:00
var sql = template.Sql(NodeObjectTypeId, parentId);
var names = Database.Fetch<SimilarNodeName>(sql);
return SimilarNodeName.GetUniqueName(names, id, nodeName);
}
protected virtual int GetNewChildSortOrder(int parentId, int first)
{
var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql
.Select("MAX(sortOrder)")
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType") && x.ParentId == SqlTemplate.Arg<int>("parentId"))
2017-12-07 16:45:25 +01:00
);
var sql = template.Sql(NodeObjectTypeId, parentId);
var sortOrder = Database.ExecuteScalar<int?>(sql);
return (sortOrder + 1) ?? first;
2017-12-07 16:45:25 +01:00
}
protected virtual NodeDto GetParentNodeDto(int parentId)
{
var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql
.Select<NodeDto>()
.From<NodeDto>()
.Where<NodeDto>(x => x.NodeId == SqlTemplate.Arg<int>("parentId"))
2017-12-07 16:45:25 +01:00
);
var sql = template.Sql(parentId);
var nodeDto = Database.First<NodeDto>(sql);
return nodeDto;
2017-12-07 16:45:25 +01:00
}
protected virtual int GetReservedId(Guid uniqueId)
{
var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql
.Select<NodeDto>(x => x.NodeId)
.From<NodeDto>()
.Where<NodeDto>(x => x.UniqueId == SqlTemplate.Arg<Guid>("uniqueId") && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation)
2017-12-07 16:45:25 +01:00
);
var sql = template.Sql(new { uniqueId });
var id = Database.ExecuteScalar<int?>(sql);
2017-12-07 16:45:25 +01:00
return id ?? 0;
}
#endregion
#region Recycle bin
public abstract int RecycleBinId { get; }
public virtual IEnumerable<TEntity>? GetRecycleBin()
2017-12-07 16:45:25 +01:00
{
return Get(Query<TEntity>().Where(entity => entity.Trashed));
}
#endregion
protected void PersistRelations(TEntity entity)
{
// Get all references from our core built in DataEditors/Property Editors
// Along with seeing if deverlopers want to collect additional references from the DataValueReferenceFactories collection
var trackedRelations = new List<UmbracoEntityReference>();
trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors));
//First delete all auto-relations for this entity
RelationRepository.DeleteByParent(entity.Id, Cms.Core.Constants.Conventions.RelationTypes.AutomaticRelationTypes);
if (trackedRelations.Count == 0) return;
trackedRelations = trackedRelations.Distinct().ToList();
var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi)
.ToDictionary(x => (Udi)x!, x => x!.Guid);
//lookup in the DB all INT ids for the GUIDs and chuck into a dictionary
var keyToIds = Database.Fetch<NodeIdKey>(Sql().Select<NodeDto>(x => x.NodeId, x => x.UniqueId).From<NodeDto>().WhereIn<NodeDto>(x => x.UniqueId, udiToGuids.Values))
.ToDictionary(x => x.UniqueId, x => x.NodeId);
var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty<int>())?
.ToDictionary(x => x.Alias, x => x);
var toSave = trackedRelations.Select(rel =>
{
if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType))
throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist");
if (!udiToGuids.TryGetValue(rel.Udi, out var guid))
return null; // This shouldn't happen!
if (!keyToIds.TryGetValue(guid, out var id))
return null; // This shouldn't happen!
return new ReadOnlyRelation(entity.Id, id, relationType.Id);
}).WhereNotNull();
2020-10-09 09:35:30 +02:00
// Save bulk relations
RelationRepository.SaveBulk(toSave);
2020-10-09 09:35:30 +02:00
}
/// <summary>
/// Inserts property values for the content entity
/// </summary>
/// <param name="entity"></param>
/// <param name="publishedVersionId"></param>
/// <param name="edited"></param>
/// <param name="editedCultures"></param>
/// <remarks>
/// Used when creating a new entity
/// </remarks>
protected void InsertPropertyValues(TEntity entity, int publishedVersionId, out bool edited, out HashSet<string>? editedCultures)
{
// persist the property data
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures);
foreach (var propertyDataDto in propertyDataDtos)
{
Database.Insert(propertyDataDto);
}
// TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
// This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
}
/// <summary>
/// Used to atomically replace the property values for the entity version specified
/// </summary>
/// <param name="entity"></param>
/// <param name="versionId"></param>
/// <param name="publishedVersionId"></param>
/// <param name="edited"></param>
/// <param name="editedCultures"></param>
protected void ReplacePropertyValues(TEntity entity, int versionId, int publishedVersionId, out bool edited, out HashSet<string>? editedCultures)
{
// Replace the property data.
// Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic
// and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues.
var propDataSql = SqlContext.Sql().Select("*").From<PropertyDataDto>().Where<PropertyDataDto>(x => x.VersionId == versionId).ForUpdate();
var existingPropData = Database.Fetch<PropertyDataDto>(propDataSql);
var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string? segment), PropertyDataDto>();
var existingPropDataIds = new List<int>();
foreach (var p in existingPropData)
{
existingPropDataIds.Add(p.Id);
propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p;
}
var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures);
foreach (var propertyDataDto in propertyDataDtos)
{
// Check if this already exists and update, else insert a new one
if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out var propData))
{
propertyDataDto.Id = propData.Id;
Database.Update(propertyDataDto);
}
else
{
// TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
// This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
Database.Insert(propertyDataDto);
}
// track which ones have been processed
existingPropDataIds.Remove(propertyDataDto.Id);
}
// For any remaining that haven't been processed they need to be deleted
if (existingPropDataIds.Count > 0)
{
Database.Execute(SqlContext.Sql().Delete<PropertyDataDto>().WhereIn<PropertyDataDto>(x => x.Id, existingPropDataIds));
}
}
private class NodeIdKey
{
[Column("id")]
public int NodeId { get; set; }
[Column("uniqueId")]
public Guid UniqueId { get; set; }
}
2017-12-07 16:45:25 +01:00
}
}