diff --git a/.editorconfig b/.editorconfig index eba04ad326..faf5c7766a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -282,7 +282,7 @@ dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR___ # All public/protected/protected_internal constant fields must be PascalCase # https://docs.microsoft.com/dotnet/standard/design-guidelines/field -dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal +dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal, internal, private dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group @@ -356,24 +356,13 @@ dotnet_naming_rule.parameters_rule.symbols = parameters_group dotnet_naming_rule.parameters_rule.style = camel_case_style dotnet_naming_rule.parameters_rule.severity = warning -# Private static fields use camelCase and start with s_ -dotnet_naming_symbols.private_static_field_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_static_field_symbols.required_modifiers = static, shared -dotnet_naming_symbols.private_static_field_symbols.applicable_kinds = field -dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.symbols = private_static_field_symbols -dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.style = camel_case_and_prefix_with_s_underscore_style -dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.severity = warning -dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.required_prefix = s_ -dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.capitalization = camel_case - # Instance fields use camelCase and are prefixed with '_' -dotnet_naming_symbols.private_field_symbols.applicable_accessibilities = private -dotnet_naming_symbols.private_field_symbols.applicable_kinds = field -dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.symbols = private_field_symbols -dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.style = camel_case_and_prefix_with_underscore_style -dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.severity = warning -dotnet_naming_style.camel_case_and_prefix_with_underscore_style.required_prefix = _ -dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization = camel_case +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = warning +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style +dotnet_naming_symbols.instance_fields.applicable_kinds = field +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ ########################################## # License @@ -408,4 +397,4 @@ dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -########################################## \ No newline at end of file +########################################## diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index be6f013f26..d357311dd5 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -180,8 +180,7 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase return string.Join(" || ", args.AsEnumerable()); } - public override string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, - string? referenceName = null, bool forInsert = false) + public override string GetColumn(DatabaseType dbType, string tableName, string columnName, string? columnAlias, string? referenceName = null, bool forInsert = false) { if (forInsert) { diff --git a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs index fc59d06016..4657c8a68a 100644 --- a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs @@ -8,54 +8,55 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Ensures that distributed cache events are setup and the is initialized +/// +public sealed class DatabaseServerMessengerNotificationHandler : + INotificationHandler, INotificationHandler { + private readonly IUmbracoDatabaseFactory _databaseFactory; + private readonly ILogger _logger; + private readonly IServerMessenger _messenger; + private readonly IRuntimeState _runtimeState; + /// - /// Ensures that distributed cache events are setup and the is initialized + /// Initializes a new instance of the class. /// - public sealed class DatabaseServerMessengerNotificationHandler : INotificationHandler, INotificationHandler + public DatabaseServerMessengerNotificationHandler( + IServerMessenger serverMessenger, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + IRuntimeState runtimeState) { - private readonly IServerMessenger _messenger; - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly ILogger _logger; - private readonly IRuntimeState _runtimeState; - - /// - /// Initializes a new instance of the class. - /// - public DatabaseServerMessengerNotificationHandler( - IServerMessenger serverMessenger, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - IRuntimeState runtimeState) - { - _databaseFactory = databaseFactory; - _logger = logger; - _messenger = serverMessenger; - _runtimeState = runtimeState; - } - - /// - public void Handle(UmbracoApplicationStartingNotification notification) - { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return; - } - - if (_databaseFactory.CanConnect == false) - { - _logger.LogWarning("Cannot connect to the database, distributed calls will not be enabled for this server."); - return; - } - - // Sync on startup, this will run through the messenger's initialization sequence - _messenger?.Sync(); - } - - /// - /// Clear the batch on end request - /// - public void Handle(UmbracoRequestEndNotification notification) => _messenger?.SendMessages(); + _databaseFactory = databaseFactory; + _logger = logger; + _messenger = serverMessenger; + _runtimeState = runtimeState; } + + /// + public void Handle(UmbracoApplicationStartingNotification notification) + { + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } + + if (_databaseFactory.CanConnect == false) + { + _logger.LogWarning( + "Cannot connect to the database, distributed calls will not be enabled for this server."); + return; + } + + // Sync on startup, this will run through the messenger's initialization sequence + _messenger?.Sync(); + } + + /// + /// Clear the batch on end request + /// + public void Handle(UmbracoRequestEndNotification notification) => _messenger?.SendMessages(); } diff --git a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs index b18cce9b3d..7f7f8d6784 100644 --- a/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/DefaultRepositoryCachePolicy.cs @@ -1,268 +1,273 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents the default cache policy. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// The default cache policy caches entities with a 5 minutes sliding expiration. +/// Each entity is cached individually. +/// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. +/// If options.GetAllCacheValidateCount then we check against the db when getting many entities. +/// +public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IEntity { - /// - /// Represents the default cache policy. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// The default cache policy caches entities with a 5 minutes sliding expiration. - /// Each entity is cached individually. - /// If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing. - /// If options.GetAllCacheValidateCount then we check against the db when getting many entities. - /// - public class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase - where TEntity : class, IEntity + private static readonly TEntity[] _emptyEntities = new TEntity[0]; // const + private readonly RepositoryCachePolicyOptions _options; + + public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) + : base(cache, scopeAccessor) => + _options = options ?? throw new ArgumentNullException(nameof(options)); + + protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; + + /// + public override void Create(TEntity entity, Action persistNew) { - private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const - private readonly RepositoryCachePolicyOptions _options; - - public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) - : base(cache, scopeAccessor) + if (entity == null) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + throw new ArgumentNullException(nameof(entity)); } - protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; - - protected string GetEntityCacheKey(TId? id) + try { - if (EqualityComparer.Default.Equals(id, default)) + persistNew(entity); + + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - return string.Empty; + Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); } - if (typeof(TId).IsValueType) - { - return EntityTypeCacheKey + id; - } - else - { - return EntityTypeCacheKey + id?.ToString()?.ToUpperInvariant(); - } + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + catch + { + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.Clear(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + + throw; + } + } + + /// + public override void Update(TEntity entity, Action persistUpdated) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); } - protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; - - protected virtual void InsertEntity(string cacheKey, TEntity entity) - => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); - - protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities) + try { - if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount) + persistUpdated(entity); + + // just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) { - // getting all of them, and finding nothing. - // if we can cache a zero count, cache an empty array, - // for as long as the cache is not cleared (no expiration) - Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); + Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); } - else + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + catch + { + // if an exception is thrown we need to remove the entry from cache, + // this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.Clear(GetEntityCacheKey(entity.Id)); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + + throw; + } + } + + /// + public override void Delete(TEntity entity, Action persistDeleted) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + try + { + persistDeleted(entity); + } + finally + { + // whatever happens, clear the cache + var cacheKey = GetEntityCacheKey(entity.Id); + Cache.Clear(cacheKey); + + // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.Clear(EntityTypeCacheKey); + } + } + + /// + public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) + { + var cacheKey = GetEntityCacheKey(id); + TEntity? fromCache = Cache.GetCacheItem(cacheKey); + + // if found in cache then return else fetch and cache + if (fromCache != null) + { + return fromCache; + } + + TEntity? entity = performGet(id); + + if (entity != null && entity.HasIdentity) + { + InsertEntity(cacheKey, entity); + } + + return entity; + } + + /// + public override TEntity? GetCached(TId id) + { + var cacheKey = GetEntityCacheKey(id); + return Cache.GetCacheItem(cacheKey); + } + + /// + public override bool Exists(TId id, Func performExists, Func?> performGetAll) + { + // if found in cache the return else check + var cacheKey = GetEntityCacheKey(id); + TEntity? fromCache = Cache.GetCacheItem(cacheKey); + return fromCache != null || performExists(id); + } + + /// + public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + { + if (ids?.Length > 0) + { + // try to get each entity from the cache + // if we can find all of them, return + TEntity[] entities = ids.Select(GetCached).WhereNotNull().ToArray(); + if (ids.Length.Equals(entities.Length)) { - if (entities is not null) + return entities; // no need for null checks, we are not caching nulls + } + } + else + { + // get everything we have + TEntity?[] entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey) + .ToArray(); // no need for null checks, we are not caching nulls + + if (entities.Length > 0) + { + // if some of them were in the cache... + if (_options.GetAllCacheValidateCount) { - // individually cache each item - foreach (var entity in entities) + // need to validate the count, get the actual count and return if ok + if (_options.PerformCount is not null) { - var capture = entity; - Cache.Insert(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); - } - } - } - } - - /// - public override void Create(TEntity entity, Action persistNew) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistNew(entity); - - // just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); - } - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - catch - { - // if an exception is thrown we need to remove the entry from cache, - // this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - Cache.Clear(GetEntityCacheKey(entity.Id)); - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - - throw; - } - } - - /// - public override void Update(TEntity entity, Action persistUpdated) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistUpdated(entity); - - // just to be safe, we cannot cache an item without an identity - if (entity.HasIdentity) - { - Cache.Insert(GetEntityCacheKey(entity.Id), () => entity, TimeSpan.FromMinutes(5), true); - } - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - catch - { - // if an exception is thrown we need to remove the entry from cache, - // this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - Cache.Clear(GetEntityCacheKey(entity.Id)); - - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - - throw; - } - } - - /// - public override void Delete(TEntity entity, Action persistDeleted) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistDeleted(entity); - } - finally - { - // whatever happens, clear the cache - var cacheKey = GetEntityCacheKey(entity.Id); - Cache.Clear(cacheKey); - // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(EntityTypeCacheKey); - } - } - - /// - public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) - { - var cacheKey = GetEntityCacheKey(id); - var fromCache = Cache.GetCacheItem(cacheKey); - - // if found in cache then return else fetch and cache - if (fromCache != null) - { - return fromCache; - } - - var entity = performGet(id); - - if (entity != null && entity.HasIdentity) - { - InsertEntity(cacheKey, entity); - } - - return entity; - } - - /// - public override TEntity? GetCached(TId id) - { - var cacheKey = GetEntityCacheKey(id); - return Cache.GetCacheItem(cacheKey); - } - - /// - public override bool Exists(TId id, Func performExists, Func?> performGetAll) - { - // if found in cache the return else check - var cacheKey = GetEntityCacheKey(id); - var fromCache = Cache.GetCacheItem(cacheKey); - return fromCache != null || performExists(id); - } - - /// - public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) - { - if (ids?.Length > 0) - { - // try to get each entity from the cache - // if we can find all of them, return - var entities = ids.Select(GetCached).WhereNotNull().ToArray(); - if (ids.Length.Equals(entities.Length)) - return entities; // no need for null checks, we are not caching nulls - } - else - { - // get everything we have - var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey)? - .ToArray(); // no need for null checks, we are not caching nulls - - if (entities?.Length > 0) - { - // if some of them were in the cache... - if (_options.GetAllCacheValidateCount) - { - // need to validate the count, get the actual count and return if ok - if (_options.PerformCount is not null) + var totalCount = _options.PerformCount(); + if (entities.Length == totalCount) { - var totalCount = _options.PerformCount(); - if (entities.Length == totalCount) - return entities.WhereNotNull().ToArray(); + return entities.WhereNotNull().ToArray(); } } - else - { - // no need to validate, just return what we have and assume it's all there is - return entities.WhereNotNull().ToArray(); - } } - else if (_options.GetAllCacheAllowZeroCount) + else { - // if none of them were in the cache - // and we allow zero count - check for the special (empty) entry - var empty = Cache.GetCacheItem(EntityTypeCacheKey); - if (empty != null) return empty; + // no need to validate, just return what we have and assume it's all there is + return entities.WhereNotNull().ToArray(); + } + } + else if (_options.GetAllCacheAllowZeroCount) + { + // if none of them were in the cache + // and we allow zero count - check for the special (empty) entry + TEntity[]? empty = Cache.GetCacheItem(EntityTypeCacheKey); + if (empty != null) + { + return empty; } } - - // cache failed, get from repo and cache - var repoEntities = performGetAll(ids)? - .WhereNotNull() // exclude nulls! - .Where(x => x.HasIdentity) // be safe, though would be weird... - .ToArray(); - - // note: if empty & allow zero count, will cache a special (empty) entry - InsertEntities(ids, repoEntities); - - return repoEntities ?? Array.Empty(); } - /// - public override void ClearAll() + // cache failed, get from repo and cache + TEntity[]? repoEntities = performGetAll(ids)? + .WhereNotNull() // exclude nulls! + .Where(x => x.HasIdentity) // be safe, though would be weird... + .ToArray(); + + // note: if empty & allow zero count, will cache a special (empty) entry + InsertEntities(ids, repoEntities); + + return repoEntities ?? Array.Empty(); + } + + /// + public override void ClearAll() => Cache.ClearByKey(EntityTypeCacheKey); + + protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; + + protected string GetEntityCacheKey(TId? id) + { + if (EqualityComparer.Default.Equals(id, default)) { - Cache.ClearByKey(EntityTypeCacheKey); + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return EntityTypeCacheKey + id; + } + + return EntityTypeCacheKey + id?.ToString()?.ToUpperInvariant(); + } + + protected virtual void InsertEntity(string cacheKey, TEntity entity) + => Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); + + protected virtual void InsertEntities(TId[]? ids, TEntity[]? entities) + { + if (ids?.Length == 0 && entities?.Length == 0 && _options.GetAllCacheAllowZeroCount) + { + // getting all of them, and finding nothing. + // if we can cache a zero count, cache an empty array, + // for as long as the cache is not cleared (no expiration) + Cache.Insert(EntityTypeCacheKey, () => _emptyEntities); + } + else + { + if (entities is not null) + { + // individually cache each item + foreach (TEntity entity in entities) + { + TEntity capture = entity; + Cache.Insert(GetEntityCacheKey(entity.Id), () => capture, TimeSpan.FromMinutes(5), true); + } + } } } } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs index 6e6f549b03..11119aaf66 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs @@ -1,365 +1,341 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; -using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; +public class DistributedCacheBinder : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { + private readonly DistributedCache _distributedCache; + /// - /// Default implementation. + /// Initializes a new instance of the class. /// - public class DistributedCacheBinder : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + public DistributedCacheBinder(DistributedCache distributedCache) { - private readonly DistributedCache _distributedCache; - - /// - /// Initializes a new instance of the class. - /// - public DistributedCacheBinder(DistributedCache distributedCache) - { - _distributedCache = distributedCache; - } - - #region PublicAccessService - - public void Handle(PublicAccessEntrySavedNotification notification) - { - _distributedCache.RefreshPublicAccess(); - } - - public void Handle(PublicAccessEntryDeletedNotification notification) - { - _distributedCache.RefreshPublicAccess(); - - } - - #endregion - - #region ContentService - - /// - /// Handles cache refreshing for when content is copied - /// - /// - /// - /// - /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the - /// case then we need to clear all user permissions cache. - /// - private void ContentService_Copied(IContentService sender, CopyEventArgs e) - { - } - - - public void Handle(ContentTreeChangeNotification notification) - { - _distributedCache.RefreshContentCache(notification.Changes.ToArray()); - } - - //private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs e) - //{ - // _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray()); - //} - - //private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs e) - //{ - // _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); - //} - - #endregion - - #region LocalizationService / Dictionary - public void Handle(DictionaryItemSavedNotification notification) - { - foreach (IDictionaryItem entity in notification.SavedEntities) - { - _distributedCache.RefreshDictionaryCache(entity.Id); - } - } - - public void Handle(DictionaryItemDeletedNotification notification) - { - foreach (IDictionaryItem entity in notification.DeletedEntities) - { - _distributedCache.RemoveDictionaryCache(entity.Id); - } - } - - #endregion - - #region DataTypeService - - public void Handle(DataTypeSavedNotification notification) - { - foreach (IDataType entity in notification.SavedEntities) - { - _distributedCache.RefreshDataTypeCache(entity); - } - _distributedCache.RefreshValueEditorCache(notification.SavedEntities); - } - - public void Handle(DataTypeDeletedNotification notification) - { - foreach (IDataType entity in notification.DeletedEntities) - { - _distributedCache.RemoveDataTypeCache(entity); - } - _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); - } - - #endregion - - #region DomainService - - public void Handle(DomainSavedNotification notification) - { - foreach (IDomain entity in notification.SavedEntities) - { - _distributedCache.RefreshDomainCache(entity); - } - } - - public void Handle(DomainDeletedNotification notification) - { - foreach (IDomain entity in notification.DeletedEntities) - { - _distributedCache.RemoveDomainCache(entity); - } - } - - #endregion - - #region LocalizationService / Language - - /// - /// Fires when a language is deleted - /// - /// - public void Handle(LanguageDeletedNotification notification) - { - foreach (ILanguage entity in notification.DeletedEntities) - { - _distributedCache.RemoveLanguageCache(entity); - } - } - - /// - /// Fires when a language is saved - /// - /// - public void Handle(LanguageSavedNotification notification) - { - foreach (ILanguage entity in notification.SavedEntities) - { - _distributedCache.RefreshLanguageCache(entity); - } - } - - #endregion - - #region Content|Media|MemberTypeService - - public void Handle(ContentTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - public void Handle(MediaTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - public void Handle(MemberTypeChangedNotification notification) => - _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); - - #endregion - - #region UserService - - public void Handle(UserSavedNotification notification) - { - foreach (IUser entity in notification.SavedEntities) - { - _distributedCache.RefreshUserCache(entity.Id); - } - } - - public void Handle(UserDeletedNotification notification) - { - foreach (IUser entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserCache(entity.Id); - } - } - - public void Handle(UserGroupWithUsersSavedNotification notification) - { - foreach (UserGroupWithUsers entity in notification.SavedEntities) - { - _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); - } - } - - public void Handle(UserGroupDeletedNotification notification) - { - foreach (IUserGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveUserGroupCache(entity.Id); - } - } - - #endregion - - #region FileService - - /// - /// Removes cache for template - /// - /// - public void Handle(TemplateDeletedNotification notification) - { - foreach (ITemplate entity in notification.DeletedEntities) - { - _distributedCache.RemoveTemplateCache(entity.Id); - } - } - - /// - /// Refresh cache for template - /// - /// - public void Handle(TemplateSavedNotification notification) - { - foreach (ITemplate entity in notification.SavedEntities) - { - _distributedCache.RefreshTemplateCache(entity.Id); - } - } - - #endregion - - #region MacroService - - public void Handle(MacroDeletedNotification notification) - { - foreach (IMacro entity in notification.DeletedEntities) - { - _distributedCache.RemoveMacroCache(entity); - } - } - - public void Handle(MacroSavedNotification notification) - { - foreach (IMacro entity in notification.SavedEntities) - { - _distributedCache.RefreshMacroCache(entity); - } - } - - #endregion - - #region MediaService - - public void Handle(MediaTreeChangeNotification notification) - { - _distributedCache.RefreshMediaCache(notification.Changes.ToArray()); - } - - #endregion - - #region MemberService - - public void Handle(MemberDeletedNotification notification) - { - _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); - } - - public void Handle(MemberSavedNotification notification) - { - _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); - } - - #endregion - - #region MemberGroupService - - /// - /// Fires when a member group is deleted - /// - /// - public void Handle(MemberGroupDeletedNotification notification) - { - foreach (IMemberGroup entity in notification.DeletedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } - } - - /// - /// Fires when a member group is saved - /// - /// - public void Handle(MemberGroupSavedNotification notification) - { - foreach (IMemberGroup entity in notification.SavedEntities) - { - _distributedCache.RemoveMemberGroupCache(entity.Id); - } - } - - #endregion - - #region RelationType - - public void Handle(RelationTypeSavedNotification notification) - { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.SavedEntities) - { - dc.RefreshRelationTypeCache(entity.Id); - } - } - - public void Handle(RelationTypeDeletedNotification notification) - { - DistributedCache dc = _distributedCache; - foreach (IRelationType entity in notification.DeletedEntities) - { - dc.RemoveRelationTypeCache(entity.Id); - } - } - - #endregion + _distributedCache = distributedCache; } + + #region PublicAccessService + + public void Handle(PublicAccessEntrySavedNotification notification) + { + _distributedCache.RefreshPublicAccess(); + } + + public void Handle(PublicAccessEntryDeletedNotification notification) => _distributedCache.RefreshPublicAccess(); + + #endregion + + #region ContentService + + public void Handle(ContentTreeChangeNotification notification) + { + _distributedCache.RefreshContentCache(notification.Changes.ToArray()); + } + + // private void ContentService_SavedBlueprint(IContentService sender, SaveEventArgs e) + // { + // _distributedCache.RefreshUnpublishedPageCache(e.SavedEntities.ToArray()); + // } + + // private void ContentService_DeletedBlueprint(IContentService sender, DeleteEventArgs e) + // { + // _distributedCache.RemoveUnpublishedPageCache(e.DeletedEntities.ToArray()); + // } + #endregion + + #region LocalizationService / Dictionary + public void Handle(DictionaryItemSavedNotification notification) + { + foreach (IDictionaryItem entity in notification.SavedEntities) + { + _distributedCache.RefreshDictionaryCache(entity.Id); + } + } + + public void Handle(DictionaryItemDeletedNotification notification) + { + foreach (IDictionaryItem entity in notification.DeletedEntities) + { + _distributedCache.RemoveDictionaryCache(entity.Id); + } + } + + #endregion + + #region DataTypeService + + public void Handle(DataTypeSavedNotification notification) + { + foreach (IDataType entity in notification.SavedEntities) + { + _distributedCache.RefreshDataTypeCache(entity); + } + + _distributedCache.RefreshValueEditorCache(notification.SavedEntities); + } + + public void Handle(DataTypeDeletedNotification notification) + { + foreach (IDataType entity in notification.DeletedEntities) + { + _distributedCache.RemoveDataTypeCache(entity); + } + + _distributedCache.RefreshValueEditorCache(notification.DeletedEntities); + } + + #endregion + + #region DomainService + + public void Handle(DomainSavedNotification notification) + { + foreach (IDomain entity in notification.SavedEntities) + { + _distributedCache.RefreshDomainCache(entity); + } + } + + public void Handle(DomainDeletedNotification notification) + { + foreach (IDomain entity in notification.DeletedEntities) + { + _distributedCache.RemoveDomainCache(entity); + } + } + + #endregion + + #region LocalizationService / Language + + /// + /// Fires when a language is deleted + /// + /// + public void Handle(LanguageDeletedNotification notification) + { + foreach (ILanguage entity in notification.DeletedEntities) + { + _distributedCache.RemoveLanguageCache(entity); + } + } + + /// + /// Fires when a language is saved + /// + /// + public void Handle(LanguageSavedNotification notification) + { + foreach (ILanguage entity in notification.SavedEntities) + { + _distributedCache.RefreshLanguageCache(entity); + } + } + + #endregion + + #region Content|Media|MemberTypeService + + public void Handle(ContentTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + + public void Handle(MediaTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + + public void Handle(MemberTypeChangedNotification notification) => + _distributedCache.RefreshContentTypeCache(notification.Changes.ToArray()); + + #endregion + + #region UserService + + public void Handle(UserSavedNotification notification) + { + foreach (IUser entity in notification.SavedEntities) + { + _distributedCache.RefreshUserCache(entity.Id); + } + } + + public void Handle(UserDeletedNotification notification) + { + foreach (IUser entity in notification.DeletedEntities) + { + _distributedCache.RemoveUserCache(entity.Id); + } + } + + public void Handle(UserGroupWithUsersSavedNotification notification) + { + foreach (UserGroupWithUsers entity in notification.SavedEntities) + { + _distributedCache.RefreshUserGroupCache(entity.UserGroup.Id); + } + } + + public void Handle(UserGroupDeletedNotification notification) + { + foreach (IUserGroup entity in notification.DeletedEntities) + { + _distributedCache.RemoveUserGroupCache(entity.Id); + } + } + + #endregion + + #region FileService + + /// + /// Removes cache for template + /// + /// + public void Handle(TemplateDeletedNotification notification) + { + foreach (ITemplate entity in notification.DeletedEntities) + { + _distributedCache.RemoveTemplateCache(entity.Id); + } + } + + /// + /// Refresh cache for template + /// + /// + public void Handle(TemplateSavedNotification notification) + { + foreach (ITemplate entity in notification.SavedEntities) + { + _distributedCache.RefreshTemplateCache(entity.Id); + } + } + + #endregion + + #region MacroService + + public void Handle(MacroDeletedNotification notification) + { + foreach (IMacro entity in notification.DeletedEntities) + { + _distributedCache.RemoveMacroCache(entity); + } + } + + public void Handle(MacroSavedNotification notification) + { + foreach (IMacro entity in notification.SavedEntities) + { + _distributedCache.RefreshMacroCache(entity); + } + } + + #endregion + + #region MediaService + + public void Handle(MediaTreeChangeNotification notification) + { + _distributedCache.RefreshMediaCache(notification.Changes.ToArray()); + } + + #endregion + + #region MemberService + + public void Handle(MemberDeletedNotification notification) + { + _distributedCache.RemoveMemberCache(notification.DeletedEntities.ToArray()); + } + + public void Handle(MemberSavedNotification notification) + { + _distributedCache.RefreshMemberCache(notification.SavedEntities.ToArray()); + } + + #endregion + + #region MemberGroupService + + /// + /// Fires when a member group is deleted + /// + /// + public void Handle(MemberGroupDeletedNotification notification) + { + foreach (IMemberGroup entity in notification.DeletedEntities) + { + _distributedCache.RemoveMemberGroupCache(entity.Id); + } + } + + /// + /// Fires when a member group is saved + /// + /// + public void Handle(MemberGroupSavedNotification notification) + { + foreach (IMemberGroup entity in notification.SavedEntities) + { + _distributedCache.RemoveMemberGroupCache(entity.Id); + } + } + + #endregion + + #region RelationType + + public void Handle(RelationTypeSavedNotification notification) + { + DistributedCache dc = _distributedCache; + foreach (IRelationType entity in notification.SavedEntities) + { + dc.RefreshRelationTypeCache(entity.Id); + } + } + + public void Handle(RelationTypeDeletedNotification notification) + { + DistributedCache dc = _distributedCache; + foreach (IRelationType entity in notification.DeletedEntities) + { + dc.RemoveRelationTypeCache(entity.Id); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs index ceac767a8c..dfa7d9b605 100644 --- a/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Infrastructure/Cache/DistributedCacheExtensions.cs @@ -1,327 +1,376 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services.Changes; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for . +/// +public static class DistributedCacheExtensions { - /// - /// Extension methods for . - /// - public static class DistributedCacheExtensions + #region PublicAccessCache + + public static void RefreshPublicAccess(this DistributedCache dc) { - #region PublicAccessCache - - public static void RefreshPublicAccess(this DistributedCache dc) - { - dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); - } - - #endregion - - #region User cache - - public static void RemoveUserCache(this DistributedCache dc, int userId) - { - dc.Remove(UserCacheRefresher.UniqueId, userId); - } - - public static void RefreshUserCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserCacheRefresher.UniqueId, userId); - } - - public static void RefreshAllUserCache(this DistributedCache dc) - { - dc.RefreshAll(UserCacheRefresher.UniqueId); - } - - #endregion - - #region User group cache - - public static void RemoveUserGroupCache(this DistributedCache dc, int userId) - { - dc.Remove(UserGroupCacheRefresher.UniqueId, userId); - } - - public static void RefreshUserGroupCache(this DistributedCache dc, int userId) - { - dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); - } - - public static void RefreshAllUserGroupCache(this DistributedCache dc) - { - dc.RefreshAll(UserGroupCacheRefresher.UniqueId); - } - - #endregion - - #region TemplateCache - - public static void RefreshTemplateCache(this DistributedCache dc, int templateId) - { - dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); - } - - public static void RemoveTemplateCache(this DistributedCache dc, int templateId) - { - dc.Remove(TemplateCacheRefresher.UniqueId, templateId); - } - - #endregion - - #region DictionaryCache - - public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } - - public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) - { - dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); - } - - #endregion - - #region DataTypeCache - - public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) return; - var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } - - public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) - { - if (dataType == null) return; - var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) }; - dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region ValueEditorCache - - public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) - { - if (dataTypes is null) - { - return; - } - - var payloads = dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); - dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region ContentCache - - public static void RefreshAllContentCache(this DistributedCache dc) - { - var payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - - // note: refresh all content cache does refresh content types too - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); - - dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region MemberCache - - public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) - { - if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); - } - - public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) - { - if (members.Length == 0) return; - dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); - } - - #endregion - - #region MemberGroupCache - - public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); - } - - public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) - { - dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); - } - - #endregion - - #region MediaCache - - public static void RefreshAllMediaCache(this DistributedCache dc) - { - var payloads = new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; - - // note: refresh all media cache does refresh content types too - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } - - public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); - - dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Published Snapshot - - public static void RefreshAllPublishedSnapshot(this DistributedCache dc) - { - // note: refresh all content & media caches does refresh content types too - dc.RefreshAllContentCache(); - dc.RefreshAllMediaCache(); - dc.RefreshAllDomainCache(); - } - - #endregion - - #region MacroCache - - public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) return; - var payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); - } - - public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) - { - if (macro == null) return; - var payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; - dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Content/Media/Member type cache - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof (IContentType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) - { - if (changes.Length == 0) return; - - var payloads = changes - .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes)); - - dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Domain Cache - - public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) return; - var payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Refresh) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) - { - if (domain == null) return; - var payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Remove) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - public static void RefreshAllDomainCache(this DistributedCache dc) - { - var payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; - dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); - } - - #endregion - - #region Language Cache - - public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) return; - - var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, - language.WasPropertyDirty(nameof(ILanguage.IsoCode)) - ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture - : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); - - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); - } - - public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) - { - if (language == null) return; - - var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); - dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); - } - - #endregion - - #region Relation type cache - - public static void RefreshRelationTypeCache(this DistributedCache dc, int id) - { - dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); - } - - public static void RemoveRelationTypeCache(this DistributedCache dc, int id) - { - dc.Remove(RelationTypeCacheRefresher.UniqueId, id); - } - - #endregion - - + dc.RefreshAll(PublicAccessCacheRefresher.UniqueId); } + + #endregion + + #region User cache + + public static void RemoveUserCache(this DistributedCache dc, int userId) + { + dc.Remove(UserCacheRefresher.UniqueId, userId); + } + + public static void RefreshUserCache(this DistributedCache dc, int userId) + { + dc.Refresh(UserCacheRefresher.UniqueId, userId); + } + + public static void RefreshAllUserCache(this DistributedCache dc) + { + dc.RefreshAll(UserCacheRefresher.UniqueId); + } + + #endregion + + #region User group cache + + public static void RemoveUserGroupCache(this DistributedCache dc, int userId) + { + dc.Remove(UserGroupCacheRefresher.UniqueId, userId); + } + + public static void RefreshUserGroupCache(this DistributedCache dc, int userId) + { + dc.Refresh(UserGroupCacheRefresher.UniqueId, userId); + } + + public static void RefreshAllUserGroupCache(this DistributedCache dc) + { + dc.RefreshAll(UserGroupCacheRefresher.UniqueId); + } + + #endregion + + #region TemplateCache + + public static void RefreshTemplateCache(this DistributedCache dc, int templateId) + { + dc.Refresh(TemplateCacheRefresher.UniqueId, templateId); + } + + public static void RemoveTemplateCache(this DistributedCache dc, int templateId) + { + dc.Remove(TemplateCacheRefresher.UniqueId, templateId); + } + + #endregion + + #region DictionaryCache + + public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) + { + dc.Refresh(DictionaryCacheRefresher.UniqueId, dictionaryItemId); + } + + public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) + { + dc.Remove(DictionaryCacheRefresher.UniqueId, dictionaryItemId); + } + + #endregion + + #region DataTypeCache + + public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType) + { + if (dataType == null) + { + return; + } + + DataTypeCacheRefresher.JsonPayload[] payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) }; + dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); + } + + public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType) + { + if (dataType == null) + { + return; + } + + DataTypeCacheRefresher.JsonPayload[] payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) }; + dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region ValueEditorCache + + public static void RefreshValueEditorCache(this DistributedCache dc, IEnumerable dataTypes) + { + if (dataTypes is null) + { + return; + } + + IEnumerable payloads = dataTypes.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)); + dc.RefreshByPayload(ValueEditorCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region ContentCache + + public static void RefreshAllContentCache(this DistributedCache dc) + { + ContentCacheRefresher.JsonPayload[] payloads = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + + // note: refresh all content cache does refresh content types too + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } + + public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region MemberCache + + public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) + { + if (members.Length == 0) + { + return; + } + + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false))); + } + + public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) + { + if (members.Length == 0) + { + return; + } + + dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true))); + } + + #endregion + + #region MemberGroupCache + + public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) + { + dc.Refresh(MemberGroupCacheRefresher.UniqueId, memberGroupId); + } + + public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) + { + dc.Remove(MemberGroupCacheRefresher.UniqueId, memberGroupId); + } + + #endregion + + #region MediaCache + + public static void RefreshAllMediaCache(this DistributedCache dc) + { + MediaCacheRefresher.JsonPayload[] payloads = new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + + // note: refresh all media cache does refresh content types too + dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } + + public static void RefreshMediaCache(this DistributedCache dc, TreeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new MediaCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes)); + + dc.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Published Snapshot + + public static void RefreshAllPublishedSnapshot(this DistributedCache dc) + { + // note: refresh all content & media caches does refresh content types too + dc.RefreshAllContentCache(); + dc.RefreshAllMediaCache(); + dc.RefreshAllDomainCache(); + } + + #endregion + + #region MacroCache + + public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) + { + return; + } + + MacroCacheRefresher.JsonPayload[] payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; + dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + } + + public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) + { + if (macro == null) + { + return; + } + + MacroCacheRefresher.JsonPayload[] payloads = new[] { new MacroCacheRefresher.JsonPayload(macro.Id, macro.Alias) }; + dc.RefreshByPayload(MacroCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Content/Media/Member type cache + + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IContentType).Name, x.Item.Id, x.ChangeTypes)); + + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } + + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMediaType).Name, x.Item.Id, x.ChangeTypes)); + + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } + + public static void RefreshContentTypeCache(this DistributedCache dc, ContentTypeChange[] changes) + { + if (changes.Length == 0) + { + return; + } + + IEnumerable payloads = changes + .Select(x => new ContentTypeCacheRefresher.JsonPayload(typeof(IMemberType).Name, x.Item.Id, x.ChangeTypes)); + + dc.RefreshByPayload(ContentTypeCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Domain Cache + + public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) + { + return; + } + + DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Refresh) }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } + + public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) + { + if (domain == null) + { + return; + } + + DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(domain.Id, DomainChangeTypes.Remove) }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } + + public static void RefreshAllDomainCache(this DistributedCache dc) + { + DomainCacheRefresher.JsonPayload[] payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; + dc.RefreshByPayload(DomainCacheRefresher.UniqueId, payloads); + } + + #endregion + + #region Language Cache + + public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) + { + return; + } + + var payload = new LanguageCacheRefresher.JsonPayload( + language.Id, + language.IsoCode, + language.WasPropertyDirty(nameof(ILanguage.IsoCode)) ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); + + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); + } + + public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) + { + if (language == null) + { + return; + } + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); + } + + #endregion + + #region Relation type cache + + public static void RefreshRelationTypeCache(this DistributedCache dc, int id) + { + dc.Refresh(RelationTypeCacheRefresher.UniqueId, id); + } + + public static void RemoveRelationTypeCache(this DistributedCache dc, int id) + { + dc.Remove(RelationTypeCacheRefresher.UniqueId, id); + } + + #endregion + } diff --git a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs index 34cdd3ce0c..91ae85c84f 100644 --- a/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/FullDataSetRepositoryCachePolicy.cs @@ -1,187 +1,191 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// Represents a caching policy that caches the entire entities set as a single collection. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// Caches the entire set of entities as a single collection. +/// +/// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, +/// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to +/// keep as a whole in memory. +/// +/// +internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IEntity { - /// - /// Represents a caching policy that caches the entire entities set as a single collection. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// Caches the entire set of entities as a single collection. - /// Used by Content-, Media- and MemberTypeRepository, DataTypeRepository, DomainRepository, - /// LanguageRepository, PublicAccessRepository, TemplateRepository... things that make sense to - /// keep as a whole in memory. - /// - internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase - where TEntity : class, IEntity + protected static readonly TId[] EmptyIds = new TId[0]; // const + private readonly Func _entityGetId; + private readonly bool _expires; + + public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, Func entityGetId, bool expires) + : base(cache, scopeAccessor) { - private readonly Func _entityGetId; - private readonly bool _expires; + _entityGetId = entityGetId; + _expires = expires; + } - public FullDataSetRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, Func entityGetId, bool expires) - : base(cache, scopeAccessor) + /// + public override void Create(TEntity entity, Action persistNew) + { + if (entity == null) { - _entityGetId = entityGetId; - _expires = expires; + throw new ArgumentNullException(nameof(entity)); } - protected static readonly TId[] EmptyIds = new TId[0]; // const - - protected string GetEntityTypeCacheKey() + try { - return $"uRepo_{typeof (TEntity).Name}_"; + persistNew(entity); } - - protected void InsertEntities(TEntity[]? entities) + finally { - if (entities is null) - { - return; - } - - // cache is expected to be a deep-cloning cache ie it deep-clones whatever is - // IDeepCloneable when it goes in, and out. it also resets dirty properties, - // making sure that no 'dirty' entity is cached. - // - // this policy is caching the entire list of entities. to ensure that entities - // are properly deep-clones when cached, it uses a DeepCloneableList. however, - // we don't want to deep-clone *each* entity in the list when fetching it from - // cache as that would not be efficient for Get(id). so the DeepCloneableList is - // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, - // and then will *not* clone when retrieving. - - var key = GetEntityTypeCacheKey(); - - if (_expires) - { - Cache.Insert(key, () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); - } - else - { - Cache.Insert(key, () => new DeepCloneableList(entities)); - } - } - - /// - public override void Create(TEntity entity, Action persistNew) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistNew(entity); - } - finally - { - ClearAll(); - } - } - - /// - public override void Update(TEntity entity, Action persistUpdated) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistUpdated(entity); - } - finally - { - ClearAll(); - } - } - - /// - public override void Delete(TEntity entity, Action persistDeleted) - { - if (entity == null) throw new ArgumentNullException(nameof(entity)); - - try - { - persistDeleted(entity); - } - finally - { - ClearAll(); - } - } - - /// - public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) - { - // get all from the cache, then look for the entity - var all = GetAllCached(performGetAll); - var entity = all.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); - - // see note in InsertEntities - what we get here is the original - // cached entity, not a clone, so we need to manually ensure it is deep-cloned. - return (TEntity?)entity?.DeepClone(); - } - - /// - public override TEntity? GetCached(TId id) - { - // get all from the cache -- and only the cache, then look for the entity - var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); - var entity = all?.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); - - // see note in InsertEntities - what we get here is the original - // cached entity, not a clone, so we need to manually ensure it is deep-cloned. - return (TEntity?) entity?.DeepClone(); - } - - /// - public override bool Exists(TId id, Func performExits, Func?> performGetAll) - { - // get all as one set, then look for the entity - var all = GetAllCached(performGetAll); - return all.Any(x => _entityGetId(x)?.Equals(id) ?? false); - } - - /// - public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) - { - // get all as one set, from cache if possible, else repo - var all = GetAllCached(performGetAll); - - // if ids have been specified, filter - if (ids?.Length > 0) all = all.Where(x => ids.Contains(_entityGetId(x))); - - // and return - // see note in SetCacheActionToInsertEntities - what we get here is the original - // cached entities, not clones, so we need to manually ensure they are deep-cloned. - return all.Select(x => (TEntity) x.DeepClone()).ToArray(); - } - - // does NOT clone anything, so be nice with the returned values - internal IEnumerable GetAllCached(Func?> performGetAll) - { - // try the cache first - var all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); - if (all != null) return all.ToArray(); - - // else get from repo and cache - var entities = performGetAll(EmptyIds)?.WhereNotNull().ToArray(); - InsertEntities(entities); // may be an empty array... - return entities ?? Enumerable.Empty(); - } - - /// - public override void ClearAll() - { - Cache.Clear(GetEntityTypeCacheKey()); + ClearAll(); } } + + protected string GetEntityTypeCacheKey() => $"uRepo_{typeof(TEntity).Name}_"; + + protected void InsertEntities(TEntity[]? entities) + { + if (entities is null) + { + return; + } + + // cache is expected to be a deep-cloning cache ie it deep-clones whatever is + // IDeepCloneable when it goes in, and out. it also resets dirty properties, + // making sure that no 'dirty' entity is cached. + // + // this policy is caching the entire list of entities. to ensure that entities + // are properly deep-clones when cached, it uses a DeepCloneableList. however, + // we don't want to deep-clone *each* entity in the list when fetching it from + // cache as that would not be efficient for Get(id). so the DeepCloneableList is + // set to ListCloneBehavior.CloneOnce ie it will clone *once* when inserting, + // and then will *not* clone when retrieving. + var key = GetEntityTypeCacheKey(); + + if (_expires) + { + Cache.Insert(key, () => new DeepCloneableList(entities), TimeSpan.FromMinutes(5), true); + } + else + { + Cache.Insert(key, () => new DeepCloneableList(entities)); + } + } + + /// + public override void Update(TEntity entity, Action persistUpdated) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + try + { + persistUpdated(entity); + } + finally + { + ClearAll(); + } + } + + /// + public override void Delete(TEntity entity, Action persistDeleted) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + try + { + persistDeleted(entity); + } + finally + { + ClearAll(); + } + } + + /// + public override TEntity? Get(TId? id, Func performGet, Func?> performGetAll) + { + // get all from the cache, then look for the entity + IEnumerable all = GetAllCached(performGetAll); + TEntity? entity = all.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); + + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return (TEntity?)entity?.DeepClone(); + } + + /// + public override TEntity? GetCached(TId id) + { + // get all from the cache -- and only the cache, then look for the entity + DeepCloneableList? all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + TEntity? entity = all?.FirstOrDefault(x => _entityGetId(x)?.Equals(id) ?? false); + + // see note in InsertEntities - what we get here is the original + // cached entity, not a clone, so we need to manually ensure it is deep-cloned. + return (TEntity?)entity?.DeepClone(); + } + + /// + public override bool Exists(TId id, Func performExits, Func?> performGetAll) + { + // get all as one set, then look for the entity + IEnumerable all = GetAllCached(performGetAll); + return all.Any(x => _entityGetId(x)?.Equals(id) ?? false); + } + + /// + public override TEntity[] GetAll(TId[]? ids, Func?> performGetAll) + { + // get all as one set, from cache if possible, else repo + IEnumerable all = GetAllCached(performGetAll); + + // if ids have been specified, filter + if (ids?.Length > 0) + { + all = all.Where(x => ids.Contains(_entityGetId(x))); + } + + // and return + // see note in SetCacheActionToInsertEntities - what we get here is the original + // cached entities, not clones, so we need to manually ensure they are deep-cloned. + return all.Select(x => (TEntity)x.DeepClone()).ToArray(); + } + + /// + public override void ClearAll() => Cache.Clear(GetEntityTypeCacheKey()); + + // does NOT clone anything, so be nice with the returned values + internal IEnumerable GetAllCached(Func?> performGetAll) + { + // try the cache first + DeepCloneableList? all = Cache.GetCacheItem>(GetEntityTypeCacheKey()); + if (all != null) + { + return all.ToArray(); + } + + // else get from repo and cache + TEntity[]? entities = performGetAll(EmptyIds)?.WhereNotNull().ToArray(); + InsertEntities(entities); // may be an empty array... + return entities ?? Enumerable.Empty(); + } } diff --git a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs index 900ff02921..7a43071b81 100644 --- a/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs +++ b/src/Umbraco.Infrastructure/Cache/RepositoryCachePolicyBase.cs @@ -1,72 +1,71 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Cache +namespace Umbraco.Cms.Core.Cache; + +/// +/// A base class for repository cache policies. +/// +/// The type of the entity. +/// The type of the identifier. +public abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy + where TEntity : class, IEntity { - /// - /// A base class for repository cache policies. - /// - /// The type of the entity. - /// The type of the identifier. - public abstract class RepositoryCachePolicyBase : IRepositoryCachePolicy - where TEntity : class, IEntity + private readonly IAppPolicyCache _globalCache; + private readonly IScopeAccessor _scopeAccessor; + + protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor) { - private readonly IAppPolicyCache _globalCache; - private readonly IScopeAccessor _scopeAccessor; + _globalCache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); + _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + } - protected RepositoryCachePolicyBase(IAppPolicyCache globalCache, IScopeAccessor scopeAccessor) + protected IAppPolicyCache Cache + { + get { - _globalCache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); - _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - } - - protected IAppPolicyCache Cache - { - get + IScope? ambientScope = _scopeAccessor.AmbientScope; + switch (ambientScope?.RepositoryCacheMode) { - var ambientScope = _scopeAccessor.AmbientScope; - switch (ambientScope?.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - return _globalCache; - case RepositoryCacheMode.Scoped: - return ambientScope.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.None: - return NoAppCache.Instance; - default: - throw new NotSupportedException($"Repository cache mode {ambientScope?.RepositoryCacheMode} is not supported."); - } + case RepositoryCacheMode.Default: + return _globalCache; + case RepositoryCacheMode.Scoped: + return ambientScope.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.None: + return NoAppCache.Instance; + default: + throw new NotSupportedException( + $"Repository cache mode {ambientScope?.RepositoryCacheMode} is not supported."); } } - - /// - public abstract TEntity? Get(TId? id, Func performGet, Func?> performGetAll); - - /// - public abstract TEntity? GetCached(TId id); - - /// - public abstract bool Exists(TId id, Func performExists, Func?> performGetAll); - - /// - public abstract void Create(TEntity entity, Action persistNew); - - /// - public abstract void Update(TEntity entity, Action persistUpdated); - - /// - public abstract void Delete(TEntity entity, Action persistDeleted); - - /// - public abstract TEntity[] GetAll(TId[]? ids, Func?> performGetAll); - - /// - public abstract void ClearAll(); } + + /// + public abstract TEntity? Get(TId? id, Func performGet, Func?> performGetAll); + + /// + public abstract TEntity? GetCached(TId id); + + /// + public abstract bool Exists(TId id, Func performExists, Func?> performGetAll); + + /// + public abstract void Create(TEntity entity, Action persistNew); + + /// + public abstract void Update(TEntity entity, Action persistUpdated); + + /// + public abstract void Delete(TEntity entity, Action persistDeleted); + + /// + public abstract TEntity[] GetAll(TId[]? ids, Func?> performGetAll); + + /// + public abstract void ClearAll(); } diff --git a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs index d23960c903..16079d059a 100644 --- a/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs +++ b/src/Umbraco.Infrastructure/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -2,31 +2,32 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Core.Cache -{ - /// - /// Represents a special policy that does not cache the result of GetAll. - /// - /// The type of the entity. - /// The type of the identifier. - /// - /// Overrides the default repository cache policy and does not writes the result of GetAll - /// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. - /// Used by DictionaryRepository. - /// - internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy - where TEntity : class, IEntity - { - public SingleItemsOnlyRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) - : base(cache, scopeAccessor, options) - { } +namespace Umbraco.Cms.Core.Cache; - protected override void InsertEntities(TId[]? ids, TEntity[]? entities) - { - // nop - } +/// +/// Represents a special policy that does not cache the result of GetAll. +/// +/// The type of the entity. +/// The type of the identifier. +/// +/// +/// Overrides the default repository cache policy and does not writes the result of GetAll +/// to cache, but only the result of individual Gets. It does read the cache for GetAll, though. +/// +/// Used by DictionaryRepository. +/// +internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy + where TEntity : class, IEntity +{ + public SingleItemsOnlyRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) + : base(cache, scopeAccessor, options) + { + } + + protected override void InsertEntities(TId[]? ids, TEntity[]? entities) + { + // nop } } diff --git a/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs b/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs index fe21141636..09cd2282da 100644 --- a/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs +++ b/src/Umbraco.Infrastructure/Configuration/NCronTabParser.cs @@ -1,24 +1,20 @@ -using System; using NCrontab; -using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Core.Configuration +namespace Umbraco.Cms.Core.Configuration; + +public class NCronTabParser : ICronTabParser { - public class NCronTabParser : ICronTabParser + public bool IsValidCronTab(string cronTab) { - public bool IsValidCronTab(string cronTab) - { - var result = CrontabSchedule.TryParse(cronTab); + var result = CrontabSchedule.TryParse(cronTab); - return !(result is null); - } - - public DateTime GetNextOccurrence(string cronTab, DateTime time) - { - var result = CrontabSchedule.Parse(cronTab); - - return result.GetNextOccurrence(time); - } + return !(result is null); } + public DateTime GetNextOccurrence(string cronTab, DateTime time) + { + var result = CrontabSchedule.Parse(cronTab); + + return result.GetNextOccurrence(time); + } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs index 7091b89cad..bc83695d94 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs @@ -3,30 +3,27 @@ using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Gets the mappers collection builder. /// - public static partial class UmbracoBuilderExtensions - { - /// - /// Gets the mappers collection builder. - /// - /// The builder. - public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); + /// The builder. + public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); - public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); + public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); - - /// - /// Gets the package migration plans collection builder. - /// - /// The builder. - public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) - => builder.WithCollectionBuilder(); - - } + /// + /// Gets the package migration plans collection builder. + /// + /// The builder. + public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 4b30a10159..9252cb7111 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog; +using SixLabors.ImageSharp; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; @@ -55,335 +56,345 @@ using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; +using IScopeProvider = Umbraco.Cms.Infrastructure.Scoping.IScopeProvider; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds all core Umbraco services required to run which may be replaced later in the pipeline + /// + public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) { - /// - /// Adds all core Umbraco services required to run which may be replaced later in the pipeline - /// - public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) + builder + .AddMainDom() + .AddLogging(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); + builder.NPocoMappers()?.Add(); + builder.PackageMigrationPlans()?.Add(() => builder.TypeLoader.GetPackageMigrationPlans()); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + + // composers + builder + .AddRepositories() + .AddServices() + .AddCoreMappingProfiles() + .AddFileSystems() + .AddWebAssets(); + + // register persistence mappers - required by database factory so needs to be done here + // means the only place the collection can be modified is in a runtime - afterwards it + // has been frozen and it is too late + builder.Mappers()?.AddCoreMappers(); + + // register the scope provider + builder.Services.AddSingleton(); // implements IScopeProvider, IScopeAccessor + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(f => f.GetRequiredService()); + + builder.Services.AddScoped(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // register database builder + // *not* a singleton, don't want to keep it around + builder.Services.AddTransient(); + + // register manifest parser, will be injected in collection builders where needed + builder.Services.AddSingleton(); + + // register the manifest filter collection builder (collection is empty by default) + builder.ManifestFilters(); + + builder.MediaUrlGenerators() + .Add() + .Add(); + + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(factory + => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault( + factory.GetRequiredService>().CurrentValue))); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => new MigrationBuilder(factory)); + + builder.AddPreValueMigrators(); + + builder.Services.AddSingleton(); + + // register the published snapshot accessor - the "current" published snapshot is in the umbraco context + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + // Config manipulator + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be + // discovered when CoreBootManager configures the converters. We will remove the basic one defined + // in core so that the more enhanced version is active. + builder.PropertyValueConverters() + .Remove(); + + // register *all* checks, except those marked [HideFromTypeFinder] of course + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + + // replace + builder.Services.AddSingleton( + services => new EmailSender( + services.GetRequiredService>(), + services.GetRequiredService>(), + services.GetRequiredService(), + services.GetService>(), + services.GetService>())); + + builder.Services.AddSingleton(); + + builder.Services.AddScoped(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(sp => + new PublishedContentQueryAccessor(sp.GetRequiredService())); + builder.Services.AddScoped(factory => { - builder - .AddMainDom() - .AddLogging(); + IUmbracoContextAccessor umbCtx = factory.GetRequiredService(); + IUmbracoContext umbracoContext = umbCtx.GetRequiredUmbracoContext(); + return new PublishedContentQuery( + umbracoContext.PublishedSnapshot, + factory.GetRequiredService(), factory.GetRequiredService()); + }); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => factory.GetRequiredService().SqlContext); - builder.NPocoMappers()?.Add(); - builder.PackageMigrationPlans()?.Add(() => builder.TypeLoader.GetPackageMigrationPlans()); + // register accessors for cultures + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.AddNotificationAsyncHandler(); - builder.AddNotificationAsyncHandler(); + builder.Services.AddSingleton(); - // composers - builder - .AddRepositories() - .AddServices() - .AddCoreMappingProfiles() - .AddFileSystems() - .AddWebAssets(); + builder.Services.AddSingleton(); - // register persistence mappers - required by database factory so needs to be done here - // means the only place the collection can be modified is in a runtime - afterwards it - // has been frozen and it is too late - builder.Mappers()?.AddCoreMappers(); + builder.Services.AddSingleton(); - // register the scope provider - builder.Services.AddSingleton(); // implements IScopeProvider, IScopeAccessor - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // Add default ImageSharp configuration and service implementations + builder.Services.AddSingleton(Configuration.Default); + builder.Services.AddSingleton(); - // register database builder - // *not* a singleton, don't want to keep it around - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.AddInstaller(); - // register manifest parser, will be injected in collection builders where needed - builder.Services.AddSingleton(); + // Services required to run background jobs (with out the handler) + builder.Services.AddSingleton(); + return builder; + } - // register the manifest filter collection builder (collection is empty by default) - builder.ManifestFilters(); + public static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.SetLogViewer(); + builder.Services.AddSingleton(factory => new SerilogJsonLogViewer( + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + Log.Logger)); - builder.MediaUrlGenerators() - .Add() - .Add(); + return builder; + } - builder.Services.AddSingleton(); + /// + /// Adds logging requirements for Umbraco + /// + private static IUmbracoBuilder AddLogging(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + return builder; + } - builder.Services.AddSingleton(factory - => new DefaultShortStringHelper(new DefaultShortStringHelperConfig().WithDefault(factory.GetRequiredService>().CurrentValue))); + private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(factory => + { + IOptions globalSettings = factory.GetRequiredService>(); + IOptionsMonitor connectionStrings = + factory.GetRequiredService>(); + IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => new MigrationBuilder(factory)); + IDbProviderFactoryCreator dbCreator = factory.GetRequiredService(); + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory = + factory.GetRequiredService(); + ILoggerFactory loggerFactory = factory.GetRequiredService(); + NPocoMapperCollection npocoMappers = factory.GetRequiredService(); + IMainDomKeyGenerator mainDomKeyGenerator = factory.GetRequiredService(); - builder.AddPreValueMigrators(); - - builder.Services.AddSingleton(); - - // register the published snapshot accessor - the "current" published snapshot is in the umbraco context - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - // Config manipulator - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be - // discovered when CoreBootManager configures the converters. We will remove the basic one defined - // in core so that the more enhanced version is active. - builder.PropertyValueConverters() - .Remove(); - - // register *all* checks, except those marked [HideFromTypeFinder] of course - builder.Services.AddSingleton(); - - builder.Services.AddSingleton(); - - builder.Services.AddScoped(); - - // replace - builder.Services.AddSingleton( - services => new EmailSender( - services.GetRequiredService>(), - services.GetRequiredService>(), - services.GetRequiredService(), - services.GetService>(), - services.GetService>())); - - builder.Services.AddSingleton(); - - builder.Services.AddScoped(); - - builder.Services.AddSingleton(); - builder.Services.AddSingleton(sp => - new PublishedContentQueryAccessor(sp.GetRequiredService()) - ); - builder.Services.AddScoped(factory => + switch (globalSettings.Value.MainDomLock) { - var umbCtx = factory.GetRequiredService(); - var umbracoContext = umbCtx.GetRequiredUmbracoContext(); - return new PublishedContentQuery(umbracoContext.PublishedSnapshot, factory.GetRequiredService(), factory.GetRequiredService()); - }); + case "SqlMainDomLock": + return new SqlMainDomLock( + loggerFactory, + globalSettings, + connectionStrings, + dbCreator, + mainDomKeyGenerator, + databaseSchemaCreatorFactory, + npocoMappers); - // register accessors for cultures - builder.Services.AddSingleton(); + case "MainDomSemaphoreLock": + return new MainDomSemaphoreLock( + loggerFactory.CreateLogger(), + hostingEnvironment); - builder.Services.AddSingleton(); + case "FileSystemMainDomLock": + default: + return new FileSystemMainDomLock( + loggerFactory.CreateLogger(), + mainDomKeyGenerator, hostingEnvironment, + factory.GetRequiredService>()); + } + }); - builder.Services.AddSingleton(); + return builder; + } - builder.Services.AddSingleton(); + private static IUmbracoBuilder AddPreValueMigrators(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); - builder.Services.AddSingleton(); + return builder; + } - builder.Services.AddSingleton(); + public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder) + { + // add handlers for sending user notifications (i.e. emails) + builder.Services.AddSingleton(); + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - // Add default ImageSharp configuration and service implementations - builder.Services.AddSingleton(SixLabors.ImageSharp.Configuration.Default); - builder.Services.AddSingleton(); + // add handlers for building content relations + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - builder.Services.AddTransient(); - builder.AddInstaller(); + // add notification handlers for property editors + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - // Services required to run background jobs (with out the handler) - builder.Services.AddSingleton(); - return builder; - } + // add notification handlers for redirect tracking + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - /// - /// Adds logging requirements for Umbraco - /// - private static IUmbracoBuilder AddLogging(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - return builder; - } + // Add notification handlers for DistributedCache + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + ; - private static IUmbracoBuilder AddMainDom(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(factory => - { - var globalSettings = factory.GetRequiredService>(); - var connectionStrings = factory.GetRequiredService>(); - var hostingEnvironment = factory.GetRequiredService(); + // add notification handlers for auditing + builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); - var dbCreator = factory.GetRequiredService(); - var databaseSchemaCreatorFactory = factory.GetRequiredService(); - var loggerFactory = factory.GetRequiredService(); - var npocoMappers = factory.GetRequiredService(); - var mainDomKeyGenerator = factory.GetRequiredService(); - - switch (globalSettings.Value.MainDomLock) - { - case "SqlMainDomLock": - return new SqlMainDomLock( - loggerFactory, - globalSettings, - connectionStrings, - dbCreator, - mainDomKeyGenerator, - databaseSchemaCreatorFactory, - npocoMappers); - - case "MainDomSemaphoreLock": - return new MainDomSemaphoreLock(loggerFactory.CreateLogger(), hostingEnvironment); - - case "FileSystemMainDomLock": - default: - return new FileSystemMainDomLock(loggerFactory.CreateLogger(), mainDomKeyGenerator, hostingEnvironment, factory.GetRequiredService>()); - } - }); - - return builder; - } - - - private static IUmbracoBuilder AddPreValueMigrators(this IUmbracoBuilder builder) - { - builder.WithCollectionBuilder() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append(); - - return builder; - } - - public static IUmbracoBuilder AddLogViewer(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.SetLogViewer(); - builder.Services.AddSingleton(factory => new SerilogJsonLogViewer(factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - Log.Logger)); - - return builder; - } - - public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder) - { - // add handlers for sending user notifications (i.e. emails) - builder.Services.AddSingleton(); - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add handlers for building content relations - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add notification handlers for property editors - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // add notification handlers for redirect tracking - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - // Add notification handlers for DistributedCache - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - ; - // add notification handlers for auditing - builder - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler() - .AddNotificationHandler(); - - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs index 71ea85d80f..21e715b803 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; @@ -7,96 +6,99 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Adds distributed cache support /// - public static partial class UmbracoBuilderExtensions + /// + /// This is still required for websites that are not load balancing because this ensures that sites hosted + /// with managed hosts like IIS/etc... work correctly when AppDomains are running in parallel. + /// + public static IUmbracoBuilder AddDistributedCache(this IUmbracoBuilder builder) { - /// - /// Adds distributed cache support - /// - /// - /// This is still required for websites that are not load balancing because this ensures that sites hosted - /// with managed hosts like IIS/etc... work correctly when AppDomains are running in parallel. - /// - public static IUmbracoBuilder AddDistributedCache(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.SetServerMessenger(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - return builder; - } + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.SetServerMessenger(); +builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The type of the server registrar. - /// The builder. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder) - where T : class, IServerRoleAccessor - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The type of the server registrar. + /// The builder. + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder) + where T : class, IServerRoleAccessor + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The builder. - /// A function creating a server registrar. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The builder. + /// A function creating a server registrar. + public static IUmbracoBuilder SetServerRegistrar( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the server registrar. - /// - /// The builder. - /// A server registrar. - public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) - { - builder.Services.AddUnique(registrar); - return builder; - } + /// + /// Sets the server registrar. + /// + /// The builder. + /// A server registrar. + public static IUmbracoBuilder SetServerRegistrar(this IUmbracoBuilder builder, IServerRoleAccessor registrar) + { + builder.Services.AddUnique(registrar); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The type of the server registrar. - /// The builder. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder) - where T : class, IServerMessenger - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The type of the server registrar. + /// The builder. + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder) + where T : class, IServerMessenger + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The builder. - /// A function creating a server messenger. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The builder. + /// A function creating a server messenger. + public static IUmbracoBuilder SetServerMessenger( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the server messenger. - /// - /// The builder. - /// A server messenger. - public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) - { - builder.Services.AddUnique(registrar); - return builder; - } + /// + /// Sets the server messenger. + /// + /// The builder. + /// A server messenger. + public static IUmbracoBuilder SetServerMessenger(this IUmbracoBuilder builder, IServerMessenger registrar) + { + builder.Services.AddUnique(registrar); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index da31a8df39..aabadc5197 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -1,7 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.PropertyEditors; @@ -12,55 +10,54 @@ using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { - /// - /// Provides extension methods to the class. - /// - public static partial class UmbracoBuilderExtensions + public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder) { - public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder) - { - // populators are not a collection: one cannot remove ours, and can only add more - // the container can inject IEnumerable and get them all - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + // populators are not a collection: one cannot remove ours, and can only add more + // the container can inject IEnumerable and get them all + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => - new ContentValueSetBuilder( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - true)); - builder.Services.AddUnique(factory => - new ContentValueSetBuilder( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - false)); - builder.Services.AddUnique, MediaValueSetBuilder>(); - builder.Services.AddUnique, MemberValueSetBuilder>(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => + new ContentValueSetBuilder( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + true)); + builder.Services.AddUnique(factory => + new ContentValueSetBuilder( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + false)); + builder.Services.AddUnique, MediaValueSetBuilder>(); + builder.Services.AddUnique, MemberValueSetBuilder>(); + builder.Services.AddSingleton(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index 310ae0b302..e9564b0263 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -3,58 +3,59 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.IO.MediaPathSchemes; using Umbraco.Extensions; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /* + * HOW TO REPLACE THE MEDIA UNDERLYING FILESYSTEM + * ---------------------------------------------- + * + * Create an implementation of IFileSystem and register it as the underlying filesystem for + * MediaFileSystem with the following extension on composition. + * + * builder.SetMediaFileSystem(factory => FactoryMethodToReturnYourImplementation()) + * + * WHAT IS SHADOWING + * ----------------- + * + * Shadowing is the technology used for Deploy to implement some sort of + * transaction-management on top of filesystems. The plumbing explained above, + * compared to creating your own physical filesystem, ensures that your filesystem + * would participate into such transactions. + * + */ + + internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) { - /* - * HOW TO REPLACE THE MEDIA UNDERLYING FILESYSTEM - * ---------------------------------------------- - * - * Create an implementation of IFileSystem and register it as the underlying filesystem for - * MediaFileSystem with the following extension on composition. - * - * builder.SetMediaFileSystem(factory => FactoryMethodToReturnYourImplementation()) - * - * WHAT IS SHADOWING - * ----------------- - * - * Shadowing is the technology used for Deploy to implement some sort of - * transaction-management on top of filesystems. The plumbing explained above, - * compared to creating your own physical filesystem, ensures that your filesystem - * would participate into such transactions. - * - */ + // register FileSystems, which manages all filesystems + builder.Services.AddSingleton(); - internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder) + // register the scheme for media paths + builder.Services.AddUnique(); + + builder.Services.AddUnique(); + builder.Services.AddUnique(); + + builder.SetMediaFileSystem(factory => { - // register FileSystems, which manages all filesystems - builder.Services.AddSingleton(); + IIOHelper ioHelper = factory.GetRequiredService(); + IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); + ILogger logger = factory.GetRequiredService>(); + GlobalSettings globalSettings = factory.GetRequiredService>().Value; - // register the scheme for media paths - builder.Services.AddUnique(); + var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) + ? globalSettings.UmbracoMediaPhysicalRootPath + : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); + var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); + return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); + }); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - - builder.SetMediaFileSystem(factory => - { - IIOHelper ioHelper = factory.GetRequiredService(); - IHostingEnvironment hostingEnvironment = factory.GetRequiredService(); - ILogger logger = factory.GetRequiredService>(); - GlobalSettings globalSettings = factory.GetRequiredService>().Value; - - var rootPath = Path.IsPathRooted(globalSettings.UmbracoMediaPhysicalRootPath) ? globalSettings.UmbracoMediaPhysicalRootPath : hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); - var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); - return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); - }); - - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index d750eb15e0..c3aa291fb7 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -7,39 +7,37 @@ using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Infrastructure.Install.InstallSteps; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds the services for the Umbraco installer + /// + internal static IUmbracoBuilder AddInstaller(this IUmbracoBuilder builder) { - /// - /// Adds the services for the Umbraco installer - /// - internal static IUmbracoBuilder AddInstaller(this IUmbracoBuilder builder) + // register the installer steps + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(provider => { - // register the installer steps - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(provider => - { - return new TelemetryIdentifierStep( - provider.GetRequiredService>(), - provider.GetRequiredService()); - }); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + return new TelemetryIdentifierStep( + provider.GetRequiredService>(), + provider.GetRequiredService()); + }); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.AddScoped(); - builder.Services.AddTransient(); - builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs index 42ce7f7932..05fc2f125c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs @@ -5,40 +5,39 @@ using Umbraco.Cms.Core.Models.Mapping; using Umbraco.Cms.Core.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Registers the core Umbraco mapper definitions + /// + public static IUmbracoBuilder AddCoreMappingProfiles(this IUmbracoBuilder builder) { - /// - /// Registers the core Umbraco mapper definitions - /// - public static IUmbracoBuilder AddCoreMappingProfiles(this IUmbracoBuilder builder) - { - builder.Services.AddUnique(); + builder.Services.AddUnique(); - builder.WithCollectionBuilder() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add(); + builder.WithCollectionBuilder() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index de6267fd43..8b20a4725a 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,77 +1,75 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Composes repositories. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Composes repositories. + /// Adds the Umbraco repositories /// - public static partial class UmbracoBuilderExtensions + internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) { - /// - /// Adds the Umbraco repositories - /// - internal static IUmbracoBuilder AddRepositories(this IUmbracoBuilder builder) - { - // repositories - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddMultipleUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(factory => factory.GetRequiredService()); - builder.Services.AddUnique(factory => factory.GetRequiredService()); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); + // repositories + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddMultipleUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => factory.GetRequiredService()); + builder.Services.AddUnique(factory => factory.GetRequiredService()); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 7e0802d558..b7d600ec7c 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -8,7 +7,7 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Extensions; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; @@ -26,93 +25,92 @@ using Umbraco.Cms.Infrastructure.Telemetry.Providers; using Umbraco.Cms.Infrastructure.Templates; using Umbraco.Extensions; using CacheInstructionService = Umbraco.Cms.Core.Services.Implement.CacheInstructionService; -using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + /// + /// Adds Umbraco services + /// + internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) { - /// - /// Adds Umbraco services - /// - internal static IUmbracoBuilder AddServices(this IUmbracoBuilder builder) - { - // register the service context - builder.Services.AddSingleton(); + // register the service context + builder.Services.AddSingleton(); - // register the special idk map - builder.Services.AddUnique(); + // register the special idk map + builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddTransient(CreateLocalizedTextServiceFileSourcesFactory); - builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); - builder.Services.AddUnique(); - builder.Services.AddSingleton(CreatePackageDataInstallation); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddTransient(CreateLocalizedTextServiceFileSourcesFactory); + builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); + builder.Services.AddUnique(); + builder.Services.AddSingleton(CreatePackageDataInstallation); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); - return builder; - } + return builder; + } + private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) + => new( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + packageRepoFileName); - private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) - => new PackagesRepository( - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - packageRepoFileName); + // Factory registration is only required because of ambiguous constructor + private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) + => new( + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService()); - // Factory registration is only required because of ambiguous constructor - private static PackageDataInstallation CreatePackageDataInstallation(IServiceProvider factory) - => new PackageDataInstallation( - factory.GetRequiredService(), - factory.GetRequiredService>(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService(), - factory.GetRequiredService()); + private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory( + IServiceProvider container) + { + IHostingEnvironment hostingEnvironment = container.GetRequiredService(); + var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); + var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); - private static LocalizedTextServiceFileSources CreateLocalizedTextServiceFileSourcesFactory(IServiceProvider container) - { - var hostingEnvironment = container.GetRequiredService(); - var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); - var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); - - return new LocalizedTextServiceFileSources( - container.GetRequiredService>(), - container.GetRequiredService(), - mainLangFolder, - container.GetServices(), - new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Lang").GetDirectoryContents(string.Empty)); - } + return new LocalizedTextServiceFileSources( + container.GetRequiredService>(), + container.GetRequiredService(), + mainLangFolder, + container.GetServices(), + new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Lang") + .GetDirectoryContents(string.Empty)); } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs index f0ab1ec344..3c1162bbab 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -1,25 +1,24 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; using Umbraco.Cms.Infrastructure.Telemetry.Providers; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static class UmbracoBuilder_TelemetryProviders { - public static class UmbracoBuilder_TelemetryProviders + public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) { - public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) - { - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - return builder; - } + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs index e3839e152b..f899f311f5 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Uniques.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Dictionary; @@ -8,208 +7,220 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +/// +/// Provides extension methods to the class. +/// +public static partial class UmbracoBuilderExtensions { /// - /// Provides extension methods to the class. + /// Sets the culture dictionary factory. /// - public static partial class UmbracoBuilderExtensions + /// The type of the factory. + /// The builder. + public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder) + where T : class, ICultureDictionaryFactory { - /// - /// Sets the culture dictionary factory. - /// - /// The type of the factory. - /// The builder. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder) - where T : class, ICultureDictionaryFactory - { - builder.Services.AddUnique(); - return builder; - } + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the default view content provider - /// - /// The type of the provider. - /// The builder. - /// - public static IUmbracoBuilder SetDefaultViewContentProvider(this IUmbracoBuilder builder) - where T : class, IDefaultViewContentProvider - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the default view content provider + /// + /// The type of the provider. + /// The builder. + /// + public static IUmbracoBuilder SetDefaultViewContentProvider(this IUmbracoBuilder builder) + where T : class, IDefaultViewContentProvider + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the culture dictionary factory. - /// - /// The builder. - /// A function creating a culture dictionary factory. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the culture dictionary factory. + /// + /// The builder. + /// A function creating a culture dictionary factory. + public static IUmbracoBuilder SetCultureDictionaryFactory( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the culture dictionary factory. - /// - /// The builder. - /// A factory. - public static IUmbracoBuilder SetCultureDictionaryFactory(this IUmbracoBuilder builder, ICultureDictionaryFactory factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the culture dictionary factory. + /// + /// The builder. + /// A factory. + public static IUmbracoBuilder SetCultureDictionaryFactory( + this IUmbracoBuilder builder, + ICultureDictionaryFactory factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The type of the factory. - /// The builder. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder) - where T : class, IPublishedModelFactory - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The type of the factory. + /// The builder. + public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder) + where T : class, IPublishedModelFactory + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The builder. - /// A function creating a published content model factory. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The builder. + /// A function creating a published content model factory. + public static IUmbracoBuilder SetPublishedContentModelFactory( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the published content model factory. - /// - /// The builder. - /// A published content model factory. - public static IUmbracoBuilder SetPublishedContentModelFactory(this IUmbracoBuilder builder, IPublishedModelFactory factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the published content model factory. + /// + /// The builder. + /// A published content model factory. + public static IUmbracoBuilder SetPublishedContentModelFactory( + this IUmbracoBuilder builder, + IPublishedModelFactory factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the short string helper. - /// - /// The type of the short string helper. - /// The builder. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder) - where T : class, IShortStringHelper - { - builder.Services.AddUnique(); - return builder; - } + /// + /// Sets the short string helper. + /// + /// The type of the short string helper. + /// The builder. + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder) + where T : class, IShortStringHelper + { + builder.Services.AddUnique(); + return builder; + } - /// - /// Sets the short string helper. - /// - /// The builder. - /// A function creating a short string helper. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + /// + /// Sets the short string helper. + /// + /// The builder. + /// A function creating a short string helper. + public static IUmbracoBuilder SetShortStringHelper( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } - /// - /// Sets the short string helper. - /// - /// A builder. - /// A short string helper. - public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) - { - builder.Services.AddUnique(helper); - return builder; - } + /// + /// Sets the short string helper. + /// + /// A builder. + /// A short string helper. + public static IUmbracoBuilder SetShortStringHelper(this IUmbracoBuilder builder, IShortStringHelper helper) + { + builder.Services.AddUnique(helper); + return builder; + } - /// - /// Sets the filesystem used by the MediaFileManager - /// - /// A builder. - /// Factory method to create an IFileSystem implementation used in the MediaFileManager - public static IUmbracoBuilder SetMediaFileSystem(this IUmbracoBuilder builder, - Func filesystemFactory) - { - builder.Services.AddUnique( - provider => - { - IFileSystem filesystem = filesystemFactory(provider); - // We need to use the Filesystems to create a shadow wrapper, - // because shadow wrapper requires the IsScoped delegate from the FileSystems. - // This is used by the scope provider when taking control of the filesystems. - FileSystems fileSystems = provider.GetRequiredService(); - IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); - - return provider.CreateInstance(shadow); - }); - return builder; - } - - /// - /// Register FileSystems with a method to configure the . - /// - /// A builder. - /// Method that configures the . - /// Throws exception if is null. - /// Throws exception if full path can't be resolved successfully. - public static IUmbracoBuilder ConfigureFileSystems(this IUmbracoBuilder builder, - Action configure) - { - if (configure == null) + /// + /// Sets the filesystem used by the MediaFileManager + /// + /// A builder. + /// Factory method to create an IFileSystem implementation used in the MediaFileManager + public static IUmbracoBuilder SetMediaFileSystem( + this IUmbracoBuilder builder, + Func filesystemFactory) + { + builder.Services.AddUnique( + provider => { - throw new ArgumentNullException(nameof(configure)); - } + IFileSystem filesystem = filesystemFactory(provider); - builder.Services.AddUnique( - provider => - { - FileSystems fileSystems = provider.CreateInstance(); - configure(provider, fileSystems); - return fileSystems; - }); - return builder; - } + // We need to use the Filesystems to create a shadow wrapper, + // because shadow wrapper requires the IsScoped delegate from the FileSystems. + // This is used by the scope provider when taking control of the filesystems. + FileSystems fileSystems = provider.GetRequiredService(); + IFileSystem shadow = fileSystems.CreateShadowWrapper(filesystem, "media"); - /// - /// Sets the log viewer. - /// - /// The type of the log viewer. - /// The builder. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) - where T : class, ILogViewer + return provider.CreateInstance(shadow); + }); + return builder; + } + + /// + /// Register FileSystems with a method to configure the . + /// + /// A builder. + /// Method that configures the . + /// Throws exception if is null. + /// Throws exception if full path can't be resolved successfully. + public static IUmbracoBuilder ConfigureFileSystems( + this IUmbracoBuilder builder, + Action configure) + { + if (configure == null) { - builder.Services.AddUnique(); - return builder; + throw new ArgumentNullException(nameof(configure)); } - /// - /// Sets the log viewer. - /// - /// The builder. - /// A function creating a log viewer. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } + builder.Services.AddUnique( + provider => + { + FileSystems fileSystems = provider.CreateInstance(); + configure(provider, fileSystems); + return fileSystems; + }); + return builder; + } - /// - /// Sets the log viewer. - /// - /// A builder. - /// A log viewer. - public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) - { - builder.Services.AddUnique(viewer); - return builder; - } + /// + /// Sets the log viewer. + /// + /// The type of the log viewer. + /// The builder. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder) + where T : class, ILogViewer + { + builder.Services.AddUnique(); + return builder; + } + + /// + /// Sets the log viewer. + /// + /// The builder. + /// A function creating a log viewer. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } + + /// + /// Sets the log viewer. + /// + /// A builder. + /// A log viewer. + public static IUmbracoBuilder SetLogViewer(this IUmbracoBuilder builder, ILogViewer viewer) + { + builder.Services.AddUnique(viewer); + return builder; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs index 53e638997b..d788f26f88 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.WebAssets.cs @@ -1,16 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Cms.Core.Events; using Umbraco.Cms.Infrastructure.WebAssets; -namespace Umbraco.Cms.Infrastructure.DependencyInjection +namespace Umbraco.Cms.Infrastructure.DependencyInjection; + +public static partial class UmbracoBuilderExtensions { - public static partial class UmbracoBuilderExtensions + internal static IUmbracoBuilder AddWebAssets(this IUmbracoBuilder builder) { - internal static IUmbracoBuilder AddWebAssets(this IUmbracoBuilder builder) - { - builder.Services.AddSingleton(); - return builder; - } + builder.Services.AddSingleton(); + return builder; } } diff --git a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs index 94dabf7b4f..eb1bc518ac 100644 --- a/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs +++ b/src/Umbraco.Infrastructure/Deploy/IGridCellValueConnector.cs @@ -1,38 +1,44 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Deploy +namespace Umbraco.Cms.Core.Deploy; + +/// +/// Defines methods that can convert a grid cell value to / from an environment-agnostic string. +/// +/// +/// Grid cell values may contain values such as content identifiers, that would be local +/// to one environment, and need to be converted in order to be deployed. +/// +public interface IGridCellValueConnector { /// - /// Defines methods that can convert a grid cell value to / from an environment-agnostic string. + /// Gets a value indicating whether the connector supports a specified grid editor view. /// - /// Grid cell values may contain values such as content identifiers, that would be local - /// to one environment, and need to be converted in order to be deployed. - public interface IGridCellValueConnector - { - /// - /// Gets a value indicating whether the connector supports a specified grid editor view. - /// - /// The grid editor view. It needs to be the view instead of the alias as the view is really what identifies what kind of connector should be used. Alias can be anything and you can have multiple different aliases using the same kind of view. - /// A value indicating whether the connector supports the grid editor view. - /// Note that can be string.Empty to indicate the "default" connector. - bool IsConnector(string view); + /// + /// The grid editor view. It needs to be the view instead of the alias as the view is really what + /// identifies what kind of connector should be used. Alias can be anything and you can have multiple different aliases + /// using the same kind of view. + /// + /// A value indicating whether the connector supports the grid editor view. + /// Note that can be string.Empty to indicate the "default" connector. + bool IsConnector(string view); - /// - /// Gets the value to be deployed from the control value as a string. - /// - /// The control containing the value. - /// The dependencies of the property. - /// The grid cell value to be deployed. - /// Note that - string? GetValue(GridValue.GridControl gridControl, ICollection dependencies); + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The dependencies of the property. + /// The grid cell value to be deployed. + /// Note that + string? GetValue(GridValue.GridControl gridControl, ICollection dependencies); - /// - /// Allows you to modify the value of a control being deployed. - /// - /// The control being deployed. - /// Follows the pattern of the property value connectors (). The SetValue method is used to modify the value of the . - - void SetValue(GridValue.GridControl gridControl); - } + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// + /// Follows the pattern of the property value connectors (). The SetValue method is + /// used to modify the value of the . + /// + void SetValue(GridValue.GridControl gridControl); } diff --git a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs index 7916de3996..b44a7c993b 100644 --- a/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs +++ b/src/Umbraco.Infrastructure/DistributedLocking/DefaultDistributedLockingMechanismFactory.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DistributedLocking; @@ -10,12 +6,12 @@ namespace Umbraco.Cms.Infrastructure.DistributedLocking; public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMechanismFactory { - private object _lock = new(); - private bool _initialized; - private IDistributedLockingMechanism _distributedLockingMechanism = null!; + private readonly IEnumerable _distributedLockingMechanisms; private readonly IOptionsMonitor _globalSettings; - private readonly IEnumerable _distributedLockingMechanisms; + private IDistributedLockingMechanism _distributedLockingMechanism = null!; + private bool _initialized; + private object _lock = new(); public DefaultDistributedLockingMechanismFactory( IOptionsMonitor globalSettings, @@ -49,7 +45,8 @@ public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMech if (value == null) { - throw new InvalidOperationException($"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); + throw new InvalidOperationException( + $"Couldn't find DistributedLockingMechanism specified by global config: {configured}"); } } @@ -59,6 +56,6 @@ public class DefaultDistributedLockingMechanismFactory : IDistributedLockingMech return defaultMechanism; } - throw new InvalidOperationException($"Couldn't find an appropriate default distributed locking mechanism."); + throw new InvalidOperationException("Couldn't find an appropriate default distributed locking mechanism."); } } diff --git a/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs b/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs index 23bfed0cd9..2bc6aa252a 100644 --- a/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Infrastructure/Events/MigrationEventArgs.cs @@ -1,94 +1,110 @@ -using System; -using System.Collections.Generic; -using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable { - public class MigrationEventArgs : CancellableObjectEventArgs>, IEquatable + public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : this(migrationTypes, null, configuredVersion, targetVersion, productName, canCancel) { - public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) - : this(migrationTypes, null, configuredVersion, targetVersion, productName, canCancel) - { } + } - internal MigrationEventArgs(IList migrationTypes, IMigrationContext? migrationContext, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) - : base(migrationTypes, canCancel) + public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName) + : this(migrationTypes, null, configuredVersion, targetVersion, productName, false) + { + } + + internal MigrationEventArgs(IList migrationTypes, IMigrationContext? migrationContext, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : base(migrationTypes, canCancel) + { + MigrationContext = migrationContext; + ConfiguredSemVersion = configuredVersion; + TargetSemVersion = targetVersion; + ProductName = productName; + } + + /// + /// Returns all migrations that were used in the migration runner + /// + public IList? MigrationsTypes => EventObject; + + /// + /// Gets the origin version of the migration, i.e. the one that is currently installed. + /// + public SemVersion ConfiguredSemVersion { get; } + + /// + /// Gets the target version of the migration. + /// + public SemVersion TargetSemVersion { get; } + + /// + /// Gets the product name. + /// + public string ProductName { get; } + + /// + /// Gets the migration context. + /// + /// Is only available after migrations have run, for post-migrations. + internal IMigrationContext? MigrationContext { get; } + + public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) => Equals(left, right); + + public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) => !Equals(left, right); + + public bool Equals(MigrationEventArgs? other) + { + if (ReferenceEquals(null, other)) { - MigrationContext = migrationContext; - ConfiguredSemVersion = configuredVersion; - TargetSemVersion = targetVersion; - ProductName = productName; + return false; } - public MigrationEventArgs(IList migrationTypes, SemVersion configuredVersion, SemVersion targetVersion, string productName) - : this(migrationTypes, null, configuredVersion, targetVersion, productName, false) - { } - - /// - /// Returns all migrations that were used in the migration runner - /// - public IList? MigrationsTypes => EventObject; - - /// - /// Gets the origin version of the migration, i.e. the one that is currently installed. - /// - public SemVersion ConfiguredSemVersion { get; } - - /// - /// Gets the target version of the migration. - /// - public SemVersion TargetSemVersion { get; } - - /// - /// Gets the product name. - /// - public string ProductName { get; } - - /// - /// Gets the migration context. - /// - /// Is only available after migrations have run, for post-migrations. - internal IMigrationContext? MigrationContext { get; } - - public bool Equals(MigrationEventArgs? other) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return base.Equals(other) && ConfiguredSemVersion.Equals(other.ConfiguredSemVersion) && (MigrationContext?.Equals(other.MigrationContext) ?? false) && string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + return true; } - public override bool Equals(object? obj) + return base.Equals(other) && ConfiguredSemVersion.Equals(other.ConfiguredSemVersion) && + (MigrationContext?.Equals(other.MigrationContext) ?? false) && + string.Equals(ProductName, other.ProductName) && TargetSemVersion.Equals(other.TargetSemVersion); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((MigrationEventArgs) obj); + return false; } - public override int GetHashCode() + if (ReferenceEquals(this, obj)) { - unchecked + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((MigrationEventArgs)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); + if (MigrationContext is not null) { - int hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ ConfiguredSemVersion.GetHashCode(); - if (MigrationContext is not null) - { - hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); - } - hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); - hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); - return hashCode; + hashCode = (hashCode * 397) ^ MigrationContext.GetHashCode(); } - } - public static bool operator ==(MigrationEventArgs left, MigrationEventArgs right) - { - return Equals(left, right); - } - - public static bool operator !=(MigrationEventArgs left, MigrationEventArgs right) - { - return !Equals(left, right); + hashCode = (hashCode * 397) ^ ProductName.GetHashCode(); + hashCode = (hashCode * 397) ^ TargetSemVersion.GetHashCode(); + return hashCode; } } } diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs index b06248c79e..768c1bc7aa 100644 --- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs @@ -2,156 +2,162 @@ // See LICENSE for more details. using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Events +namespace Umbraco.Cms.Core.Events; + +// TODO: lots of duplicate code in this one, refactor +public sealed class RelateOnTrashNotificationHandler : + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { - // TODO: lots of duplicate code in this one, refactor - public sealed class RelateOnTrashNotificationHandler : - INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + private readonly IAuditService _auditService; + private readonly IEntityService _entityService; + private readonly IRelationService _relationService; + private readonly IScopeProvider _scopeProvider; + private readonly ILocalizedTextService _textService; + + public RelateOnTrashNotificationHandler( + IRelationService relationService, + IEntityService entityService, + ILocalizedTextService textService, + IAuditService auditService, + IScopeProvider scopeProvider) { - private readonly IRelationService _relationService; - private readonly IEntityService _entityService; - private readonly ILocalizedTextService _textService; - private readonly IAuditService _auditService; - private readonly IScopeProvider _scopeProvider; + _relationService = relationService; + _entityService = entityService; + _textService = textService; + _auditService = auditService; + _scopeProvider = scopeProvider; + } - public RelateOnTrashNotificationHandler( - IRelationService relationService, - IEntityService entityService, - ILocalizedTextService textService, - IAuditService auditService, - IScopeProvider scopeProvider) + public void Handle(ContentMovedNotification notification) + { + foreach (MoveEventInfo item in notification.MoveInfoCollection.Where(x => + x.OriginalPath.Contains(Constants.System.RecycleBinContentString))) { - _relationService = relationService; - _entityService = entityService; - _textService = textService; - _auditService = auditService; - _scopeProvider = scopeProvider; - } + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; + IEnumerable relations = _relationService.GetByChildId(item.Entity.Id); - public void Handle(ContentMovedNotification notification) - { - foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinContentString))) + foreach (IRelation relation in + relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relations = _relationService.GetByChildId(item.Entity.Id); - - foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) - { - _relationService.Delete(relation); - } + _relationService.Delete(relation); } } + } - public void Handle(ContentMovedToRecycleBinNotification notification) + public void Handle(ContentMovedToRecycleBinNotification notification) + { + using (IScope scope = _scopeProvider.CreateScope()) { - using (var scope = _scopeProvider.CreateScope()) + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; + IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); + + // check that the relation-type exists, if not, then recreate it + if (relationType == null) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; - var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); + Guid documentObjectType = Constants.ObjectTypes.Document; + const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - // check that the relation-type exists, if not, then recreate it - if (relationType == null) - { - var documentObjectType = Constants.ObjectTypes.Document; - const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); - _relationService.Save(relationType); - } - - foreach (var item in notification.MoveInfoCollection) - { - var originalPath = item.OriginalPath.ToDelimitedList(); - var originalParentId = originalPath.Count > 2 - ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) - : Constants.System.Root; - - //before we can create this relation, we need to ensure that the original parent still exists which - //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - - if (_entityService.Exists(originalParentId)) - { - // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later - var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType); - _relationService.Save(relation); - - _auditService.Add(AuditType.Delete, - item.Entity.WriterId, - item.Entity.Id, - ObjectTypes.GetName(UmbracoObjectTypes.Document), - string.Format(_textService.Localize("recycleBin","contentTrashed"), item.Entity.Id, originalParentId) - ); - } - } - - scope.Complete(); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); + _relationService.Save(relationType); } - } - public void Handle(MediaMovedNotification notification) - { - foreach (var item in notification.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinMediaString))) + foreach (MoveEventInfo item in notification.MoveInfoCollection) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relations = _relationService.GetByChildId(item.Entity.Id); - foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) + IList originalPath = item.OriginalPath.ToDelimitedList(); + var originalParentId = originalPath.Count > 2 + ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) + : Constants.System.Root; + + // before we can create this relation, we need to ensure that the original parent still exists which + // may not be the case if the encompassing transaction also deleted it when this item was moved to the bin + if (_entityService.Exists(originalParentId)) { - _relationService.Delete(relation); + // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later + IRelation relation = + _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? + new Relation(originalParentId, item.Entity.Id, relationType); + _relationService.Save(relation); + + _auditService.Add( + AuditType.Delete, + item.Entity.WriterId, + item.Entity.Id, + UmbracoObjectTypes.Document.GetName(), + string.Format(_textService.Localize("recycleBin", "contentTrashed"), item.Entity.Id, originalParentId)); } } + scope.Complete(); } + } - public void Handle(MediaMovedToRecycleBinNotification notification) + public void Handle(MediaMovedNotification notification) + { + foreach (MoveEventInfo item in notification.MoveInfoCollection.Where(x => + x.OriginalPath.Contains(Constants.System.RecycleBinMediaString))) { - using (var scope = _scopeProvider.CreateScope()) + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + IEnumerable relations = _relationService.GetByChildId(item.Entity.Id); + foreach (IRelation relation in + relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) { - const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; - var relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); - // check that the relation-type exists, if not, then recreate it - if (relationType == null) - { - var documentObjectType = Constants.ObjectTypes.Document; - const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); - _relationService.Save(relationType); - } + _relationService.Delete(relation); + } + } + } - foreach (var item in notification.MoveInfoCollection) - { - var originalPath = item.OriginalPath.ToDelimitedList(); - var originalParentId = originalPath.Count > 2 - ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) - : Constants.System.Root; - //before we can create this relation, we need to ensure that the original parent still exists which - //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin - if (_entityService.Exists(originalParentId)) - { - // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later - var relation = _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? new Relation(originalParentId, item.Entity.Id, relationType); - _relationService.Save(relation); - _auditService.Add(AuditType.Delete, - item.Entity.CreatorId, - item.Entity.Id, - ObjectTypes.GetName(UmbracoObjectTypes.Media), - string.Format(_textService.Localize("recycleBin","mediaTrashed"), item.Entity.Id, originalParentId) - ); - } - } + public void Handle(MediaMovedToRecycleBinNotification notification) + { + using (IScope scope = _scopeProvider.CreateScope()) + { + const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias); - scope.Complete(); + // check that the relation-type exists, if not, then recreate it + if (relationType == null) + { + Guid documentObjectType = Constants.ObjectTypes.Document; + const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); + _relationService.Save(relationType); } + foreach (MoveEventInfo item in notification.MoveInfoCollection) + { + IList originalPath = item.OriginalPath.ToDelimitedList(); + var originalParentId = originalPath.Count > 2 + ? int.Parse(originalPath[originalPath.Count - 2], CultureInfo.InvariantCulture) + : Constants.System.Root; + + // before we can create this relation, we need to ensure that the original parent still exists which + // may not be the case if the encompassing transaction also deleted it when this item was moved to the bin + if (_entityService.Exists(originalParentId)) + { + // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later + IRelation relation = + _relationService.GetByParentAndChildId(originalParentId, item.Entity.Id, relationType) ?? + new Relation(originalParentId, item.Entity.Id, relationType); + _relationService.Save(relation); + _auditService.Add( + AuditType.Delete, + item.Entity.CreatorId, + item.Entity.Id, + UmbracoObjectTypes.Media.GetName(), + string.Format(_textService.Localize("recycleBin", "mediaTrashed"), item.Entity.Id, originalParentId)); + } + } + + scope.Complete(); } } } diff --git a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs index ceb203c15c..db6182ee3e 100644 --- a/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/BaseValueSetBuilder.cs @@ -1,70 +1,88 @@ -using System.Collections.Generic; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine -{ - /// - public abstract class BaseValueSetBuilder : IValueSetBuilder - where TContent : IContentBase - { - protected bool PublishedValuesOnly { get; } - private readonly PropertyEditorCollection _propertyEditors; +namespace Umbraco.Cms.Infrastructure.Examine; - protected BaseValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) +/// +public abstract class BaseValueSetBuilder : IValueSetBuilder + where TContent : IContentBase +{ + private readonly PropertyEditorCollection _propertyEditors; + + protected BaseValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + { + PublishedValuesOnly = publishedValuesOnly; + _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + } + + protected bool PublishedValuesOnly { get; } + + /// + public abstract IEnumerable GetValueSets(params TContent[] content); + + protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary>? values) + { + IDataEditor? editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; + if (editor == null) { - PublishedValuesOnly = publishedValuesOnly; - _propertyEditors = propertyEditors ?? throw new System.ArgumentNullException(nameof(propertyEditors)); + return; } - /// - public abstract IEnumerable GetValueSets(params TContent[] content); - - protected void AddPropertyValue(IProperty property, string? culture, string? segment, IDictionary>? values) + IEnumerable>> indexVals = + editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); + foreach (KeyValuePair> keyVal in indexVals) { - var editor = _propertyEditors[property.PropertyType.PropertyEditorAlias]; - if (editor == null) return; - - var indexVals = editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly); - foreach (var keyVal in indexVals) + if (keyVal.Key.IsNullOrWhiteSpace()) { - if (keyVal.Key.IsNullOrWhiteSpace()) continue; + continue; + } - var cultureSuffix = culture == null ? string.Empty : "_" + culture; + var cultureSuffix = culture == null ? string.Empty : "_" + culture; - foreach (var val in keyVal.Value) + foreach (var val in keyVal.Value) + { + switch (val) { - switch (val) - { - //only add the value if its not null or empty (we'll check for string explicitly here too) - case null: + // only add the value if its not null or empty (we'll check for string explicitly here too) + case null: + continue; + case string strVal: + { + if (strVal.IsNullOrWhiteSpace()) + { continue; - case string strVal: - { - if (strVal.IsNullOrWhiteSpace()) continue; - var key = $"{keyVal.Key}{cultureSuffix}"; - if (values?.TryGetValue(key, out var v) ?? false) - values[key] = new List(v) { val }.ToArray(); - else - values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); - } - break; - default: - { - var key = $"{keyVal.Key}{cultureSuffix}"; - if (values?.TryGetValue(key, out var v) ?? false) - values[key] = new List(v) { val }.ToArray(); - else - values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); - } + } - break; + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values?.TryGetValue(key, out IEnumerable? v) ?? false) + { + values[key] = new List(v) { val }.ToArray(); + } + else + { + values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } } + + break; + default: + { + var key = $"{keyVal.Key}{cultureSuffix}"; + if (values?.TryGetValue(key, out IEnumerable? v) ?? false) + { + values[key] = new List(v) { val }.ToArray(); + } + else + { + values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield()); + } + } + + break; } } } } - } diff --git a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs index c16370aff8..647429ebc4 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentIndexPopulator.cs @@ -1,175 +1,165 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Examine; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a content index +/// +public class ContentIndexPopulator : IndexPopulator { + private readonly IContentService _contentService; + private readonly IValueSetBuilder _contentValueSetBuilder; + private readonly ILogger _logger; + private readonly int? _parentId; + + private readonly bool _publishedValuesOnly; + private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + /// - /// Performs the data lookups required to rebuild a content index + /// This is a static query, it's parameters don't change so store statically /// - public class ContentIndexPopulator : IndexPopulator + private IQuery? _publishedQuery; + + /// + /// Default constructor to lookup all content data + /// + public ContentIndexPopulator( + ILogger logger, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IContentValueSetBuilder contentValueSetBuilder) + : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) { - private readonly IContentService _contentService; - private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; - private readonly IValueSetBuilder _contentValueSetBuilder; + } - /// - /// This is a static query, it's parameters don't change so store statically - /// - private IQuery? _publishedQuery; - private IQuery PublishedQuery => _publishedQuery ??= _umbracoDatabaseFactory.SqlContext.Query().Where(x => x.Published); + /// + /// Optional constructor allowing specifying custom query parameters + /// + public ContentIndexPopulator( + ILogger logger, + bool publishedValuesOnly, + int? parentId, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IValueSetBuilder contentValueSetBuilder) + { + _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + _umbracoDatabaseFactory = umbracoDatabaseFactory ?? throw new ArgumentNullException(nameof(umbracoDatabaseFactory)); + _contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _publishedValuesOnly = publishedValuesOnly; + _parentId = parentId; + } - private readonly bool _publishedValuesOnly; - private readonly int? _parentId; - private readonly ILogger _logger; + private IQuery PublishedQuery => _publishedQuery ??= + _umbracoDatabaseFactory.SqlContext.Query().Where(x => x.Published); - /// - /// Default constructor to lookup all content data - /// - /// - /// - /// - public ContentIndexPopulator( - ILogger logger, - IContentService contentService, - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IContentValueSetBuilder contentValueSetBuilder) - : this(logger, false, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) + public override bool IsRegistered(IUmbracoContentIndex index) => + + // check if it should populate based on published values + _publishedValuesOnly == index.PublishedValuesOnly; + + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { + _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); + return; } - /// - /// Optional constructor allowing specifying custom query parameters - /// - public ContentIndexPopulator( - ILogger logger, - bool publishedValuesOnly, - int? parentId, - IContentService contentService, - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IValueSetBuilder contentValueSetBuilder) + const int pageSize = 10000; + var pageIndex = 0; + + var contentParentId = -1; + if (_parentId.HasValue && _parentId.Value > 0) { - _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); - _umbracoDatabaseFactory = umbracoDatabaseFactory ?? throw new ArgumentNullException(nameof(umbracoDatabaseFactory)); - _contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _publishedValuesOnly = publishedValuesOnly; - _parentId = parentId; + contentParentId = _parentId.Value; } - public override bool IsRegistered(IUmbracoContentIndex index) + if (_publishedValuesOnly) { - // check if it should populate based on published values - return _publishedValuesOnly == index.PublishedValuesOnly; + IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); } - - protected override void PopulateIndexes(IReadOnlyList indexes) + else { - if (indexes.Count == 0) - { - _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); - return; - } - - const int pageSize = 10000; - var pageIndex = 0; - - var contentParentId = -1; - if (_parentId.HasValue && _parentId.Value > 0) - { - contentParentId = _parentId.Value; - } - - if (_publishedValuesOnly) - { - IndexPublishedContent(contentParentId, pageIndex, pageSize, indexes); - } - else - { - IndexAllContent(contentParentId, pageIndex, pageSize, indexes); - } - } - - protected void IndexAllContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) - { - IContent[] content; - - do - { - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); - - if (content.Length > 0) - { - var valueSets = _contentValueSetBuilder.GetValueSets(content).ToList(); - - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - { - index.IndexItems(valueSets); - } - } - - pageIndex++; - } while (content.Length == pageSize); - } - - protected void IndexPublishedContent(int contentParentId, int pageIndex, int pageSize, - IReadOnlyList indexes) - { - IContent[] content; - - var publishedPages = new HashSet(); - - do - { - //add the published filter - //note: We will filter for published variants in the validator - content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, PublishedQuery, - Ordering.By("Path", Direction.Ascending)).ToArray(); - - - if (content.Length > 0) - { - var indexableContent = new List(); - - foreach (var item in content) - { - if (item.Level == 1) - { - // first level pages are always published so no need to filter them - indexableContent.Add(item); - publishedPages.Add(item.Id); - } - else - { - if (publishedPages.Contains(item.ParentId)) - { - // only index when parent is published - publishedPages.Add(item.Id); - indexableContent.Add(item); - } - } - } - - var valueSets = _contentValueSetBuilder.GetValueSets(indexableContent.ToArray()).ToList(); - - foreach (IIndex index in indexes) - { - index.IndexItems(valueSets); - } - } - - pageIndex++; - } while (content.Length == pageSize); + IndexAllContent(contentParentId, pageIndex, pageSize, indexes); } } + protected void IndexAllContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) + { + IContent[] content; + do + { + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _).ToArray(); + + if (content.Length > 0) + { + var valueSets = _contentValueSetBuilder.GetValueSets(content).ToList(); + + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) + { + index.IndexItems(valueSets); + } + } + + pageIndex++; + } + while (content.Length == pageSize); + } + + protected void IndexPublishedContent(int contentParentId, int pageIndex, int pageSize, IReadOnlyList indexes) + { + IContent[] content; + + var publishedPages = new HashSet(); + + do + { + // add the published filter + // note: We will filter for published variants in the validator + content = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out _, PublishedQuery, Ordering.By("Path")).ToArray(); + + if (content.Length > 0) + { + var indexableContent = new List(); + + foreach (IContent item in content) + { + if (item.Level == 1) + { + // first level pages are always published so no need to filter them + indexableContent.Add(item); + publishedPages.Add(item.Id); + } + else + { + if (publishedPages.Contains(item.ParentId)) + { + // only index when parent is published + publishedPages.Add(item.Id); + indexableContent.Add(item); + } + } + } + + var valueSets = _contentValueSetBuilder.GetValueSets(indexableContent.ToArray()).ToList(); + + foreach (IIndex index in indexes) + { + index.IndexItems(valueSets); + } + } + + pageIndex++; + } + while (content.Length == pageSize); + } } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs index f42293a7a1..f92079513d 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetBuilder.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; @@ -8,128 +6,143 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Builds s for items +/// +public class ContentValueSetBuilder : BaseValueSetBuilder, IContentValueSetBuilder, + IPublishedContentValueSetBuilder { - /// - /// Builds s for items - /// - public class ContentValueSetBuilder : BaseValueSetBuilder, IContentValueSetBuilder, IPublishedContentValueSetBuilder + private readonly IScopeProvider _scopeProvider; + + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IUserService _userService; + + public ContentValueSetBuilder( + PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + IShortStringHelper shortStringHelper, + IScopeProvider scopeProvider, + bool publishedValuesOnly) + : base(propertyEditors, publishedValuesOnly) { - private readonly UrlSegmentProviderCollection _urlSegmentProviders; - private readonly IUserService _userService; - private readonly IScopeProvider _scopeProvider; - - private readonly IShortStringHelper _shortStringHelper; - - public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, - UrlSegmentProviderCollection urlSegmentProviders, - IUserService userService, - IShortStringHelper shortStringHelper, - IScopeProvider scopeProvider, - bool publishedValuesOnly) - : base(propertyEditors, publishedValuesOnly) - { - _urlSegmentProviders = urlSegmentProviders; - _userService = userService; - _shortStringHelper = shortStringHelper; - _scopeProvider = scopeProvider; - } - - /// - public override IEnumerable GetValueSets(params IContent[] content) - { - Dictionary creatorIds; - Dictionary writerIds; - - // We can lookup all of the creator/writer names at once which can save some - // processing below instead of one by one. - using (var scope = _scopeProvider.CreateScope()) - { - creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) - .ToDictionary(x => x.Id, x => x); - writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) - .ToDictionary(x => x.Id, x => x); - scope.Complete(); - } - - return GetValueSetsEnumerable(content, creatorIds, writerIds); - } - - private IEnumerable GetValueSetsEnumerable(IContent[] content, Dictionary creatorIds, Dictionary writerIds) - { - // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways - // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since - // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` - // references and then each array is an array of `FieldValue[]` and values are assigned accordingly. Not sure if it will make a difference or not. - - foreach (var c in content) - { - var isVariant = c.ContentType.VariesByCulture(); - - var urlValue = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); //Always add invariant urlName - var values = new Dictionary> - { - {"icon", c.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {UmbracoExamineFieldNames.PublishedFieldName, new object[] {c.Published ? "y" : "n"}}, //Always add invariant published value - {"id", new object[] {c.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {c.Key}}, - {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, - {"level", new object[] {c.Level}}, - {"creatorID", new object[] {c.CreatorId}}, - {"sortOrder", new object[] {c.SortOrder}}, - {"createDate", new object[] {c.CreateDate}}, //Always add invariant createDate - {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate - {UmbracoExamineFieldNames.NodeNameFieldName, (PublishedValuesOnly //Always add invariant nodeName - ? c.PublishName?.Yield() - : c.Name?.Yield()) ?? Enumerable.Empty()}, - {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName - {"path", c.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name! : "??").Yield() }, - {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name! : "??").Yield() }, - {"writerID", new object[] {c.WriterId}}, - {"templateID", new object[] {c.TemplateId ?? 0}}, - {UmbracoExamineFieldNames.VariesByCultureFieldName, new object[] {"n"}}, - }; - - if (isVariant) - { - values[UmbracoExamineFieldNames.VariesByCultureFieldName] = new object[] { "y" }; - - foreach (var culture in c.AvailableCultures) - { - var variantUrl = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, culture); - var lowerCulture = culture.ToLowerInvariant(); - values[$"urlName_{lowerCulture}"] = variantUrl?.Yield() ?? Enumerable.Empty(); - values[$"nodeName_{lowerCulture}"] = (PublishedValuesOnly - ? c.GetPublishName(culture)?.Yield() - : c.GetCultureName(culture)?.Yield()) ?? Enumerable.Empty(); - values[$"{UmbracoExamineFieldNames.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? "y" : "n").Yield(); - values[$"updateDate_{lowerCulture}"] = (PublishedValuesOnly - ? c.GetPublishDate(culture) - : c.GetUpdateDate(culture))?.Yield() ?? Enumerable.Empty(); - } - } - - foreach (var property in c.Properties) - { - if (!property.PropertyType.VariesByCulture()) - { - AddPropertyValue(property, null, null, values); - } - else - { - foreach (var culture in c.AvailableCultures) - AddPropertyValue(property, culture.ToLowerInvariant(), null, values); - } - } - - var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); - - yield return vs; - } - } + _urlSegmentProviders = urlSegmentProviders; + _userService = userService; + _shortStringHelper = shortStringHelper; + _scopeProvider = scopeProvider; } + /// + public override IEnumerable GetValueSets(params IContent[] content) + { + Dictionary creatorIds; + Dictionary writerIds; + + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (IScope scope = _scopeProvider.CreateScope()) + { + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); + } + + return GetValueSetsEnumerable(content, creatorIds, writerIds); + } + + private IEnumerable GetValueSetsEnumerable(IContent[] content, Dictionary creatorIds, Dictionary writerIds) + { + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways + // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since + // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` + // references and then each array is an array of `FieldValue[]` and values are assigned accordingly. Not sure if it will make a difference or not. + foreach (IContent c in content) + { + var isVariant = c.ContentType.VariesByCulture(); + + var urlValue = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); // Always add invariant urlName + var values = new Dictionary> + { + { "icon", c.ContentType.Icon?.Yield() ?? Enumerable.Empty() }, + { + UmbracoExamineFieldNames.PublishedFieldName, new object[] { c.Published ? "y" : "n" } + }, // Always add invariant published value + { "id", new object[] { c.Id } }, + { UmbracoExamineFieldNames.NodeKeyFieldName, new object[] { c.Key } }, + { "parentID", new object[] { c.Level > 1 ? c.ParentId : -1 } }, + { "level", new object[] { c.Level } }, + { "creatorID", new object[] { c.CreatorId } }, + { "sortOrder", new object[] { c.SortOrder } }, + { "createDate", new object[] { c.CreateDate } }, // Always add invariant createDate + { "updateDate", new object[] { c.UpdateDate } }, // Always add invariant updateDate + { + UmbracoExamineFieldNames.NodeNameFieldName, (PublishedValuesOnly // Always add invariant nodeName + ? c.PublishName?.Yield() + : c.Name?.Yield()) ?? Enumerable.Empty() + }, + { "urlName", urlValue?.Yield() ?? Enumerable.Empty() }, // Always add invariant urlName + { "path", c.Path.Yield() }, + { "nodeType", c.ContentType.Id.ToString().Yield() }, + { + "creatorName", + (creatorIds.TryGetValue(c.CreatorId, out IProfile? creatorProfile) ? creatorProfile.Name! : "??") + .Yield() + }, + { + "writerName", + (writerIds.TryGetValue(c.WriterId, out IProfile? writerProfile) ? writerProfile.Name! : "??") + .Yield() + }, + { "writerID", new object[] { c.WriterId } }, + { "templateID", new object[] { c.TemplateId ?? 0 } }, + { UmbracoExamineFieldNames.VariesByCultureFieldName, new object[] { "n" } }, + }; + + if (isVariant) + { + values[UmbracoExamineFieldNames.VariesByCultureFieldName] = new object[] { "y" }; + + foreach (var culture in c.AvailableCultures) + { + var variantUrl = c.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, culture); + var lowerCulture = culture.ToLowerInvariant(); + values[$"urlName_{lowerCulture}"] = variantUrl?.Yield() ?? Enumerable.Empty(); + values[$"nodeName_{lowerCulture}"] = (PublishedValuesOnly + ? c.GetPublishName(culture)?.Yield() + : c.GetCultureName(culture)?.Yield()) ?? Enumerable.Empty(); + values[$"{UmbracoExamineFieldNames.PublishedFieldName}_{lowerCulture}"] = + (c.IsCulturePublished(culture) ? "y" : "n").Yield(); + values[$"updateDate_{lowerCulture}"] = (PublishedValuesOnly + ? c.GetPublishDate(culture) + : c.GetUpdateDate(culture))?.Yield() ?? Enumerable.Empty(); + } + } + + foreach (IProperty property in c.Properties) + { + if (!property.PropertyType.VariesByCulture()) + { + AddPropertyValue(property, null, null, values); + } + else + { + foreach (var culture in c.AvailableCultures) + { + AddPropertyValue(property, culture.ToLowerInvariant(), null, values); + } + } + } + + var vs = new ValueSet(c.Id.ToInvariantString(), IndexTypes.Content, c.ContentType.Alias, values); + + yield return vs; + } + } } diff --git a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs index ed57184cb8..7120bd37d4 100644 --- a/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/ContentValueSetValidator.cs @@ -1,161 +1,191 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Examine; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to validate a ValueSet for content/media - based on permissions, parent id, etc.... +/// +public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator { - /// - /// Used to validate a ValueSet for content/media - based on permissions, parent id, etc.... - /// - public class ContentValueSetValidator : ValueSetValidator, IContentValueSetValidator + private const string PathKey = "path"; + private static readonly IEnumerable ValidCategories = new[] {IndexTypes.Content, IndexTypes.Media}; + private readonly IPublicAccessService? _publicAccessService; + private readonly IScopeProvider? _scopeProvider; + + // used for tests + public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) + : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) { - private readonly IPublicAccessService? _publicAccessService; - private readonly IScopeProvider? _scopeProvider; - private const string PathKey = "path"; - private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Content, IndexTypes.Media }; - protected override IEnumerable ValidIndexCategories => ValidCategories; + } - public bool PublishedValuesOnly { get; } - public bool SupportProtectedContent { get; } - public int? ParentId { get; } + public ContentValueSetValidator( + bool publishedValuesOnly, + bool supportProtectedContent, + IPublicAccessService? publicAccessService, + IScopeProvider? scopeProvider, + int? parentId = null, + IEnumerable? includeItemTypes = null, + IEnumerable? excludeItemTypes = null) + : base(includeItemTypes, excludeItemTypes, null, null) + { + PublishedValuesOnly = publishedValuesOnly; + SupportProtectedContent = supportProtectedContent; + ParentId = parentId; + _publicAccessService = publicAccessService; + _scopeProvider = scopeProvider; + } - public bool ValidatePath(string path, string category) + protected override IEnumerable ValidIndexCategories => ValidCategories; + + public bool PublishedValuesOnly { get; } + public bool SupportProtectedContent { get; } + public int? ParentId { get; } + + public bool ValidatePath(string path, string category) + { + //check if this document is a descendent of the parent + if (ParentId.HasValue && ParentId.Value > 0) { - //check if this document is a descendent of the parent - if (ParentId.HasValue && ParentId.Value > 0) + // we cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by parent Id in the cases that umbraco data is moved to an illegal parent. + if (!path.Contains(string.Concat(",", ParentId.Value.ToString(CultureInfo.InvariantCulture), ","))) { - // we cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there - // because we need to remove anything that doesn't pass by parent Id in the cases that umbraco data is moved to an illegal parent. - if (!path.Contains(string.Concat(",", ParentId.Value.ToString(CultureInfo.InvariantCulture), ","))) - return false; + return false; } - - return true; } - public bool ValidateRecycleBin(string path, string category) - { - var recycleBinId = category == IndexTypes.Content ? Constants.System.RecycleBinContentString : Constants.System.RecycleBinMediaString; + return true; + } - //check for recycle bin - if (PublishedValuesOnly) + public bool ValidateRecycleBin(string path, string category) + { + var recycleBinId = category == IndexTypes.Content + ? Constants.System.RecycleBinContentString + : Constants.System.RecycleBinMediaString; + + //check for recycle bin + if (PublishedValuesOnly) + { + if (path.Contains(string.Concat(",", recycleBinId, ","))) { - if (path.Contains(string.Concat(",", recycleBinId, ","))) - return false; + return false; } - return true; } - public bool ValidateProtectedContent(string path, string category) + return true; + } + + public bool ValidateProtectedContent(string path, string category) + { + if (category == IndexTypes.Content && !SupportProtectedContent) { - if (category == IndexTypes.Content && !SupportProtectedContent) + //if the service is null we can't look this up so we'll return false + if (_publicAccessService == null || _scopeProvider == null) { - //if the service is null we can't look this up so we'll return false - if (_publicAccessService == null || _scopeProvider == null) + return false; + } + + // explicit scope since we may be in a background thread + using (_scopeProvider.CreateScope(autoComplete: true)) + { + if (_publicAccessService.IsProtected(path).Success) { return false; } - - // explicit scope since we may be in a background thread - using (_scopeProvider.CreateScope(autoComplete: true)) - { - if (_publicAccessService.IsProtected(path).Success) - { - return false; - } - } } - - return true; } - // used for tests - public ContentValueSetValidator(bool publishedValuesOnly, int? parentId = null, - IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) - : this(publishedValuesOnly, true, null, null, parentId, includeItemTypes, excludeItemTypes) + return true; + } + + public override ValueSetValidationResult Validate(ValueSet valueSet) + { + ValueSetValidationResult baseValidate = base.Validate(valueSet); + valueSet = baseValidate.ValueSet; + if (baseValidate.Status == ValueSetValidationStatus.Failed) { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); } - public ContentValueSetValidator(bool publishedValuesOnly, bool supportProtectedContent, - IPublicAccessService? publicAccessService, - IScopeProvider? scopeProvider, - int? parentId = null, - IEnumerable? includeItemTypes = null, IEnumerable? excludeItemTypes = null) - : base(includeItemTypes, excludeItemTypes, null, null) - { - PublishedValuesOnly = publishedValuesOnly; - SupportProtectedContent = supportProtectedContent; - ParentId = parentId; - _publicAccessService = publicAccessService; - _scopeProvider = scopeProvider; - } + var isFiltered = baseValidate.Status == ValueSetValidationStatus.Filtered; - public override ValueSetValidationResult Validate(ValueSet valueSet) + var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); + //check for published content + if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) { - var baseValidate = base.Validate(valueSet); - valueSet = baseValidate.ValueSet; - if (baseValidate.Status == ValueSetValidationStatus.Failed) + if (!valueSet.Values.TryGetValue(UmbracoExamineFieldNames.PublishedFieldName, out IReadOnlyList? published)) + { return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - var isFiltered = baseValidate.Status == ValueSetValidationStatus.Filtered; - - var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - //check for published content - if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) + if (!published[0].Equals("y")) { - if (!valueSet.Values.TryGetValue(UmbracoExamineFieldNames.PublishedFieldName, out var published)) - { - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - } + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - if (!published[0].Equals("y")) + //deal with variants, if there are unpublished variants than we need to remove them from the value set + if (valueSet.Values.TryGetValue(UmbracoExamineFieldNames.VariesByCultureFieldName, out IReadOnlyList? variesByCulture) + && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) + { + //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values + foreach (KeyValuePair> publishField in valueSet.Values + .Where(x => x.Key.StartsWith($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) { - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - } - - //deal with variants, if there are unpublished variants than we need to remove them from the value set - if (valueSet.Values.TryGetValue(UmbracoExamineFieldNames.VariesByCultureFieldName, out var variesByCulture) - && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) - { - //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values - foreach (var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineFieldNames.PublishedFieldName}_")).ToList()) + if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) { - if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) + //this culture is not published, so remove all of these culture values + var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); + foreach (KeyValuePair> cultureField in valueSet.Values + .Where(x => x.Key.InvariantEndsWith(cultureSuffix)).ToList()) { - //this culture is not published, so remove all of these culture values - var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); - foreach (var cultureField in valueSet.Values.Where(x => x.Key.InvariantEndsWith(cultureSuffix)).ToList()) - { - filteredValues.Remove(cultureField.Key); - isFiltered = true; - } + filteredValues.Remove(cultureField.Key); + isFiltered = true; } } } } - - //must have a 'path' - if (!valueSet.Values.TryGetValue(PathKey, out var pathValues)) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues.Count == 0) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues[0] == null) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - if (pathValues[0].ToString().IsNullOrWhiteSpace()) return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); - var path = pathValues[0].ToString(); - - var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x=>x.Key, x=> (IEnumerable)x.Value)); - // We need to validate the path of the content based on ParentId, protected content and recycle bin rules. - // We cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there - // because we need to remove anything that doesn't pass by protected content in the cases that umbraco data is moved to an illegal parent. - if (!ValidatePath(path!, valueSet.Category) - || !ValidateRecycleBin(path!, valueSet.Category) - || !ValidateProtectedContent(path!, valueSet.Category)) - return new ValueSetValidationResult(ValueSetValidationStatus.Filtered, filteredValueSet); - - return new ValueSetValidationResult(isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } + + //must have a 'path' + if (!valueSet.Values.TryGetValue(PathKey, out IReadOnlyList? pathValues)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues.Count == 0) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues[0] == null) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + if (pathValues[0].ToString().IsNullOrWhiteSpace()) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } + + var path = pathValues[0].ToString(); + + var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); + // We need to validate the path of the content based on ParentId, protected content and recycle bin rules. + // We cannot return FAILED here because we need the value set to get into the indexer and then deal with it from there + // because we need to remove anything that doesn't pass by protected content in the cases that umbraco data is moved to an illegal parent. + if (!ValidatePath(path!, valueSet.Category) + || !ValidateRecycleBin(path!, valueSet.Category) + || !ValidateProtectedContent(path!, valueSet.Category)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Filtered, filteredValueSet); + } + + return new ValueSetValidationResult( + isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs index 076353a990..6ac45b4184 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineExtensions.cs @@ -1,94 +1,106 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Examine; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Extension methods for Examine. +/// +public static class ExamineExtensions { /// - /// Extension methods for Examine. + /// Creates an containing all content from the + /// . /// - public static class ExamineExtensions + /// The search results. + /// The cache to fetch the content from. + /// + /// An containing all content. + /// + /// cache + /// + /// Search results are skipped if it can't be fetched from the by its integer id. + /// + public static IEnumerable ToPublishedSearchResults( + this IEnumerable results, + IPublishedCache? cache) { - /// - /// Creates an containing all content from the . - /// - /// The search results. - /// The cache to fetch the content from. - /// - /// An containing all content. - /// - /// cache - /// - /// Search results are skipped if it can't be fetched from the by its integer id. - /// - public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedCache? cache) + if (cache == null) { - if (cache == null) throw new ArgumentNullException(nameof(cache)); + throw new ArgumentNullException(nameof(cache)); + } - var publishedSearchResults = new List(); + var publishedSearchResults = new List(); - foreach (var result in results) + foreach (ISearchResult result in results) + { + if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId)) { - if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && - cache.GetById(contentId) is IPublishedContent content) + IPublishedContent? content = cache.GetById(contentId); + if (content is not null) { publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); } } - - return publishedSearchResults; } - /// - /// Creates an containing all content, media or members from the . - /// - /// The search results. - /// The snapshot. - /// - /// An containing all content, media or members. - /// - /// snapshot - /// - /// Search results are skipped if it can't be fetched from the respective cache by its integer id. - /// - public static IEnumerable ToPublishedSearchResults(this IEnumerable results, IPublishedSnapshot snapshot) + return publishedSearchResults; + } + + /// + /// Creates an containing all content, media or members from the + /// . + /// + /// The search results. + /// The snapshot. + /// + /// An containing all content, media or members. + /// + /// snapshot + /// + /// Search results are skipped if it can't be fetched from the respective cache by its integer id. + /// + public static IEnumerable ToPublishedSearchResults( + this IEnumerable results, + IPublishedSnapshot snapshot) + { + if (snapshot == null) { - if (snapshot == null) throw new ArgumentNullException(nameof(snapshot)); + throw new ArgumentNullException(nameof(snapshot)); + } - var publishedSearchResults = new List(); + var publishedSearchResults = new List(); - foreach (var result in results) + foreach (ISearchResult result in results) + { + if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && + result.Values.TryGetValue(ExamineFieldNames.CategoryFieldName, out var indexType)) { - if (int.TryParse(result.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentId) && - result.Values.TryGetValue(ExamineFieldNames.CategoryFieldName, out var indexType)) + IPublishedContent? content; + switch (indexType) { - IPublishedContent? content; - switch (indexType) - { - case IndexTypes.Content: - content = snapshot.Content?.GetById(contentId); - break; - case IndexTypes.Media: - content = snapshot.Media?.GetById(contentId); - break; - case IndexTypes.Member: - throw new NotSupportedException("Cannot convert search results to member instances"); - default: - continue; - } + case IndexTypes.Content: + content = snapshot.Content?.GetById(contentId); + break; + case IndexTypes.Media: + content = snapshot.Media?.GetById(contentId); + break; + case IndexTypes.Member: + throw new NotSupportedException("Cannot convert search results to member instances"); + default: + continue; + } - if (content != null) - { - publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); - } + if (content != null) + { + publishedSearchResults.Add(new PublishedSearchResult(content, result.Score)); } } - - return publishedSearchResults; } + + return publishedSearchResults; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs index bb5a4f5a46..681efa0326 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexModel.cs @@ -1,25 +1,22 @@ -using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +[DataContract(Name = "indexer", Namespace = "")] +public class ExamineIndexModel { - [DataContract(Name = "indexer", Namespace = "")] - public class ExamineIndexModel - { - [DataMember(Name = "name")] - public string? Name { get; set; } + [DataMember(Name = "name")] + public string? Name { get; set; } - [DataMember(Name = "healthStatus")] - public string? HealthStatus { get; set; } + [DataMember(Name = "healthStatus")] + public string? HealthStatus { get; set; } - [DataMember(Name = "isHealthy")] - public bool IsHealthy => HealthStatus == "Healthy"; + [DataMember(Name = "isHealthy")] + public bool IsHealthy => HealthStatus == "Healthy"; - [DataMember(Name = "providerProperties")] - public IReadOnlyDictionary? ProviderProperties { get; set; } + [DataMember(Name = "providerProperties")] + public IReadOnlyDictionary? ProviderProperties { get; set; } - [DataMember(Name = "canRebuild")] - public bool CanRebuild { get; set; } - - } + [DataMember(Name = "canRebuild")] + public bool CanRebuild { get; set; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index ef6d361970..a1c70d0ec3 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -1,11 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; @@ -13,203 +8,205 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.HostedServices; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class ExamineIndexRebuilder : IIndexRebuilder { - public class ExamineIndexRebuilder : IIndexRebuilder + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IExamineManager _examineManager; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IEnumerable _populators; + private readonly object _rebuildLocker = new(); + private readonly IRuntimeState _runtimeState; + + /// + /// Initializes a new instance of the class. + /// + public ExamineIndexRebuilder( + IMainDom mainDom, + IRuntimeState runtimeState, + ILogger logger, + IExamineManager examineManager, + IEnumerable populators, + IBackgroundTaskQueue backgroundTaskQueue) { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly IMainDom _mainDom; - private readonly IRuntimeState _runtimeState; - private readonly ILogger _logger; - private readonly IExamineManager _examineManager; - private readonly IEnumerable _populators; - private readonly object _rebuildLocker = new(); + _mainDom = mainDom; + _runtimeState = runtimeState; + _logger = logger; + _examineManager = examineManager; + _populators = populators; + _backgroundTaskQueue = backgroundTaskQueue; + } - /// - /// Initializes a new instance of the class. - /// - public ExamineIndexRebuilder( - IMainDom mainDom, - IRuntimeState runtimeState, - ILogger logger, - IExamineManager examineManager, - IEnumerable populators, - IBackgroundTaskQueue backgroundTaskQueue) + public bool CanRebuild(string indexName) + { + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) { - _mainDom = mainDom; - _runtimeState = runtimeState; - _logger = logger; - _examineManager = examineManager; - _populators = populators; - _backgroundTaskQueue = backgroundTaskQueue; + throw new InvalidOperationException("No index found by name " + indexName); } - public bool CanRebuild(string indexName) - { - if (!_examineManager.TryGetIndex(indexName, out IIndex index)) - { - throw new InvalidOperationException("No index found by name " + indexName); - } + return _populators.Any(x => x.IsRegistered(index)); + } - return _populators.Any(x => x.IsRegistered(index)); + public virtual void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) + { + if (delay == null) + { + delay = TimeSpan.Zero; } - public virtual void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) + if (!CanRun()) { - if (delay == null) - { - delay = TimeSpan.Zero; - } + return; + } - if (!CanRun()) - { - return; - } + if (useBackgroundThread) + { + _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName); - if (useBackgroundThread) - { - _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.",indexName); + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken))); + } + else + { + RebuildIndex(indexName, delay.Value, CancellationToken.None); + } + } - _backgroundTaskQueue.QueueBackgroundWorkItem( - cancellationToken => Task.Run(() => RebuildIndex(indexName, delay.Value, cancellationToken))); + public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) + { + if (delay == null) + { + delay = TimeSpan.Zero; + } + + if (!CanRun()) + { + return; + } + + if (useBackgroundThread) + { + _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); + + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => + { + // This is a fire/forget task spawned by the background thread queue (which means we + // don't need to worry about ExecutionContext flowing). + Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); + + // immediately return so the queue isn't waiting. + return Task.CompletedTask; + }); + } + else + { + RebuildIndexes(onlyEmptyIndexes, delay.Value, CancellationToken.None); + } + } + + private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; + + private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) + { + if (delay > TimeSpan.Zero) + { + Thread.Sleep(delay); + } + + try + { + if (!Monitor.TryEnter(_rebuildLocker)) + { + _logger.LogWarning( + "Call was made to RebuildIndexes but the task runner for rebuilding is already running"); } else { - RebuildIndex(indexName, delay.Value, CancellationToken.None); - } - } - - public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) - { - if (delay == null) - { - delay = TimeSpan.Zero; - } - - if (!CanRun()) - { - return; - } - - if (useBackgroundThread) - { - _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); - - _backgroundTaskQueue.QueueBackgroundWorkItem( - cancellationToken => - { - // This is a fire/forget task spawned by the background thread queue (which means we - // don't need to worry about ExecutionContext flowing). - Task.Run(() => RebuildIndexes(onlyEmptyIndexes, delay.Value, cancellationToken)); - - // immediately return so the queue isn't waiting. - return Task.CompletedTask; - }); - } - else - { - RebuildIndexes(onlyEmptyIndexes, delay.Value, CancellationToken.None); - } - } - - private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; - - private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) - { - if (delay > TimeSpan.Zero) - { - Thread.Sleep(delay); - } - - try - { - if (!Monitor.TryEnter(_rebuildLocker)) + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) { - _logger.LogWarning("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); + throw new InvalidOperationException($"No index found with name {indexName}"); } - else + + index.CreateIndex(); // clear the index + foreach (IIndexPopulator populator in _populators) { - if (!_examineManager.TryGetIndex(indexName, out IIndex index)) - { - throw new InvalidOperationException($"No index found with name {indexName}"); - } - - index.CreateIndex(); // clear the index - foreach (IIndexPopulator populator in _populators) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - populator.Populate(index); - } - } - } - finally - { - if (Monitor.IsEntered(_rebuildLocker)) - { - Monitor.Exit(_rebuildLocker); - } - } - } - - private void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) - { - if (delay > TimeSpan.Zero) - { - Thread.Sleep(delay); - } - - try - { - if (!Monitor.TryEnter(_rebuildLocker)) - { - _logger.LogWarning($"Call was made to {nameof(RebuildIndexes)} but the task runner for rebuilding is already running"); - } - else - { - // If an index exists but it has zero docs we'll consider it empty and rebuild - IIndex[] indexes = (onlyEmptyIndexes - ? _examineManager.Indexes.Where(x => !x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0)) - : _examineManager.Indexes).ToArray(); - - if (indexes.Length == 0) + if (cancellationToken.IsCancellationRequested) { return; } - foreach (IIndex index in indexes) + populator.Populate(index); + } + } + } + finally + { + if (Monitor.IsEntered(_rebuildLocker)) + { + Monitor.Exit(_rebuildLocker); + } + } + } + + private void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) + { + if (delay > TimeSpan.Zero) + { + Thread.Sleep(delay); + } + + try + { + if (!Monitor.TryEnter(_rebuildLocker)) + { + _logger.LogWarning( + $"Call was made to {nameof(RebuildIndexes)} but the task runner for rebuilding is already running"); + } + else + { + // If an index exists but it has zero docs we'll consider it empty and rebuild + IIndex[] indexes = (onlyEmptyIndexes + ? _examineManager.Indexes.Where(x => + !x.IndexExists() || (x is IIndexStats stats && stats.GetDocumentCount() == 0)) + : _examineManager.Indexes).ToArray(); + + if (indexes.Length == 0) + { + return; + } + + foreach (IIndex index in indexes) + { + index.CreateIndex(); // clear the index + } + + // run each populator over the indexes + foreach (IIndexPopulator populator in _populators) + { + if (cancellationToken.IsCancellationRequested) { - index.CreateIndex(); // clear the index + return; } - // run each populator over the indexes - foreach (IIndexPopulator populator in _populators) + try { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - try - { - populator.Populate(indexes); - } - catch (Exception e) - { - _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); - } + populator.Populate(indexes); + } + catch (Exception e) + { + _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); } } } - finally + } + finally + { + if (Monitor.IsEntered(_rebuildLocker)) { - if (Monitor.IsEntered(_rebuildLocker)) - { - Monitor.Exit(_rebuildLocker); - } + Monitor.Exit(_rebuildLocker); } } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs b/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs index 1fd30de319..aa99ce3fdb 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineSearcherModel.cs @@ -1,17 +1,10 @@ using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +[DataContract(Name = "searcher", Namespace = "")] +public class ExamineSearcherModel { - [DataContract(Name = "searcher", Namespace = "")] - public class ExamineSearcherModel - { - public ExamineSearcherModel() - { - } - - [DataMember(Name = "name")] - public string? Name { get; set; } - - } - + [DataMember(Name = "name")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs index c8a07f6193..fb3d7e0720 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineUmbracoIndexingHandler.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Examine; using Examine.Search; using Microsoft.Extensions.Logging; @@ -14,425 +10,451 @@ using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Indexing handler for Examine indexes +/// +internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler { - /// - /// Indexing handler for Examine indexes - /// - internal class ExamineUmbracoIndexingHandler : IUmbracoIndexingHandler + // the default enlist priority is 100 + // enlist with a lower priority to ensure that anything "default" runs after us + // but greater that SafeXmlReaderWriter priority which is 60 + private const int EnlistPriority = 80; + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IContentValueSetBuilder _contentValueSetBuilder; + private readonly Lazy _enabled; + private readonly IExamineManager _examineManager; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IValueSetBuilder _mediaValueSetBuilder; + private readonly IValueSetBuilder _memberValueSetBuilder; + private readonly IProfilingLogger _profilingLogger; + private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; + private readonly ICoreScopeProvider _scopeProvider; + + public ExamineUmbracoIndexingHandler( + IMainDom mainDom, + ILogger logger, + IProfilingLogger profilingLogger, + ICoreScopeProvider scopeProvider, + IExamineManager examineManager, + IBackgroundTaskQueue backgroundTaskQueue, + IContentValueSetBuilder contentValueSetBuilder, + IPublishedContentValueSetBuilder publishedContentValueSetBuilder, + IValueSetBuilder mediaValueSetBuilder, + IValueSetBuilder memberValueSetBuilder) { - // the default enlist priority is 100 - // enlist with a lower priority to ensure that anything "default" runs after us - // but greater that SafeXmlReaderWriter priority which is 60 - private const int EnlistPriority = 80; - private readonly IMainDom _mainDom; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IExamineManager _examineManager; - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly IContentValueSetBuilder _contentValueSetBuilder; - private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; - private readonly IValueSetBuilder _mediaValueSetBuilder; - private readonly IValueSetBuilder _memberValueSetBuilder; - private readonly Lazy _enabled; - - public ExamineUmbracoIndexingHandler( - IMainDom mainDom, - ILogger logger, - IProfilingLogger profilingLogger, - ICoreScopeProvider scopeProvider, - IExamineManager examineManager, - IBackgroundTaskQueue backgroundTaskQueue, - IContentValueSetBuilder contentValueSetBuilder, - IPublishedContentValueSetBuilder publishedContentValueSetBuilder, - IValueSetBuilder mediaValueSetBuilder, - IValueSetBuilder memberValueSetBuilder) - { - _mainDom = mainDom; - _logger = logger; - _profilingLogger = profilingLogger; - _scopeProvider = scopeProvider; - _examineManager = examineManager; - _backgroundTaskQueue = backgroundTaskQueue; - _contentValueSetBuilder = contentValueSetBuilder; - _publishedContentValueSetBuilder = publishedContentValueSetBuilder; - _mediaValueSetBuilder = mediaValueSetBuilder; - _memberValueSetBuilder = memberValueSetBuilder; - _enabled = new Lazy(IsEnabled); - } - - /// - /// Used to lazily check if Examine Index handling is enabled - /// - /// - private bool IsEnabled() - { - //let's deal with shutting down Examine with MainDom - var examineShutdownRegistered = _mainDom.Register(release: () => - { - using (_profilingLogger.TraceDuration("Examine shutting down")) - { - _examineManager.Dispose(); - } - }); - - if (!examineShutdownRegistered) - { - _logger.LogInformation("Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); - - //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! - Suspendable.ExamineEvents.SuspendIndexers(_logger); - return false; //exit, do not continue - } - - _logger.LogDebug("Examine shutdown registered with MainDom"); - - var registeredIndexers = _examineManager.Indexes.OfType().Count(x => x.EnableDefaultEventHandler); - - _logger.LogInformation("Adding examine event handlers for {RegisteredIndexers} index providers.", registeredIndexers); - - // don't bind event handlers if we're not suppose to listen - if (registeredIndexers == 0) - { - return false; - } - - return true; - } - - /// - public bool Enabled => _enabled.Value; - - /// - public void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished)); - } - else - { - DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished); - } - } - - /// - public void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedDeleteIndex(this, entityIds, keepIfUnpublished)); - } - else - { - DeferedDeleteIndex.Execute(this, entityIds, keepIfUnpublished); - } - } - - /// - public void ReIndexForContent(IContent sender, bool isPublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForContent(_backgroundTaskQueue, this, sender, isPublished)); - } - else - { - DeferedReIndexForContent.Execute(_backgroundTaskQueue, this, sender, isPublished); - } - } - - /// - public void ReIndexForMedia(IMedia sender, bool isPublished) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForMedia(_backgroundTaskQueue, this, sender, isPublished)); - } - else - { - DeferedReIndexForMedia.Execute(_backgroundTaskQueue, this, sender, isPublished); - } - } - - /// - public void ReIndexForMember(IMember member) - { - var actions = DeferedActions.Get(_scopeProvider); - if (actions != null) - { - actions.Add(new DeferedReIndexForMember(_backgroundTaskQueue, this, member)); - } - else - { - DeferedReIndexForMember.Execute(_backgroundTaskQueue, this, member); - } - } - - /// - public void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes) - { - const int pageSize = 500; - - //Delete all content of this content/media/member type that is in any content indexer by looking up matched examine docs - foreach (var id in removedContentTypes) - { - foreach (var index in _examineManager.Indexes.OfType()) - { - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) - { - //paging with examine, see https://shazwazza.com/post/paging-with-examine/ - var results = index.Searcher - .CreateQuery() - .Field("nodeType", id.ToInvariantString()) - .Execute(QueryOptions.SkipTake(page * pageSize, pageSize)); - total = results.TotalItemCount; - - foreach (ISearchResult item in results) - { - if (int.TryParse(item.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out int contentId)) - { - DeleteIndexForEntity(contentId, false); - } - } - - page++; - } - } - } - } - - #region Deferred Actions - private class DeferedActions - { - private readonly List _actions = new List(); - - public static DeferedActions? Get(ICoreScopeProvider scopeProvider) - { - IScopeContext? scopeContext = scopeProvider.Context; - - return scopeContext?.Enlist("examineEvents", - () => new DeferedActions(), // creator - (completed, actions) => // action - { - if (completed) - { - actions?.Execute(); - } - }, EnlistPriority); - } - - public void Add(DeferedAction action) => _actions.Add(action); - - private void Execute() - { - foreach (DeferedAction action in _actions) - { - action.Execute(); - } - } - } - - /// - /// An action that will execute at the end of the Scope being completed - /// - private abstract class DeferedAction - { - public virtual void Execute() - { } - } - - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForContent : DeferedAction - { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IContent _content; - private readonly bool _isPublished; - - public DeferedReIndexForContent(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) - { - _backgroundTaskQueue = backgroundTaskQueue; - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _content = content; - _isPublished = isPublished; - } - - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _content, _isPublished); - - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) - => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => - { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - - // for content we have a different builder for published vs unpublished - // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published - var builders = new Dictionary>> - { - [true] = new Lazy>(() => examineUmbracoIndexingHandler._publishedContentValueSetBuilder.GetValueSets(content).ToList()), - [false] = new Lazy>(() => examineUmbracoIndexingHandler._contentValueSetBuilder.GetValueSets(content).ToList()) - }; - - // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) - { - if (cancellationToken.IsCancellationRequested) - { - return Task.CompletedTask; - } - - List valueSet = builders[index.PublishedValuesOnly].Value; - index.IndexItems(valueSet); - } - - return Task.CompletedTask; - }); - } - - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForMedia : DeferedAction - { - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IMedia _media; - private readonly bool _isPublished; - - public DeferedReIndexForMedia(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) - { - _backgroundTaskQueue = backgroundTaskQueue; - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _media = media; - _isPublished = isPublished; - } - - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _media, _isPublished); - - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) => - // perform the ValueSet lookup on a background thread - backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => - { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - - var valueSet = examineUmbracoIndexingHandler._mediaValueSetBuilder.GetValueSets(media).ToList(); - - // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => isPublished || !x.PublishedValuesOnly) - .Where(x => x.EnableDefaultEventHandler)) - { - index.IndexItems(valueSet); - } - - return Task.CompletedTask; - }); - } - - /// - /// Re-indexes an item on a background thread - /// - private class DeferedReIndexForMember : DeferedAction - { - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly IMember _member; - private readonly IBackgroundTaskQueue _backgroundTaskQueue; - - public DeferedReIndexForMember(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) - { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _member = member; - _backgroundTaskQueue = backgroundTaskQueue; - } - - public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _member); - - public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) => - // perform the ValueSet lookup on a background thread - backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => - { - using ICoreScope scope = examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); - - var valueSet = examineUmbracoIndexingHandler._memberValueSetBuilder.GetValueSets(member).ToList(); - - // only process for IUmbracoMemberIndex (not content indexes) - foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - //filter the indexers - .Where(x => x.EnableDefaultEventHandler)) - { - index.IndexItems(valueSet); - } - - return Task.CompletedTask; - }); - } - - private class DeferedDeleteIndex : DeferedAction - { - private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; - private readonly int _id; - private readonly IReadOnlyCollection? _ids; - private readonly bool _keepIfUnpublished; - - public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) - { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _id = id; - _keepIfUnpublished = keepIfUnpublished; - } - - public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) - { - _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; - _ids = ids; - _keepIfUnpublished = keepIfUnpublished; - } - - public override void Execute() - { - if (_ids is null) - { - Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); - } - else - { - Execute(_examineUmbracoIndexingHandler, _ids, _keepIfUnpublished); - } - } - - public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, bool keepIfUnpublished) - { - foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) - .Where(x => x.EnableDefaultEventHandler)) - { - index.DeleteFromIndex(id.ToString(CultureInfo.InvariantCulture)); - } - } - - public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IReadOnlyCollection ids, bool keepIfUnpublished) - { - foreach (var index in examineUmbracoIndexingHandler._examineManager.Indexes.OfType() - .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) - .Where(x => x.EnableDefaultEventHandler)) - { - index.DeleteFromIndex(ids.Select(x => x.ToString(CultureInfo.InvariantCulture))); - } - } - } - #endregion + _mainDom = mainDom; + _logger = logger; + _profilingLogger = profilingLogger; + _scopeProvider = scopeProvider; + _examineManager = examineManager; + _backgroundTaskQueue = backgroundTaskQueue; + _contentValueSetBuilder = contentValueSetBuilder; + _publishedContentValueSetBuilder = publishedContentValueSetBuilder; + _mediaValueSetBuilder = mediaValueSetBuilder; + _memberValueSetBuilder = memberValueSetBuilder; + _enabled = new Lazy(IsEnabled); } + + /// + public bool Enabled => _enabled.Value; + + /// + public void DeleteIndexForEntity(int entityId, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished)); + } + else + { + DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished); + } + } + + /// + public void DeleteIndexForEntities(IReadOnlyCollection entityIds, bool keepIfUnpublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedDeleteIndex(this, entityIds, keepIfUnpublished)); + } + else + { + DeferedDeleteIndex.Execute(this, entityIds, keepIfUnpublished); + } + } + + /// + public void ReIndexForContent(IContent sender, bool isPublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedReIndexForContent(_backgroundTaskQueue, this, sender, isPublished)); + } + else + { + DeferedReIndexForContent.Execute(_backgroundTaskQueue, this, sender, isPublished); + } + } + + /// + public void ReIndexForMedia(IMedia sender, bool isPublished) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedReIndexForMedia(_backgroundTaskQueue, this, sender, isPublished)); + } + else + { + DeferedReIndexForMedia.Execute(_backgroundTaskQueue, this, sender, isPublished); + } + } + + /// + public void ReIndexForMember(IMember member) + { + var actions = DeferedActions.Get(_scopeProvider); + if (actions != null) + { + actions.Add(new DeferedReIndexForMember(_backgroundTaskQueue, this, member)); + } + else + { + DeferedReIndexForMember.Execute(_backgroundTaskQueue, this, member); + } + } + + /// + public void DeleteDocumentsForContentTypes(IReadOnlyCollection removedContentTypes) + { + const int pageSize = 500; + + //Delete all content of this content/media/member type that is in any content indexer by looking up matched examine docs + foreach (var id in removedContentTypes) + { + foreach (IUmbracoIndex index in _examineManager.Indexes.OfType()) + { + var page = 0; + var total = long.MaxValue; + while (page * pageSize < total) + { + //paging with examine, see https://shazwazza.com/post/paging-with-examine/ + ISearchResults? results = index.Searcher + .CreateQuery() + .Field("nodeType", id.ToInvariantString()) + .Execute(QueryOptions.SkipTake(page * pageSize, pageSize)); + total = results.TotalItemCount; + + foreach (ISearchResult item in results) + { + if (int.TryParse(item.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var contentId)) + { + DeleteIndexForEntity(contentId, false); + } + } + + page++; + } + } + } + } + + /// + /// Used to lazily check if Examine Index handling is enabled + /// + /// + private bool IsEnabled() + { + //let's deal with shutting down Examine with MainDom + var examineShutdownRegistered = _mainDom.Register(release: () => + { + using (_profilingLogger.TraceDuration("Examine shutting down")) + { + _examineManager.Dispose(); + } + }); + + if (!examineShutdownRegistered) + { + _logger.LogInformation( + "Examine shutdown not registered, this AppDomain is not the MainDom, Examine will be disabled"); + + //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! + Suspendable.ExamineEvents.SuspendIndexers(_logger); + return false; //exit, do not continue + } + + _logger.LogDebug("Examine shutdown registered with MainDom"); + + var registeredIndexers = + _examineManager.Indexes.OfType().Count(x => x.EnableDefaultEventHandler); + + _logger.LogInformation("Adding examine event handlers for {RegisteredIndexers} index providers.", + registeredIndexers); + + // don't bind event handlers if we're not suppose to listen + if (registeredIndexers == 0) + { + return false; + } + + return true; + } + + #region Deferred Actions + + private class DeferedActions + { + private readonly List _actions = new(); + + public static DeferedActions? Get(ICoreScopeProvider scopeProvider) + { + IScopeContext? scopeContext = scopeProvider.Context; + + return scopeContext?.Enlist("examineEvents", + () => new DeferedActions(), // creator + (completed, actions) => // action + { + if (completed) + { + actions?.Execute(); + } + }, EnlistPriority); + } + + public void Add(DeferedAction action) => _actions.Add(action); + + private void Execute() + { + foreach (DeferedAction action in _actions) + { + action.Execute(); + } + } + } + + /// + /// An action that will execute at the end of the Scope being completed + /// + private abstract class DeferedAction + { + public virtual void Execute() + { + } + } + + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForContent : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IContent _content; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly bool _isPublished; + + public DeferedReIndexForContent(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) + { + _backgroundTaskQueue = backgroundTaskQueue; + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _content = content; + _isPublished = isPublished; + } + + public override void Execute() => + Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _content, _isPublished); + + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IContent content, bool isPublished) + => backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + + // for content we have a different builder for published vs unpublished + // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published + var builders = new Dictionary>> + { + [true] = new(() => examineUmbracoIndexingHandler._publishedContentValueSetBuilder.GetValueSets(content).ToList()), + [false] = new(() => examineUmbracoIndexingHandler._contentValueSetBuilder.GetValueSets(content).ToList()) + }; + + // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.CompletedTask; + } + + List valueSet = builders[index.PublishedValuesOnly].Value; + index.IndexItems(valueSet); + } + + return Task.CompletedTask; + }); + } + + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForMedia : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly bool _isPublished; + private readonly IMedia _media; + + public DeferedReIndexForMedia(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) + { + _backgroundTaskQueue = backgroundTaskQueue; + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _media = media; + _isPublished = isPublished; + } + + public override void Execute() => + Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _media, _isPublished); + + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMedia media, bool isPublished) => + // perform the ValueSet lookup on a background thread + backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + + var valueSet = examineUmbracoIndexingHandler._mediaValueSetBuilder.GetValueSets(media).ToList(); + + // This is only for content - so only index items for IUmbracoContentIndex (to exlude members) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => isPublished || !x.PublishedValuesOnly) + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } + + return Task.CompletedTask; + }); + } + + /// + /// Re-indexes an item on a background thread + /// + private class DeferedReIndexForMember : DeferedAction + { + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly IMember _member; + + public DeferedReIndexForMember(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _member = member; + _backgroundTaskQueue = backgroundTaskQueue; + } + + public override void Execute() => Execute(_backgroundTaskQueue, _examineUmbracoIndexingHandler, _member); + + public static void Execute(IBackgroundTaskQueue backgroundTaskQueue, + ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, IMember member) => + // perform the ValueSet lookup on a background thread + backgroundTaskQueue.QueueBackgroundWorkItem(cancellationToken => + { + using ICoreScope scope = + examineUmbracoIndexingHandler._scopeProvider.CreateCoreScope(autoComplete: true); + + var valueSet = examineUmbracoIndexingHandler._memberValueSetBuilder.GetValueSets(member).ToList(); + + // only process for IUmbracoMemberIndex (not content indexes) + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + //filter the indexers + .Where(x => x.EnableDefaultEventHandler)) + { + index.IndexItems(valueSet); + } + + return Task.CompletedTask; + }); + } + + private class DeferedDeleteIndex : DeferedAction + { + private readonly ExamineUmbracoIndexingHandler _examineUmbracoIndexingHandler; + private readonly int _id; + private readonly IReadOnlyCollection? _ids; + private readonly bool _keepIfUnpublished; + + public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, + bool keepIfUnpublished) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _id = id; + _keepIfUnpublished = keepIfUnpublished; + } + + public DeferedDeleteIndex(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, + IReadOnlyCollection ids, bool keepIfUnpublished) + { + _examineUmbracoIndexingHandler = examineUmbracoIndexingHandler; + _ids = ids; + _keepIfUnpublished = keepIfUnpublished; + } + + public override void Execute() + { + if (_ids is null) + { + Execute(_examineUmbracoIndexingHandler, _id, _keepIfUnpublished); + } + else + { + Execute(_examineUmbracoIndexingHandler, _ids, _keepIfUnpublished); + } + } + + public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, int id, + bool keepIfUnpublished) + { + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) + .Where(x => x.EnableDefaultEventHandler)) + { + index.DeleteFromIndex(id.ToString(CultureInfo.InvariantCulture)); + } + } + + public static void Execute(ExamineUmbracoIndexingHandler examineUmbracoIndexingHandler, + IReadOnlyCollection ids, bool keepIfUnpublished) + { + foreach (IUmbracoIndex index in examineUmbracoIndexingHandler._examineManager.Indexes + .OfType() + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) + .Where(x => x.EnableDefaultEventHandler)) + { + index.DeleteFromIndex(ids.Select(x => x.ToString(CultureInfo.InvariantCulture))); + } + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs index c2bf6b002d..5bbd115cef 100644 --- a/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Infrastructure/Examine/GenericIndexDiagnostics.cs @@ -1,70 +1,70 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Reflection; using Examine; using Examine.Search; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to return diagnostic data for any index +/// +public class GenericIndexDiagnostics : IIndexDiagnostics { + private static readonly string[] _ignoreProperties = { "Description" }; - /// - /// Used to return diagnostic data for any index - /// - public class GenericIndexDiagnostics : IIndexDiagnostics + private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; + private readonly IIndex _index; + + public GenericIndexDiagnostics(IIndex index) => _index = index; + + public int DocumentCount => -1; // unknown + + public int FieldCount => -1; // unknown + + public IReadOnlyDictionary Metadata { - private readonly IIndex _index; - private static readonly string[] s_ignoreProperties = { "Description" }; - - private readonly ISet _idOnlyFieldSet = new HashSet { "id" }; - public GenericIndexDiagnostics(IIndex index) => _index = index; - - public int DocumentCount => -1; //unknown - - public int FieldCount => -1; //unknown - - public Attempt IsHealthy() + get { - if (!_index.IndexExists()) - return Attempt.Fail("Does not exist"); + var result = new Dictionary(); - try + IOrderedEnumerable props = TypeHelper + .CachedDiscoverableProperties(_index.GetType(), mustWrite: false) + .Where(x => _ignoreProperties.InvariantContains(x.Name) == false) + .OrderBy(x => x.Name); + + foreach (PropertyInfo p in props) { - var result = _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet).Execute(new QueryOptions(0, 1)); - return Attempt.Succeed(); //if we can search we'll assume it's healthy + var val = p.GetValue(_index, null) ?? string.Empty; + + result.Add(p.Name, val); } - catch (Exception e) - { - return Attempt.Fail($"Error: {e.Message}"); - } - } - public long GetDocumentCount() => -1L; - - public IEnumerable GetFieldNames() => Enumerable.Empty(); - - public IReadOnlyDictionary Metadata - { - get - { - var result = new Dictionary(); - - var props = TypeHelper.CachedDiscoverableProperties(_index.GetType(), mustWrite: false) - .Where(x => s_ignoreProperties.InvariantContains(x.Name) == false) - .OrderBy(x => x.Name); - - foreach (var p in props) - { - var val = p.GetValue(_index, null) ?? string.Empty; - - result.Add(p.Name, val); - } - - return result; - } + return result; } } + + public Attempt IsHealthy() + { + if (!_index.IndexExists()) + { + return Attempt.Fail("Does not exist"); + } + + try + { + _index.Searcher.CreateQuery().ManagedQuery("test").SelectFields(_idOnlyFieldSet) + .Execute(new QueryOptions(0, 1)); + return Attempt.Succeed(); // if we can search we'll assume it's healthy + } + catch (Exception e) + { + return Attempt.Fail($"Error: {e.Message}"); + } + } + + public long GetDocumentCount() => -1L; + + public IEnumerable GetFieldNames() => Enumerable.Empty(); } diff --git a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs index dc01d7bc94..eb6ce0f01c 100644 --- a/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/IBackOfficeExamineSearcher.cs @@ -1,17 +1,19 @@ -using System.Collections.Generic; using Examine; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Used to search the back office for Examine indexed entities (Documents, Media and Members) +/// +public interface IBackOfficeExamineSearcher { - /// - /// Used to search the back office for Examine indexed entities (Documents, Media and Members) - /// - public interface IBackOfficeExamineSearcher - { - IEnumerable Search(string query, - UmbracoEntityTypes entityType, - int pageSize, - long pageIndex, out long totalFound, string? searchFrom = null, bool ignoreUserStartNodes = false); - } + IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string? searchFrom = null, + bool ignoreUserStartNodes = false); } diff --git a/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs index af6b613e24..67c64f603d 100644 --- a/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IContentValueSetBuilder.cs @@ -1,12 +1,11 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// +/// Marker interface for a builder for supporting unpublished content +/// +public interface IContentValueSetBuilder : IValueSetBuilder { - /// - /// - /// Marker interface for a builder for supporting unpublished content - /// - public interface IContentValueSetBuilder : IValueSetBuilder - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs index e76153f25e..553732cec8 100644 --- a/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/IContentValueSetValidator.cs @@ -1,31 +1,32 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An extended for content indexes +/// +public interface IContentValueSetValidator : IValueSetValidator { /// - /// An extended for content indexes + /// When set to true the index will only retain published values /// - public interface IContentValueSetValidator : IValueSetValidator - { - /// - /// When set to true the index will only retain published values - /// - /// - /// Any non-published values will not be put or kept in the index: - /// * Deleted, Trashed, non-published Content items - /// * non-published Variants - /// - bool PublishedValuesOnly { get; } + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } - /// - /// If true, protected content will be indexed otherwise it will not be put or kept in the index - /// - bool SupportProtectedContent { get; } + /// + /// If true, protected content will be indexed otherwise it will not be put or kept in the index + /// + bool SupportProtectedContent { get; } - int? ParentId { get; } + int? ParentId { get; } - bool ValidatePath(string path, string category); - bool ValidateRecycleBin(string path, string category); - bool ValidateProtectedContent(string path, string category); - } + bool ValidatePath(string path, string category); + + bool ValidateRecycleBin(string path, string category); + + bool ValidateProtectedContent(string path, string category); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs b/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs index dd9ee63239..5db9847c1e 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexDiagnostics.cs @@ -1,30 +1,26 @@ -using System.Collections.Generic; -using System.Threading.Tasks; using Examine; using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Exposes diagnostic information about an index +/// +public interface IIndexDiagnostics : IIndexStats { + /// + /// A key/value collection of diagnostic properties for the index + /// + /// + /// Used to display in the UI + /// + IReadOnlyDictionary Metadata { get; } /// - /// Exposes diagnostic information about an index + /// If the index can be open/read /// - public interface IIndexDiagnostics : IIndexStats - { - /// - /// If the index can be open/read - /// - /// - /// A successful attempt if it is healthy, else a failed attempt with a message if unhealthy - /// - Attempt IsHealthy(); - - /// - /// A key/value collection of diagnostic properties for the index - /// - /// - /// Used to display in the UI - /// - IReadOnlyDictionary Metadata { get; } - } + /// + /// A successful attempt if it is healthy, else a failed attempt with a message if unhealthy + /// + Attempt IsHealthy(); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs b/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs index b39ef5c3a8..2d484eb03b 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexDiagnosticsFactory.cs @@ -1,13 +1,11 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Creates for an index if it doesn't implement +/// +public interface IIndexDiagnosticsFactory { - - /// - /// Creates for an index if it doesn't implement - /// - public interface IIndexDiagnosticsFactory - { - IIndexDiagnostics Create(IIndex index); - } + IIndexDiagnostics Create(IIndex index); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs index 2089bd923a..ca4549f4a2 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexPopulator.cs @@ -1,20 +1,19 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public interface IIndexPopulator { - public interface IIndexPopulator - { - /// - /// If this index is registered with this populator - /// - /// - /// - bool IsRegistered(IIndex index); + /// + /// If this index is registered with this populator + /// + /// + /// + bool IsRegistered(IIndex index); - /// - /// Populate indexers - /// - /// - void Populate(params IIndex[] indexes); - } + /// + /// Populate indexers + /// + /// + void Populate(params IIndex[] indexes); } diff --git a/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs index 127a20d685..a85c551b8d 100644 --- a/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IIndexRebuilder.cs @@ -1,14 +1,10 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public interface IIndexRebuilder { - public interface IIndexRebuilder - { - bool CanRebuild(string indexName); - void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true); - void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true); - } + bool CanRebuild(string indexName); + + void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true); + + void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true); } diff --git a/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs index 8c5348ed46..3f6982d004 100644 --- a/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IPublishedContentValueSetBuilder.cs @@ -1,12 +1,11 @@ -using Examine; +using Examine; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Marker interface for a builder for only published content +/// +public interface IPublishedContentValueSetBuilder : IValueSetBuilder { - /// - /// Marker interface for a builder for only published content - /// - public interface IPublishedContentValueSetBuilder : IValueSetBuilder - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs index 0735cc255d..a47c328fc0 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoContentIndex.cs @@ -1,11 +1,8 @@ -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +/// +/// Marker interface for indexes of Umbraco content +/// +public interface IUmbracoContentIndex : IUmbracoIndex { - /// - /// Marker interface for indexes of Umbraco content - /// - public interface IUmbracoContentIndex : IUmbracoIndex - { - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs index f2221e5c91..a94201894a 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoIndex.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// A Marker interface for defining an Umbraco indexer +/// +public interface IUmbracoIndex : IIndex, IIndexStats { /// - /// A Marker interface for defining an Umbraco indexer + /// When set to true Umbraco will keep the index in sync with Umbraco data automatically /// - public interface IUmbracoIndex : IIndex, IIndexStats - { - /// - /// When set to true Umbraco will keep the index in sync with Umbraco data automatically - /// - bool EnableDefaultEventHandler { get; } + bool EnableDefaultEventHandler { get; } - /// - /// When set to true the index will only retain published values - /// - /// - /// Any non-published values will not be put or kept in the index: - /// * Deleted, Trashed, non-published Content items - /// * non-published Variants - /// - bool PublishedValuesOnly { get; } - } + /// + /// When set to true the index will only retain published values + /// + /// + /// Any non-published values will not be put or kept in the index: + /// * Deleted, Trashed, non-published Content items + /// * non-published Variants + /// + bool PublishedValuesOnly { get; } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs index 83a3730b97..0aedcc90f5 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoIndexConfig.cs @@ -1,12 +1,12 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine -{ - public interface IUmbracoIndexConfig - { - IContentValueSetValidator GetContentValueSetValidator(); - IContentValueSetValidator GetPublishedContentValueSetValidator(); - IValueSetValidator GetMemberValueSetValidator(); +namespace Umbraco.Cms.Infrastructure.Examine; - } +public interface IUmbracoIndexConfig +{ + IContentValueSetValidator GetContentValueSetValidator(); + + IContentValueSetValidator GetPublishedContentValueSetValidator(); + + IValueSetValidator GetMemberValueSetValidator(); } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs index 7dc07688a9..e914c90d37 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoMemberIndex.cs @@ -1,9 +1,5 @@ -using Examine; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public interface IUmbracoMemberIndex : IUmbracoIndex { - public interface IUmbracoMemberIndex : IUmbracoIndex - { - - } } diff --git a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs index fe135a82b7..ee4fb38760 100644 --- a/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Infrastructure/Examine/IUmbracoTreeSearcherFields.cs @@ -1,35 +1,35 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +/// +/// Used to propagate hardcoded internal Field lists +/// +public interface IUmbracoTreeSearcherFields { /// - /// Used to propagate hardcoded internal Field lists + /// The default index fields that are searched on in the back office search for umbraco content entities. /// - public interface IUmbracoTreeSearcherFields - { - /// - /// The default index fields that are searched on in the back office search for umbraco content entities. - /// - IEnumerable GetBackOfficeFields(); + IEnumerable GetBackOfficeFields(); - /// - /// The additional index fields that are searched on in the back office for member entities. - /// - IEnumerable GetBackOfficeMembersFields(); + /// + /// The additional index fields that are searched on in the back office for member entities. + /// + IEnumerable GetBackOfficeMembersFields(); - /// - /// The additional index fields that are searched on in the back office for media entities. - /// - IEnumerable GetBackOfficeMediaFields(); + /// + /// The additional index fields that are searched on in the back office for media entities. + /// + IEnumerable GetBackOfficeMediaFields(); - /// - /// The additional index fields that are searched on in the back office for document entities. - /// - IEnumerable GetBackOfficeDocumentFields(); + /// + /// The additional index fields that are searched on in the back office for document entities. + /// + IEnumerable GetBackOfficeDocumentFields(); - ISet GetBackOfficeFieldsToLoad(); - ISet GetBackOfficeMembersFieldsToLoad(); - ISet GetBackOfficeDocumentFieldsToLoad(); - ISet GetBackOfficeMediaFieldsToLoad(); - } + ISet GetBackOfficeFieldsToLoad(); + + ISet GetBackOfficeMembersFieldsToLoad(); + + ISet GetBackOfficeDocumentFieldsToLoad(); + + ISet GetBackOfficeMediaFieldsToLoad(); } diff --git a/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs index 0e1d05440d..d9c5fe9566 100644 --- a/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/IValueSetBuilder.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Creates a collection of to be indexed based on a collection of +/// +/// +public interface IValueSetBuilder { /// - /// Creates a collection of to be indexed based on a collection of + /// Creates a collection of to be indexed based on a collection of /// - /// - public interface IValueSetBuilder - { - /// - /// Creates a collection of to be indexed based on a collection of - /// - /// - /// - IEnumerable GetValueSets(params T[] content); - } - + /// + /// + IEnumerable GetValueSets(params T[] content); } diff --git a/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs b/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs index a60a373e65..acaf42b4b0 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexDiagnosticsFactory.cs @@ -1,20 +1,20 @@ using Examine; -namespace Umbraco.Cms.Infrastructure.Examine -{ - /// - /// Default implementation of which returns for indexes that don't have an implementation - /// - public class IndexDiagnosticsFactory : IIndexDiagnosticsFactory - { - public virtual IIndexDiagnostics Create(IIndex index) - { - if (index is not IIndexDiagnostics indexDiag) - { - indexDiag = new GenericIndexDiagnostics(index); - } +namespace Umbraco.Cms.Infrastructure.Examine; - return indexDiag; +/// +/// Default implementation of which returns +/// for indexes that don't have an implementation +/// +public class IndexDiagnosticsFactory : IIndexDiagnosticsFactory +{ + public virtual IIndexDiagnostics Create(IIndex index) + { + if (index is not IIndexDiagnostics indexDiag) + { + indexDiag = new GenericIndexDiagnostics(index); } + + return indexDiag; } } diff --git a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs index d32470d875..db3fe2373f 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexPopulator.cs @@ -1,53 +1,46 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Collections; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// An that is automatically associated to any index of type +/// +/// +public abstract class IndexPopulator : IndexPopulator + where TIndex : IIndex { - /// - /// An that is automatically associated to any index of type - /// - /// - public abstract class IndexPopulator : IndexPopulator where TIndex : IIndex + public override bool IsRegistered(IIndex index) { - public override bool IsRegistered(IIndex index) + if (base.IsRegistered(index)) { - if (base.IsRegistered(index)) - return true; - - if (!(index is TIndex casted)) - return false; - - return IsRegistered(casted); + return true; } - public virtual bool IsRegistered(TIndex index) => true; + if (!(index is TIndex casted)) + { + return false; + } + + return IsRegistered(casted); } - public abstract class IndexPopulator : IIndexPopulator - { - private readonly ConcurrentHashSet _registeredIndexes = new ConcurrentHashSet(); - - public virtual bool IsRegistered(IIndex index) - { - return _registeredIndexes.Contains(index.Name); - } - - /// - /// Registers an index for this populator - /// - /// - public void RegisterIndex(string indexName) - { - _registeredIndexes.Add(indexName); - } - - public void Populate(params IIndex[] indexes) - { - PopulateIndexes(indexes.Where(IsRegistered).ToList()); - } - - protected abstract void PopulateIndexes(IReadOnlyList indexes); - } + public virtual bool IsRegistered(TIndex index) => true; +} + +public abstract class IndexPopulator : IIndexPopulator +{ + private readonly ConcurrentHashSet _registeredIndexes = new(); + + public virtual bool IsRegistered(IIndex index) => _registeredIndexes.Contains(index.Name); + + public void Populate(params IIndex[] indexes) => PopulateIndexes(indexes.Where(IsRegistered).ToList()); + + /// + /// Registers an index for this populator + /// + /// + public void RegisterIndex(string indexName) => _registeredIndexes.Add(indexName); + + protected abstract void PopulateIndexes(IReadOnlyList indexes); } diff --git a/src/Umbraco.Infrastructure/Examine/IndexTypes.cs b/src/Umbraco.Infrastructure/Examine/IndexTypes.cs index bb6edaa78b..9b180aa5ea 100644 --- a/src/Umbraco.Infrastructure/Examine/IndexTypes.cs +++ b/src/Umbraco.Infrastructure/Examine/IndexTypes.cs @@ -1,33 +1,31 @@ -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// The index types stored in the Lucene Index +/// +public static class IndexTypes { /// - /// The index types stored in the Lucene Index + /// The content index type /// - public static class IndexTypes - { + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Content = "content"; - /// - /// The content index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Content = "content"; + /// + /// The media index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Media = "media"; - /// - /// The media index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Media = "media"; - - /// - /// The member index type - /// - /// - /// Is lower case because the Standard Analyzer requires lower case - /// - public const string Member = "member"; - } + /// + /// The member index type + /// + /// + /// Is lower case because the Standard Analyzer requires lower case + /// + public const string Member = "member"; } diff --git a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs index 9f6e33f8dd..19a2a96160 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaIndexPopulator.cs @@ -1,80 +1,75 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a media index +/// +public class MediaIndexPopulator : IndexPopulator { + private readonly ILogger _logger; + private readonly IMediaService _mediaService; + private readonly IValueSetBuilder _mediaValueSetBuilder; + private readonly int? _parentId; + /// - /// Performs the data lookups required to rebuild a media index + /// Default constructor to lookup all content data /// - public class MediaIndexPopulator : IndexPopulator + public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + : this(logger, null, mediaService, mediaValueSetBuilder) { - private readonly ILogger _logger; - private readonly int? _parentId; - private readonly IMediaService _mediaService; - private readonly IValueSetBuilder _mediaValueSetBuilder; + } - /// - /// Default constructor to lookup all content data - /// - /// - /// - public MediaIndexPopulator(ILogger logger, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) - : this(logger, null, mediaService, mediaValueSetBuilder) + /// + /// Optional constructor allowing specifying custom query parameters + /// + public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + { + _logger = logger; + _parentId = parentId; + _mediaService = mediaService; + _mediaValueSetBuilder = mediaValueSetBuilder; + } + + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { + _logger.LogDebug( + $"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); + return; } - /// - /// Optional constructor allowing specifying custom query parameters - /// - /// - /// - /// - public MediaIndexPopulator(ILogger logger, int? parentId, IMediaService mediaService, IValueSetBuilder mediaValueSetBuilder) + const int pageSize = 10000; + var pageIndex = 0; + + var mediaParentId = -1; + + if (_parentId.HasValue && _parentId.Value > 0) { - _logger = logger; - _parentId = parentId; - _mediaService = mediaService; - _mediaValueSetBuilder = mediaValueSetBuilder; + mediaParentId = _parentId.Value; } - protected override void PopulateIndexes(IReadOnlyList indexes) + IMedia[] media; + + do { - if (indexes.Count == 0) + media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out _).ToArray(); + + if (media.Length > 0) { - _logger.LogDebug($"{nameof(PopulateIndexes)} called with no indexes to populate. Typically means no index is registered with this populator."); - return; - } - - const int pageSize = 10000; - var pageIndex = 0; - - var mediaParentId = -1; - - if (_parentId.HasValue && _parentId.Value > 0) - { - mediaParentId = _parentId.Value; - } - - IMedia[] media; - - do - { - media = _mediaService.GetPagedDescendants(mediaParentId, pageIndex, pageSize, out var total).ToArray(); - - if (media.Length > 0) + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) { - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - index.IndexItems(_mediaValueSetBuilder.GetValueSets(media)); + index.IndexItems(_mediaValueSetBuilder.GetValueSets(media)); } + } - pageIndex++; - } while (media.Length == pageSize); + pageIndex++; } - + while (media.Length == pageSize); } } diff --git a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs index ff0c1f142c..344c7d08d2 100644 --- a/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MediaValueSetBuilder.cs @@ -1,87 +1,76 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Examine; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MediaValueSetBuilder : BaseValueSetBuilder { - public class MediaValueSetBuilder : BaseValueSetBuilder + private readonly ContentSettings _contentSettings; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + private readonly IUserService _userService; + + public MediaValueSetBuilder( + PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + MediaUrlGeneratorCollection mediaUrlGenerators, + IUserService userService, + IShortStringHelper shortStringHelper, + IOptions contentSettings) + : base(propertyEditors, false) { - private readonly UrlSegmentProviderCollection _urlSegmentProviders; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IUserService _userService; - private readonly IShortStringHelper _shortStringHelper; - private readonly ContentSettings _contentSettings; - - public MediaValueSetBuilder( - PropertyEditorCollection propertyEditors, - UrlSegmentProviderCollection urlSegmentProviders, - MediaUrlGeneratorCollection mediaUrlGenerators, - IUserService userService, - IShortStringHelper shortStringHelper, - IOptions contentSettings) - : base(propertyEditors, false) - { - _urlSegmentProviders = urlSegmentProviders; - _mediaUrlGenerators = mediaUrlGenerators; - _userService = userService; - _shortStringHelper = shortStringHelper; - _contentSettings = contentSettings.Value; - } - - /// - public override IEnumerable GetValueSets(params IMedia[] media) - { - foreach (IMedia m in media) - { - - var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); - - IEnumerable mediaFiles = m.GetUrls(_contentSettings, _mediaUrlGenerators) - .Select(x => Path.GetFileName(x)) - .Distinct(); - - var values = new Dictionary> - { - {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {"id", new object[] {m.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, - {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, - {"path", m.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", m.ContentType.Id.ToString().Yield() }, - {"creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield()}, - {UmbracoExamineFieldNames.UmbracoFileFieldName, mediaFiles} - }; - - foreach (var property in m.Properties) - { - AddPropertyValue(property, null, null, values); - } - - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); - - yield return vs; - } - } + _urlSegmentProviders = urlSegmentProviders; + _mediaUrlGenerators = mediaUrlGenerators; + _userService = userService; + _shortStringHelper = shortStringHelper; + _contentSettings = contentSettings.Value; } + /// + public override IEnumerable GetValueSets(params IMedia[] media) + { + foreach (IMedia m in media) + { + var urlValue = m.GetUrlSegment(_shortStringHelper, _urlSegmentProviders); + + IEnumerable mediaFiles = m.GetUrls(_contentSettings, _mediaUrlGenerators) + .Select(x => Path.GetFileName(x)) + .Distinct(); + + var values = new Dictionary> + { + { "icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty() }, + { "id", new object[] { m.Id } }, + { UmbracoExamineFieldNames.NodeKeyFieldName, new object[] { m.Key } }, + { "parentID", new object[] { m.Level > 1 ? m.ParentId : -1 } }, + { "level", new object[] { m.Level } }, + { "creatorID", new object[] { m.CreatorId } }, + { "sortOrder", new object[] { m.SortOrder } }, + { "createDate", new object[] { m.CreateDate } }, + { "updateDate", new object[] { m.UpdateDate } }, + { UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty() }, + { "urlName", urlValue?.Yield() ?? Enumerable.Empty() }, + { "path", m.Path.Yield() }, + { "nodeType", m.ContentType.Id.ToString().Yield() }, + { "creatorName", (m.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, + { UmbracoExamineFieldNames.UmbracoFileFieldName, mediaFiles }, + }; + + foreach (IProperty property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } + + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Media, m.ContentType.Alias, values); + + yield return vs; + } + } } diff --git a/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs index 76dee23450..9dc2102d5e 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberIndexPopulator.cs @@ -1,44 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MemberIndexPopulator : IndexPopulator { - public class MemberIndexPopulator : IndexPopulator + private readonly IMemberService _memberService; + private readonly IValueSetBuilder _valueSetBuilder; + + public MemberIndexPopulator(IMemberService memberService, IValueSetBuilder valueSetBuilder) { - private readonly IMemberService _memberService; - private readonly IValueSetBuilder _valueSetBuilder; + _memberService = memberService; + _valueSetBuilder = valueSetBuilder; + } - public MemberIndexPopulator(IMemberService memberService, IValueSetBuilder valueSetBuilder) + protected override void PopulateIndexes(IReadOnlyList indexes) + { + if (indexes.Count == 0) { - _memberService = memberService; - _valueSetBuilder = valueSetBuilder; + return; } - protected override void PopulateIndexes(IReadOnlyList indexes) + + const int pageSize = 1000; + var pageIndex = 0; + + IMember[] members; + + // no node types specified, do all members + do { - if (indexes.Count == 0) return; + members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); - const int pageSize = 1000; - var pageIndex = 0; - - IMember[] members; - - //no node types specified, do all members - do + if (members.Length > 0) { - members = _memberService.GetAll(pageIndex, pageSize, out _).ToArray(); - - if (members.Length > 0) + // ReSharper disable once PossibleMultipleEnumeration + foreach (IIndex index in indexes) { - // ReSharper disable once PossibleMultipleEnumeration - foreach (var index in indexes) - index.IndexItems(_valueSetBuilder.GetValueSets(members)); + index.IndexItems(_valueSetBuilder.GetValueSets(members)); } + } - pageIndex++; - } while (members.Length == pageSize); + pageIndex++; } + while (members.Length == pageSize); } } diff --git a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs index 33481c96a3..74d3829d6a 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberValueSetBuilder.cs @@ -1,53 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class MemberValueSetBuilder : BaseValueSetBuilder { - - public class MemberValueSetBuilder : BaseValueSetBuilder + public MemberValueSetBuilder(PropertyEditorCollection propertyEditors) + : base(propertyEditors, false) { - public MemberValueSetBuilder(PropertyEditorCollection propertyEditors) - : base(propertyEditors, false) - { - } - - /// - public override IEnumerable GetValueSets(params IMember[] members) - { - foreach (var m in members) - { - var values = new Dictionary> - { - {"icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty()}, - {"id", new object[] {m.Id}}, - {UmbracoExamineFieldNames.NodeKeyFieldName, new object[] {m.Key}}, - {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, - {"level", new object[] {m.Level}}, - {"creatorID", new object[] {m.CreatorId}}, - {"sortOrder", new object[] {m.SortOrder}}, - {"createDate", new object[] {m.CreateDate}}, - {"updateDate", new object[] {m.UpdateDate}}, - {UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty()}, - {"path", m.Path?.Yield() ?? Enumerable.Empty()}, - {"nodeType", m.ContentType.Id.ToString().Yield() }, - {"loginName", m.Username?.Yield() ?? Enumerable.Empty()}, - {"email", m.Email?.Yield() ?? Enumerable.Empty()}, - }; - - foreach (var property in m.Properties) - { - AddPropertyValue(property, null, null, values); - } - - var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); - - yield return vs; - } - } } + /// + public override IEnumerable GetValueSets(params IMember[] members) + { + foreach (IMember m in members) + { + var values = new Dictionary> + { + { "icon", m.ContentType.Icon?.Yield() ?? Enumerable.Empty() }, + { "id", new object[] { m.Id } }, + { UmbracoExamineFieldNames.NodeKeyFieldName, new object[] { m.Key } }, + { "parentID", new object[] { m.Level > 1 ? m.ParentId : -1 } }, + { "level", new object[] { m.Level } }, + { "creatorID", new object[] { m.CreatorId } }, + { "sortOrder", new object[] { m.SortOrder } }, + { "createDate", new object[] { m.CreateDate } }, + { "updateDate", new object[] { m.UpdateDate } }, + { UmbracoExamineFieldNames.NodeNameFieldName, m.Name?.Yield() ?? Enumerable.Empty() }, + { "path", m.Path.Yield() }, + { "nodeType", m.ContentType.Id.ToString().Yield() }, + { "loginName", m.Username.Yield() }, + { "email", m.Email.Yield() }, + }; + + foreach (IProperty property in m.Properties) + { + AddPropertyValue(property, null, null, values); + } + + var vs = new ValueSet(m.Id.ToInvariantString(), IndexTypes.Member, m.ContentType.Alias, values); + + yield return vs; + } + } } diff --git a/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs index f92a9dc620..f1fa96e0fa 100644 --- a/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/MemberValueSetValidator.cs @@ -1,30 +1,32 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Cms.Infrastructure.Examine +public class MemberValueSetValidator : ValueSetValidator { - public class MemberValueSetValidator : ValueSetValidator + /// + /// By default these are the member fields we index + /// + public static readonly string[] DefaultMemberIndexFields = { - public MemberValueSetValidator() : base(null, null, DefaultMemberIndexFields, null) - { - } + "id", UmbracoExamineFieldNames.NodeNameFieldName, "updateDate", "loginName", "email", + UmbracoExamineFieldNames.NodeKeyFieldName, + }; - public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes) - : base(includeItemTypes, excludeItemTypes, DefaultMemberIndexFields, null) - { - } - - public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes, IEnumerable includeFields, IEnumerable excludeFields) - : base(includeItemTypes, excludeItemTypes, includeFields, excludeFields) - { - } - - /// - /// By default these are the member fields we index - /// - public static readonly string[] DefaultMemberIndexFields = { "id", UmbracoExamineFieldNames.NodeNameFieldName, "updateDate", "loginName", "email", UmbracoExamineFieldNames.NodeKeyFieldName }; - - private static readonly IEnumerable ValidCategories = new[] { IndexTypes.Member }; - protected override IEnumerable ValidIndexCategories => ValidCategories; + private static readonly IEnumerable _validCategories = new[] { IndexTypes.Member }; + public MemberValueSetValidator() + : base(null, null, DefaultMemberIndexFields, null) + { } + + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes) + : base(includeItemTypes, excludeItemTypes, DefaultMemberIndexFields, null) + { + } + + public MemberValueSetValidator(IEnumerable includeItemTypes, IEnumerable excludeItemTypes, IEnumerable includeFields, IEnumerable excludeFields) + : base(includeItemTypes, excludeItemTypes, includeFields, excludeFields) + { + } + + protected override IEnumerable ValidIndexCategories => _validCategories; } diff --git a/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs b/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs index cb01bae57a..625a0fda53 100644 --- a/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs +++ b/src/Umbraco.Infrastructure/Examine/NoopBackOfficeExamineSearcher.cs @@ -1,17 +1,20 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Cms.Core.Models.ContentEditing; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class NoopBackOfficeExamineSearcher : IBackOfficeExamineSearcher { - public class NoopBackOfficeExamineSearcher : IBackOfficeExamineSearcher + public IEnumerable Search( + string query, + UmbracoEntityTypes entityType, + int pageSize, + long pageIndex, + out long totalFound, + string? searchFrom = null, + bool ignoreUserStartNodes = false) { - public IEnumerable Search(string query, UmbracoEntityTypes entityType, int pageSize, long pageIndex, out long totalFound, - string? searchFrom = null, bool ignoreUserStartNodes = false) - { - totalFound = 0; - return Enumerable.Empty(); - } + totalFound = 0; + return Enumerable.Empty(); } } diff --git a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs index f9ccaffdbc..67d59d02d9 100644 --- a/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs +++ b/src/Umbraco.Infrastructure/Examine/PublishedContentIndexPopulator.cs @@ -2,21 +2,25 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performs the data lookups required to rebuild a content index containing only published content +/// +/// +/// The published (external) index will still rebuild just fine using the default +/// which is what is used when rebuilding all indexes, +/// but this will be used when the single index is rebuilt and will go a little bit faster since the data query is more specific. +/// since the data query is more specific. +/// +public class PublishedContentIndexPopulator : ContentIndexPopulator { - /// - /// Performs the data lookups required to rebuild a content index containing only published content - /// - /// - /// The published (external) index will still rebuild just fine using the default which is what - /// is used when rebuilding all indexes, but this will be used when the single index is rebuilt and will go a little bit faster - /// since the data query is more specific. - /// - public class PublishedContentIndexPopulator : ContentIndexPopulator + public PublishedContentIndexPopulator( + ILogger logger, + IContentService contentService, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IPublishedContentValueSetBuilder contentValueSetBuilder) + : base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) { - public PublishedContentIndexPopulator(ILogger logger, IContentService contentService, IUmbracoDatabaseFactory umbracoDatabaseFactory, IPublishedContentValueSetBuilder contentValueSetBuilder) : - base(logger, true, null, contentService, umbracoDatabaseFactory, contentValueSetBuilder) - { - } } } diff --git a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs index 9ec145b030..ca7bc49dee 100644 --- a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs @@ -1,72 +1,68 @@ -using System; -using System.Threading; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Handles how the indexes are rebuilt on startup +/// +/// +/// On the first HTTP request this will rebuild the Examine indexes if they are empty. +/// If it is a cold boot, they are all rebuilt. +/// +public sealed class RebuildOnStartupHandler : INotificationHandler { - /// - /// Handles how the indexes are rebuilt on startup - /// - /// - /// On the first HTTP request this will rebuild the Examine indexes if they are empty. - /// If it is a cold boot, they are all rebuilt. - /// - public sealed class RebuildOnStartupHandler : INotificationHandler + // These must be static because notification handlers are transient. + // this does unfortunatley mean that one RebuildOnStartupHandler instance + // will be created for each front-end request even though we only use the first one. + // TODO: Is there a better way to acheive this without allocating? We cannot remove + // a handler from the notification system. It's not a huge deal but would be better + // with less objects. + private static bool _isReady; + private static bool _isReadSet; + private static object? _isReadyLock; + private readonly ExamineIndexRebuilder _backgroundIndexRebuilder; + private readonly IRuntimeState _runtimeState; + private readonly ISyncBootStateAccessor _syncBootStateAccessor; + + public RebuildOnStartupHandler( + ISyncBootStateAccessor syncBootStateAccessor, + ExamineIndexRebuilder backgroundIndexRebuilder, + IRuntimeState runtimeState) { - private readonly ISyncBootStateAccessor _syncBootStateAccessor; - private readonly ExamineIndexRebuilder _backgroundIndexRebuilder; - private readonly IRuntimeState _runtimeState; + _syncBootStateAccessor = syncBootStateAccessor; + _backgroundIndexRebuilder = backgroundIndexRebuilder; + _runtimeState = runtimeState; + } - // These must be static because notification handlers are transient. - // this does unfortunatley mean that one RebuildOnStartupHandler instance - // will be created for each front-end request even though we only use the first one. - // TODO: Is there a better way to acheive this without allocating? We cannot remove - // a handler from the notification system. It's not a huge deal but would be better - // with less objects. - private static bool _isReady; - private static bool _isReadSet; - private static object? _isReadyLock; - - public RebuildOnStartupHandler( - ISyncBootStateAccessor syncBootStateAccessor, - ExamineIndexRebuilder backgroundIndexRebuilder, - IRuntimeState runtimeState) + /// + /// On first http request schedule an index rebuild for any empty indexes (or all if it's a cold boot) + /// + /// + public void Handle(UmbracoRequestBeginNotification notification) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - _syncBootStateAccessor = syncBootStateAccessor; - _backgroundIndexRebuilder = backgroundIndexRebuilder; - _runtimeState = runtimeState; + return; } - /// - /// On first http request schedule an index rebuild for any empty indexes (or all if it's a cold boot) - /// - /// - public void Handle(UmbracoRequestBeginNotification notification) - { - if (_runtimeState.Level != RuntimeLevel.Run) + LazyInitializer.EnsureInitialized( + ref _isReady, + ref _isReadSet, + ref _isReadyLock, + () => { - return; - } + SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); - LazyInitializer.EnsureInitialized( - ref _isReady, - ref _isReadSet, - ref _isReadyLock, - () => - { - SyncBootState bootState = _syncBootStateAccessor.GetSyncBootState(); + // if it's not a cold boot, only rebuild empty ones + _backgroundIndexRebuilder.RebuildIndexes( + bootState != SyncBootState.ColdBoot, + TimeSpan.FromMinutes(1)); - _backgroundIndexRebuilder.RebuildIndexes( - // if it's not a cold boot, only rebuild empty ones - bootState != SyncBootState.ColdBoot, - TimeSpan.FromMinutes(1)); - - return true; - }); - } + return true; + }); } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs index 7ae567739e..7ba10019c7 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineExtensions.cs @@ -1,136 +1,138 @@ -using System.Collections.Generic; using System.Text.RegularExpressions; -using System.Threading.Tasks; using Examine; using Examine.Search; using Umbraco.Cms.Infrastructure.Examine; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class UmbracoExamineExtensions { - public static class UmbracoExamineExtensions + /// + /// Matches a culture iso name suffix + /// + /// + /// myFieldName_en-us will match the "en-us" + /// + internal static readonly Regex _cultureIsoCodeFieldNameMatchExpression = new( + "^(?[_\\w]+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", + RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + // TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression + + /// + /// Returns all index fields that are culture specific (suffixed) + /// + /// + /// + /// + public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) { - /// - /// Matches a culture iso name suffix - /// - /// - /// myFieldName_en-us will match the "en-us" - /// - internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^(?[_\\w]+)_(?[a-z]{2,3}(-[a-z0-9]{2,4})?)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + IEnumerable allFields = index.GetFieldNames(); - //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression - - /// - /// Returns all index fields that are culture specific (suffixed) - /// - /// - /// - /// - public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) + var results = new List(); + foreach (var field in allFields) { - IEnumerable allFields = index.GetFieldNames(); - - var results = new List(); - foreach (var field in allFields) + Match match = _cultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) { - var match = CultureIsoCodeFieldNameMatchExpression.Match(field); - if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) - { - results.Add(field); - } - } - - return results; - } - - /// - /// Returns all index fields that are culture specific (suffixed) or invariant - /// - /// - /// - /// - public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) - { - IEnumerable allFields = index.GetFieldNames(); - - foreach (var field in allFields) - { - var match = CultureIsoCodeFieldNameMatchExpression.Match(field); - if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) - { - yield return field; //matches this culture field - } - else if (!match.Success) - { - yield return field; //matches no culture field (invariant) - } + results.Add(field); } } - public static IBooleanOperation Id(this IQuery query, int id) - { - var fieldQuery = query.Id(id.ToInvariantString()); - return fieldQuery; - } + return results; + } - /// - /// Query method to search on parent id - /// - /// - /// - /// - public static IBooleanOperation ParentId(this IQuery query, int id) - { - var fieldQuery = query.Field("parentID", id); - return fieldQuery; - } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + IEnumerable allFields = index.GetFieldNames(); - /// - /// Query method to search on node name - /// - /// - /// - /// - public static IBooleanOperation NodeName(this IQuery query, string nodeName) + foreach (var field in allFields) { - var fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, (IExamineValue)new ExamineValue(Examineness.Explicit, nodeName)); - return fieldQuery; + Match match = _cultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && culture.InvariantEquals(match.Groups["CultureName"].Value)) + { + yield return field; // matches this culture field + } + else if (!match.Success) + { + yield return field; // matches no culture field (invariant) + } } + } - /// - /// Query method to search on node name - /// - /// - /// - /// - public static IBooleanOperation NodeName(this IQuery query, IExamineValue nodeName) - { - var fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, nodeName); - return fieldQuery; - } + public static IBooleanOperation Id(this IQuery query, int id) + { + IBooleanOperation? fieldQuery = query.Id(id.ToInvariantString()); + return fieldQuery; + } - /// - /// Query method to search on node type alias - /// - /// - /// - /// - public static IBooleanOperation NodeTypeAlias(this IQuery query, string nodeTypeAlias) - { - var fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, (IExamineValue)new ExamineValue(Examineness.Explicit, nodeTypeAlias)); - return fieldQuery; - } + /// + /// Query method to search on parent id + /// + /// + /// + /// + public static IBooleanOperation ParentId(this IQuery query, int id) + { + IBooleanOperation? fieldQuery = query.Field("parentID", id); + return fieldQuery; + } - /// - /// Query method to search on node type alias - /// - /// - /// - /// - public static IBooleanOperation NodeTypeAlias(this IQuery query, IExamineValue nodeTypeAlias) - { - var fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, nodeTypeAlias); - return fieldQuery; - } + /// + /// Query method to search on node name + /// + /// + /// + /// + public static IBooleanOperation NodeName(this IQuery query, string nodeName) + { + IBooleanOperation? fieldQuery = query.Field( + UmbracoExamineFieldNames.NodeNameFieldName, + (IExamineValue)new ExamineValue(Examineness.Explicit, nodeName)); + return fieldQuery; + } + /// + /// Query method to search on node name + /// + /// + /// + /// + public static IBooleanOperation NodeName(this IQuery query, IExamineValue nodeName) + { + IBooleanOperation? fieldQuery = query.Field(UmbracoExamineFieldNames.NodeNameFieldName, nodeName); + return fieldQuery; + } + + /// + /// Query method to search on node type alias + /// + /// + /// + /// + public static IBooleanOperation NodeTypeAlias(this IQuery query, string nodeTypeAlias) + { + IBooleanOperation? fieldQuery = query.Field( + ExamineFieldNames.ItemTypeFieldName, + (IExamineValue)new ExamineValue(Examineness.Explicit, nodeTypeAlias)); + return fieldQuery; + } + + /// + /// Query method to search on node type alias + /// + /// + /// + /// + public static IBooleanOperation NodeTypeAlias(this IQuery query, IExamineValue nodeTypeAlias) + { + IBooleanOperation? fieldQuery = query.Field(ExamineFieldNames.ItemTypeFieldName, nodeTypeAlias); + return fieldQuery; } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs index 72e914c584..5e2779e9a3 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoExamineFieldNames.cs @@ -1,28 +1,28 @@ -using Examine; +using Examine; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public static class UmbracoExamineFieldNames { - public static class UmbracoExamineFieldNames - { - /// - /// Used to store the path of a content object - /// - public const string IndexPathFieldName = ExamineFieldNames.SpecialFieldPrefix + "Path"; - public const string NodeKeyFieldName = ExamineFieldNames.SpecialFieldPrefix + "Key"; - public const string UmbracoFileFieldName = "umbracoFileSrc"; - public const string IconFieldName = ExamineFieldNames.SpecialFieldPrefix + "Icon"; - public const string PublishedFieldName = ExamineFieldNames.SpecialFieldPrefix + "Published"; + /// + /// Used to store the path of a content object + /// + public const string IndexPathFieldName = ExamineFieldNames.SpecialFieldPrefix + "Path"; - /// - /// The prefix added to a field when it is duplicated in order to store the original raw value. - /// - public const string RawFieldPrefix = ExamineFieldNames.SpecialFieldPrefix + "Raw_"; + public const string NodeKeyFieldName = ExamineFieldNames.SpecialFieldPrefix + "Key"; + public const string UmbracoFileFieldName = "umbracoFileSrc"; + public const string IconFieldName = ExamineFieldNames.SpecialFieldPrefix + "Icon"; + public const string PublishedFieldName = ExamineFieldNames.SpecialFieldPrefix + "Published"; - public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture"; + /// + /// The prefix added to a field when it is duplicated in order to store the original raw value. + /// + public const string RawFieldPrefix = ExamineFieldNames.SpecialFieldPrefix + "Raw_"; - public const string NodeNameFieldName = "nodeName"; - public const string ItemIdFieldName ="__NodeId"; - public const string CategoryFieldName = "__IndexType"; - public const string ItemTypeFieldName = "__NodeTypeAlias"; - } + public const string VariesByCultureFieldName = ExamineFieldNames.SpecialFieldPrefix + "VariesByCulture"; + + public const string NodeNameFieldName = "nodeName"; + public const string ItemIdFieldName = "__NodeId"; + public const string CategoryFieldName = "__IndexType"; + public const string ItemTypeFieldName = "__NodeTypeAlias"; } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs index 912ae75460..9f9e214316 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoFieldDefinitionCollection.cs @@ -1,94 +1,89 @@ -using Examine; +using System.Text.RegularExpressions; +using Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Custom allowing dynamic creation of +/// +public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection { /// - /// Custom allowing dynamic creation of + /// A type that defines the type of index for each Umbraco field (non user defined fields) + /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene + /// for retreival after searching. /// - public class UmbracoFieldDefinitionCollection : FieldDefinitionCollection + public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = { + new("parentID", FieldDefinitionTypes.Integer), new("level", FieldDefinitionTypes.Integer), + new("writerID", FieldDefinitionTypes.Integer), new("creatorID", FieldDefinitionTypes.Integer), + new("sortOrder", FieldDefinitionTypes.Integer), new("template", FieldDefinitionTypes.Integer), + new("createDate", FieldDefinitionTypes.DateTime), new("updateDate", FieldDefinitionTypes.DateTime), + new(UmbracoExamineFieldNames.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("version", FieldDefinitionTypes.Raw), new("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("template", FieldDefinitionTypes.Raw), new("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new("path", FieldDefinitionTypes.Raw), new("email", FieldDefinitionTypes.EmailAddress), + new(UmbracoExamineFieldNames.PublishedFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.IconFieldName, FieldDefinitionTypes.Raw), + new(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw), + }; - public UmbracoFieldDefinitionCollection() - : base(UmbracoIndexFieldDefinitions) + public UmbracoFieldDefinitionCollection() + : base(UmbracoIndexFieldDefinitions) + { + } + + /// + /// Overridden to dynamically add field definitions for culture variations + /// + /// + /// + /// + /// + /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions + /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... + /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like + /// `nodeName_en-us` + /// and we don't want to have a full static list of all of these definitions when we can just define the one definition + /// and then + /// dynamically apply that to culture specific fields. + /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and + /// store a new field + /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more + /// objects. This does mean + /// however that if a language is deleted, the field definitions for that language will still exist in memory. This + /// isn't going to cause any + /// problems and the mem will be cleared on next site restart but it's worth pointing out. + /// + public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) + { + if (base.TryGetValue(fieldName, out fieldDefinition)) { + return true; } - /// - /// A type that defines the type of index for each Umbraco field (non user defined fields) - /// Alot of standard umbraco fields shouldn't be tokenized or even indexed, just stored into lucene - /// for retreival after searching. - /// - public static readonly FieldDefinition[] UmbracoIndexFieldDefinitions = + // before we use regex to match do some faster simple matching since this is going to execute quite a lot + if (!fieldName.Contains("_")) { - new FieldDefinition("parentID", FieldDefinitionTypes.Integer), - new FieldDefinition("level", FieldDefinitionTypes.Integer), - new FieldDefinition("writerID", FieldDefinitionTypes.Integer), - new FieldDefinition("creatorID", FieldDefinitionTypes.Integer), - new FieldDefinition("sortOrder", FieldDefinitionTypes.Integer), - new FieldDefinition("template", FieldDefinitionTypes.Integer), - - new FieldDefinition("createDate", FieldDefinitionTypes.DateTime), - new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime), - - new FieldDefinition(UmbracoExamineFieldNames.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("version", FieldDefinitionTypes.Raw), - new FieldDefinition("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("template", FieldDefinitionTypes.Raw), - new FieldDefinition("urlName", FieldDefinitionTypes.InvariantCultureIgnoreCase), - new FieldDefinition("path", FieldDefinitionTypes.Raw), - - new FieldDefinition("email", FieldDefinitionTypes.EmailAddress), - - new FieldDefinition(UmbracoExamineFieldNames.PublishedFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.IndexPathFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.IconFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineFieldNames.VariesByCultureFieldName, FieldDefinitionTypes.Raw), - }; - - - /// - /// Overridden to dynamically add field definitions for culture variations - /// - /// - /// - /// - /// - /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions - /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... - /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like `nodeName_en-us` - /// and we don't want to have a full static list of all of these definitions when we can just define the one definition and then - /// dynamically apply that to culture specific fields. - /// - /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and store a new field - /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more objects. This does mean - /// however that if a language is deleted, the field definitions for that language will still exist in memory. This isn't going to cause any - /// problems and the mem will be cleared on next site restart but it's worth pointing out. - /// - public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) - { - if (base.TryGetValue(fieldName, out fieldDefinition)) - return true; - - //before we use regex to match do some faster simple matching since this is going to execute quite a lot - if (!fieldName.Contains("_")) - return false; - - var match = UmbracoExamineExtensions.CultureIsoCodeFieldNameMatchExpression.Match(fieldName); - if (match.Success) - { - var nonCultureFieldName = match.Groups["FieldName"].Value; - //check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field - if (base.TryGetValue(nonCultureFieldName, out var existingFieldDefinition)) - { - //now add a new field def - fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); - return true; - } - } return false; } + Match match = UmbracoExamineExtensions._cultureIsoCodeFieldNameMatchExpression.Match(fieldName); + if (match.Success) + { + var nonCultureFieldName = match.Groups["FieldName"].Value; + // check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field + if (base.TryGetValue(nonCultureFieldName, out FieldDefinition existingFieldDefinition)) + { + // now add a new field def + fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); + return true; + } + } + + return false; } } diff --git a/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs index 49607b5851..2c6377768a 100644 --- a/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs +++ b/src/Umbraco.Infrastructure/Examine/UmbracoIndexConfig.cs @@ -2,36 +2,29 @@ using Examine; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +public class UmbracoIndexConfig : IUmbracoIndexConfig { - public class UmbracoIndexConfig : IUmbracoIndexConfig + public UmbracoIndexConfig(IPublicAccessService publicAccessService, IScopeProvider scopeProvider) { - - public UmbracoIndexConfig(IPublicAccessService publicAccessService, IScopeProvider scopeProvider) - { - ScopeProvider = scopeProvider; - PublicAccessService = publicAccessService; - } - - protected IPublicAccessService PublicAccessService { get; } - protected IScopeProvider ScopeProvider { get; } - public IContentValueSetValidator GetContentValueSetValidator() - { - return new ContentValueSetValidator(false, true, PublicAccessService, ScopeProvider); - } - - public IContentValueSetValidator GetPublishedContentValueSetValidator() - { - return new ContentValueSetValidator(true, false, PublicAccessService, ScopeProvider); - } - - /// - /// Returns the for the member indexer - /// - /// - public IValueSetValidator GetMemberValueSetValidator() - { - return new MemberValueSetValidator(); - } + ScopeProvider = scopeProvider; + PublicAccessService = publicAccessService; } + + protected IPublicAccessService PublicAccessService { get; } + + protected IScopeProvider ScopeProvider { get; } + + public IContentValueSetValidator GetContentValueSetValidator() => + new ContentValueSetValidator(false, true, PublicAccessService, ScopeProvider); + + public IContentValueSetValidator GetPublishedContentValueSetValidator() => + new ContentValueSetValidator(true, false, PublicAccessService, ScopeProvider); + + /// + /// Returns the for the member indexer + /// + /// + public IValueSetValidator GetMemberValueSetValidator() => new MemberValueSetValidator(); } diff --git a/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs b/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs index 3bf4b97bf1..4931bd5220 100644 --- a/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs +++ b/src/Umbraco.Infrastructure/Examine/ValueSetValidator.cs @@ -1,100 +1,104 @@ -using System.Collections.Generic; -using System.Linq; using Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Examine +namespace Umbraco.Cms.Infrastructure.Examine; + +/// +/// Performing basic validation of a value set +/// +public class ValueSetValidator : IValueSetValidator { - /// - /// Performing basic validation of a value set - /// - public class ValueSetValidator : IValueSetValidator + public ValueSetValidator( + IEnumerable? includeItemTypes, + IEnumerable? excludeItemTypes, + IEnumerable? includeFields, + IEnumerable? excludeFields) { - public ValueSetValidator( - IEnumerable? includeItemTypes, - IEnumerable? excludeItemTypes, - IEnumerable? includeFields, - IEnumerable? excludeFields) + IncludeItemTypes = includeItemTypes; + ExcludeItemTypes = excludeItemTypes; + IncludeFields = includeFields; + ExcludeFields = excludeFields; + ValidIndexCategories = null; + } + + /// + /// Optional inclusion list of content types to index + /// + /// + /// All other types will be ignored if they do not match this list + /// + public IEnumerable? IncludeItemTypes { get; } + + /// + /// Optional exclusion list of content types to ignore + /// + /// + /// Any content type alias matched in this will not be included in the index + /// + public IEnumerable? ExcludeItemTypes { get; } + + /// + /// Optional inclusion list of index fields to index + /// + /// + /// If specified, all other fields in a will be filtered + /// + public IEnumerable? IncludeFields { get; } + + /// + /// Optional exclusion list of index fields + /// + /// + /// If specified, all fields matching these field names will be filtered from the + /// + public IEnumerable? ExcludeFields { get; } + + protected virtual IEnumerable? ValidIndexCategories { get; } + + public virtual ValueSetValidationResult Validate(ValueSet valueSet) + { + if (ValidIndexCategories != null && !ValidIndexCategories.InvariantContains(valueSet.Category)) { - IncludeItemTypes = includeItemTypes; - ExcludeItemTypes = excludeItemTypes; - IncludeFields = includeFields; - ExcludeFields = excludeFields; - ValidIndexCategories = null; + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); } - protected virtual IEnumerable? ValidIndexCategories { get; } - - /// - /// Optional inclusion list of content types to index - /// - /// - /// All other types will be ignored if they do not match this list - /// - public IEnumerable? IncludeItemTypes { get; } - - /// - /// Optional exclusion list of content types to ignore - /// - /// - /// Any content type alias matched in this will not be included in the index - /// - public IEnumerable? ExcludeItemTypes { get; } - - /// - /// Optional inclusion list of index fields to index - /// - /// - /// If specified, all other fields in a will be filtered - /// - public IEnumerable? IncludeFields { get; } - - /// - /// Optional exclusion list of index fields - /// - /// - /// If specified, all fields matching these field names will be filtered from the - /// - public IEnumerable? ExcludeFields { get; } - - public virtual ValueSetValidationResult Validate(ValueSet valueSet) + // check if this document is of a correct type of node type alias + if (IncludeItemTypes != null && !IncludeItemTypes.InvariantContains(valueSet.ItemType)) { - if (ValidIndexCategories != null && !ValidIndexCategories.InvariantContains(valueSet.Category)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - //check if this document is of a correct type of node type alias - if (IncludeItemTypes != null && !IncludeItemTypes.InvariantContains(valueSet.ItemType)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + // if this node type is part of our exclusion list + if (ExcludeItemTypes != null && ExcludeItemTypes.InvariantContains(valueSet.ItemType)) + { + return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + } - //if this node type is part of our exclusion list - if (ExcludeItemTypes != null && ExcludeItemTypes.InvariantContains(valueSet.ItemType)) - return new ValueSetValidationResult(ValueSetValidationStatus.Failed, valueSet); + var isFiltered = false; - var isFiltered = false; + var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - var filteredValues = valueSet.Values.ToDictionary(x => x.Key, x => x.Value.ToList()); - //filter based on the fields provided (if any) - if (IncludeFields != null || ExcludeFields != null) + // filter based on the fields provided (if any) + if (IncludeFields != null || ExcludeFields != null) + { + foreach (var key in valueSet.Values.Keys.ToList()) { - foreach (var key in valueSet.Values.Keys.ToList()) + if (IncludeFields != null && !IncludeFields.InvariantContains(key)) { - if (IncludeFields != null && !IncludeFields.InvariantContains(key)) - { - filteredValues.Remove(key); //remove any value with a key that doesn't match the inclusion list - isFiltered = true; - } - - if (ExcludeFields != null && ExcludeFields.InvariantContains(key)) - { - filteredValues.Remove(key); //remove any value with a key that matches the exclusion list - isFiltered = true; - } + filteredValues.Remove(key); // remove any value with a key that doesn't match the inclusion list + isFiltered = true; + } + if (ExcludeFields != null && ExcludeFields.InvariantContains(key)) + { + filteredValues.Remove(key); // remove any value with a key that matches the exclusion list + isFiltered = true; } } - - var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); - return new ValueSetValidationResult(isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } + + var filteredValueSet = new ValueSet(valueSet.Id, valueSet.Category, valueSet.ItemType, filteredValues.ToDictionary(x => x.Key, x => (IEnumerable)x.Value)); + return new ValueSetValidationResult( + isFiltered ? ValueSetValidationStatus.Filtered : ValueSetValidationStatus.Valid, filteredValueSet); } } diff --git a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs index 8918b5f951..6eb3350d71 100644 --- a/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/EmailMessageExtensions.cs @@ -1,132 +1,128 @@ -using System; -using System.Collections.Generic; using MimeKit; using MimeKit.Text; using Umbraco.Cms.Core.Models.Email; -namespace Umbraco.Cms.Infrastructure.Extensions +namespace Umbraco.Cms.Infrastructure.Extensions; + +internal static class EmailMessageExtensions { - internal static class EmailMessageExtensions + public static MimeMessage ToMimeMessage(this EmailMessage mailMessage, string configuredFromAddress) { - public static MimeMessage ToMimeMessage(this EmailMessage mailMessage, string configuredFromAddress) + var fromEmail = string.IsNullOrEmpty(mailMessage.From) ? configuredFromAddress : mailMessage.From; + + if (!InternetAddress.TryParse(fromEmail, out InternetAddress fromAddress)) { - var fromEmail = string.IsNullOrEmpty(mailMessage.From) ? configuredFromAddress : mailMessage.From; + throw new ArgumentException( + $"Email could not be sent. Could not parse from address {fromEmail} as a valid email address."); + } - if (!InternetAddress.TryParse(fromEmail, out InternetAddress fromAddress)) + var messageToSend = new MimeMessage { From = { fromAddress }, Subject = mailMessage.Subject }; + + AddAddresses(messageToSend, mailMessage.To, x => x.To, true); + AddAddresses(messageToSend, mailMessage.Cc, x => x.Cc); + AddAddresses(messageToSend, mailMessage.Bcc, x => x.Bcc); + AddAddresses(messageToSend, mailMessage.ReplyTo, x => x.ReplyTo); + + if (mailMessage.HasAttachments) + { + var builder = new BodyBuilder(); + if (mailMessage.IsBodyHtml) { - throw new ArgumentException($"Email could not be sent. Could not parse from address {fromEmail} as a valid email address."); - } - - var messageToSend = new MimeMessage - { - From = { fromAddress }, - Subject = mailMessage.Subject, - }; - - AddAddresses(messageToSend, mailMessage.To, x => x.To, throwIfNoneValid: true); - AddAddresses(messageToSend, mailMessage.Cc, x => x.Cc); - AddAddresses(messageToSend, mailMessage.Bcc, x => x.Bcc); - AddAddresses(messageToSend, mailMessage.ReplyTo, x => x.ReplyTo); - - if (mailMessage.HasAttachments) - { - var builder = new BodyBuilder(); - if (mailMessage.IsBodyHtml) - { - builder.HtmlBody = mailMessage.Body; - } - else - { - builder.TextBody = mailMessage.Body; - } - - foreach (EmailMessageAttachment attachment in mailMessage.Attachments!) - { - builder.Attachments.Add(attachment.FileName, attachment.Stream); - } - - messageToSend.Body = builder.ToMessageBody(); + builder.HtmlBody = mailMessage.Body; } else { - messageToSend.Body = new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body }; + builder.TextBody = mailMessage.Body; } - return messageToSend; + foreach (EmailMessageAttachment attachment in mailMessage.Attachments!) + { + builder.Attachments.Add(attachment.FileName, attachment.Stream); + } + + messageToSend.Body = builder.ToMessageBody(); + } + else + { + messageToSend.Body = + new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body }; } - private static void AddAddresses(MimeMessage message, string?[]? addresses, Func addressListGetter, bool throwIfNoneValid = false) + return messageToSend; + } + + public static NotificationEmailModel ToNotificationEmail( + this EmailMessage emailMessage, + string? configuredFromAddress) + { + var fromEmail = string.IsNullOrEmpty(emailMessage.From) ? configuredFromAddress : emailMessage.From; + + NotificationEmailAddress? from = ToNotificationAddress(fromEmail); + + return new NotificationEmailModel( + from, + GetNotificationAddresses(emailMessage.To), + GetNotificationAddresses(emailMessage.Cc), + GetNotificationAddresses(emailMessage.Bcc), + GetNotificationAddresses(emailMessage.ReplyTo), + emailMessage.Subject, + emailMessage.Body, + emailMessage.Attachments, + emailMessage.IsBodyHtml); + } + + private static void AddAddresses(MimeMessage message, string?[]? addresses, Func addressListGetter, bool throwIfNoneValid = false) + { + var foundValid = false; + if (addresses != null) { - var foundValid = false; - if (addresses != null) + foreach (var address in addresses) { - foreach (var address in addresses) + if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) { - if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) - { - addressListGetter(message).Add(internetAddress); - foundValid = true; - } + addressListGetter(message).Add(internetAddress); + foundValid = true; } } + } - if (throwIfNoneValid && foundValid == false) + if (throwIfNoneValid && foundValid == false) + { + throw new InvalidOperationException("Email could not be sent. Could not parse a valid recipient address."); + } + } + + private static NotificationEmailAddress? ToNotificationAddress(string? address) + { + if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) + { + if (internetAddress is MailboxAddress mailboxAddress) { - throw new InvalidOperationException($"Email could not be sent. Could not parse a valid recipient address."); + return new NotificationEmailAddress(mailboxAddress.Address, internetAddress.Name); } } - public static NotificationEmailModel ToNotificationEmail(this EmailMessage emailMessage, - string? configuredFromAddress) + return null; + } + + private static IEnumerable? GetNotificationAddresses(IEnumerable? addresses) + { + if (addresses is null) { - var fromEmail = string.IsNullOrEmpty(emailMessage.From) ? configuredFromAddress : emailMessage.From; - - NotificationEmailAddress? from = ToNotificationAddress(fromEmail); - - return new NotificationEmailModel( - from, - GetNotificationAddresses(emailMessage.To), - GetNotificationAddresses(emailMessage.Cc), - GetNotificationAddresses(emailMessage.Bcc), - GetNotificationAddresses(emailMessage.ReplyTo), - emailMessage.Subject, - emailMessage.Body, - emailMessage.Attachments, - emailMessage.IsBodyHtml); - } - - private static NotificationEmailAddress? ToNotificationAddress(string? address) - { - if (InternetAddress.TryParse(address, out InternetAddress internetAddress)) - { - if (internetAddress is MailboxAddress mailboxAddress) - { - return new NotificationEmailAddress(mailboxAddress.Address, internetAddress.Name); - } - } - return null; } - private static IEnumerable? GetNotificationAddresses(IEnumerable? addresses) + var notificationAddresses = new List(); + + foreach (var address in addresses) { - if (addresses is null) + NotificationEmailAddress? notificationAddress = ToNotificationAddress(address); + if (notificationAddress is not null) { - return null; + notificationAddresses.Add(notificationAddress); } - - var notificationAddresses = new List(); - - foreach (var address in addresses) - { - NotificationEmailAddress? notificationAddress = ToNotificationAddress(address); - if (notificationAddress is not null) - { - notificationAddresses.Add(notificationAddress); - } - } - - return notificationAddresses; } + + return notificationAddresses; } } diff --git a/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs b/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs index b420f77253..bb835a5244 100644 --- a/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/InfrastuctureTypeLoaderExtensions.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Packaging; -namespace Umbraco.Extensions -{ - public static class InfrastuctureTypeLoaderExtensions - { - /// - /// Gets all types implementing - /// - /// - /// - public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => mgr.GetTypes(); +namespace Umbraco.Extensions; - } +public static class InfrastuctureTypeLoaderExtensions +{ + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => + mgr.GetTypes(); } diff --git a/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs b/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs index 10f919589a..d9662501e2 100644 --- a/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/InstanceIdentifiableExtensions.cs @@ -1,20 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Text; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Extensions -{ - internal static class InstanceIdentifiableExtensions - { - public static string GetDebugInfo(this IInstanceIdentifiable instance) - { - if (instance == null) - { - return "(NULL)"; - } +namespace Umbraco.Extensions; - return $"(id: {instance.InstanceId.ToString("N").Substring(0, 8)} from thread: {instance.CreatedThreadId})"; +internal static class InstanceIdentifiableExtensions +{ + public static string GetDebugInfo(this IInstanceIdentifiable? instance) + { + if (instance == null) + { + return "(NULL)"; } + + return $"(id: {instance.InstanceId.ToString("N").Substring(0, 8)} from thread: {instance.CreatedThreadId})"; } } diff --git a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs index 62a3f96b22..3cad487bbc 100644 --- a/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/MediaPicker3ConfigurationExtensions.cs @@ -1,43 +1,40 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class MediaPicker3ConfigurationExtensions { - public static class MediaPicker3ConfigurationExtensions + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration? configuration) { - /// - /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. - /// - /// The configuration. - public static void ApplyConfiguration(this ImageCropperValue imageCropperValue, MediaPicker3Configuration? configuration) + var crops = new List(); + + MediaPicker3Configuration.CropConfiguration[]? configuredCrops = configuration?.Crops; + if (configuredCrops != null) { - var crops = new List(); - - var configuredCrops = configuration?.Crops; - if (configuredCrops != null) + foreach (MediaPicker3Configuration.CropConfiguration configuredCrop in configuredCrops) { - foreach (var configuredCrop in configuredCrops) + ImageCropperValue.ImageCropperCrop? crop = + imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperValue.ImageCropperCrop { - var crop = imageCropperValue.Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); - - crops.Add(new ImageCropperValue.ImageCropperCrop - { - Alias = configuredCrop.Alias, - Width = configuredCrop.Width, - Height = configuredCrop.Height, - Coordinates = crop?.Coordinates - }); - } + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates, + }); } + } - imageCropperValue.Crops = crops; + imageCropperValue.Crops = crops; - if (configuration?.EnableLocalFocalPoint == false) - { - imageCropperValue.FocalPoint = null; - } + if (configuration?.EnableLocalFocalPoint == false) + { + imageCropperValue.FocalPoint = null; } } } diff --git a/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs index 609d8165a3..6a34ae73f0 100644 --- a/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/ObjectJsonExtensions.cs @@ -1,52 +1,56 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Reflection; using Newtonsoft.Json; using Umbraco.Cms.Core; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides object extension methods. +/// +public static class ObjectJsonExtensions { + private static readonly ConcurrentDictionary> _toObjectTypes = new(); + /// - /// Provides object extension methods. + /// Converts an object's properties into a dictionary. /// - public static class ObjectJsonExtensions + /// The object to convert. + /// A property namer function. + /// A dictionary containing each properties. + public static Dictionary ToObjectDictionary(T obj, Func? namer = null) { - private static readonly ConcurrentDictionary> ToObjectTypes = new ConcurrentDictionary>(); - - /// - /// Converts an object's properties into a dictionary. - /// - /// The object to convert. - /// A property namer function. - /// A dictionary containing each properties. - public static Dictionary ToObjectDictionary(T obj, Func? namer = null) + if (obj == null) { - if (obj == null) return new Dictionary(); - - string DefaultNamer(PropertyInfo property) - { - var jsonProperty = property.GetCustomAttribute(); - return jsonProperty?.PropertyName ?? property.Name; - } - - var t = obj.GetType(); - - if (namer == null) namer = DefaultNamer; - - if (!ToObjectTypes.TryGetValue(t, out var properties)) - { - properties = new Dictionary(); - - foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)) - properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter(p); - - ToObjectTypes[t] = properties; - } - - return properties.ToDictionary(x => x.Key, x => ((Func) x.Value)(obj)); + return new Dictionary(); } + string DefaultNamer(PropertyInfo property) + { + JsonPropertyAttribute? jsonProperty = property.GetCustomAttribute(); + return jsonProperty?.PropertyName ?? property.Name; + } + + Type t = obj.GetType(); + + if (namer == null) + { + namer = DefaultNamer; + } + + if (!_toObjectTypes.TryGetValue(t, out Dictionary? properties)) + { + properties = new Dictionary(); + + foreach (PropertyInfo p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | + BindingFlags.FlattenHierarchy)) + { + properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter(p); + } + + _toObjectTypes[t] = properties; + } + + return properties.ToDictionary(x => x.Key, x => ((Func)x.Value)(obj)); } } diff --git a/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs index ed2c520070..d49c576924 100644 --- a/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs +++ b/src/Umbraco.Infrastructure/Extensions/ScopeExtensions.cs @@ -1,24 +1,22 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Scoping; -namespace Umbraco.Extensions -{ - public static class ScopeExtensions - { - public static void ReadLock(this IScope scope, ICollection lockIds) - { - foreach(var lockId in lockIds) - { - scope.ReadLock(lockId); - } - } +namespace Umbraco.Extensions; - public static void WriteLock(this IScope scope, ICollection lockIds) +public static class ScopeExtensions +{ + public static void ReadLock(this IScope scope, ICollection lockIds) + { + foreach (var lockId in lockIds) { - foreach (var lockId in lockIds) - { - scope.WriteLock(lockId); - } + scope.ReadLock(lockId); + } + } + + public static void WriteLock(this IScope scope, ICollection lockIds) + { + foreach (var lockId in lockIds) + { + scope.WriteLock(lockId); } } } diff --git a/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs index 64e329be97..4020dc3136 100644 --- a/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs +++ b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs @@ -1,34 +1,31 @@ -using HeyRed.MarkdownSharp; +using HeyRed.MarkdownSharp; using Umbraco.Cms.Core.HealthChecks; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Cms.Infrastructure.HealthChecks +namespace Umbraco.Cms.Infrastructure.HealthChecks; + +public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter { - public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter + public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity) { - public string ToHtml(HealthCheckResults results, HealthCheckNotificationVerbosity verbosity) - { - var mark = new Markdown(); - var html = mark.Transform(results.ResultsAsMarkDown(verbosity)); - html = ApplyHtmlHighlighting(html); - return html; - } - - private string ApplyHtmlHighlighting(string html) - { - const string SuccessHexColor = "5cb85c"; - const string WarningHexColor = "f0ad4e"; - const string ErrorHexColor = "d9534f"; - - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, SuccessHexColor); - html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, WarningHexColor); - return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, ErrorHexColor); - } - - private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color) - { - return html - .Replace("Result: '" + status + "'", "Result: " + status + ""); - } + var mark = new Markdown(); + var html = mark.Transform(results.ResultsAsMarkDown(verbosity)); + html = ApplyHtmlHighlighting(html); + return html; } + + private string ApplyHtmlHighlighting(string html) + { + const string successHexColor = "5cb85c"; + const string warningHexColor = "f0ad4e"; + const string errorHexColor = "d9534f"; + + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Success, successHexColor); + html = ApplyHtmlHighlightingForStatus(html, StatusResultType.Warning, warningHexColor); + return ApplyHtmlHighlightingForStatus(html, StatusResultType.Error, errorHexColor); + } + + private string ApplyHtmlHighlightingForStatus(string html, StatusResultType status, string color) => + html + .Replace("Result: '" + status + "'", "Result: " + status + ""); } diff --git a/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs index ece1827ed0..522fae5c4d 100644 --- a/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs +++ b/src/Umbraco.Infrastructure/HostedServices/BackgroundTaskQueue.cs @@ -1,42 +1,37 @@ -using System; using System.Collections.Concurrent; -using System.Threading; -using System.Threading.Tasks; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// A Background Task Queue, to enqueue tasks for executing in the background. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public class BackgroundTaskQueue : IBackgroundTaskQueue { - /// - /// A Background Task Queue, to enqueue tasks for executing in the background. - /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public class BackgroundTaskQueue : IBackgroundTaskQueue + private readonly SemaphoreSlim _signal = new(0); + + private readonly ConcurrentQueue> _workItems = new(); + + /// + public void QueueBackgroundWorkItem(Func workItem) { - private readonly ConcurrentQueue> _workItems = - new ConcurrentQueue>(); - - private readonly SemaphoreSlim _signal = new SemaphoreSlim(0); - - /// - public void QueueBackgroundWorkItem(Func workItem) + if (workItem == null) { - if (workItem == null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - _workItems.Enqueue(workItem); - _signal.Release(); + throw new ArgumentNullException(nameof(workItem)); } - /// - public async Task?> DequeueAsync(CancellationToken cancellationToken) - { - await _signal.WaitAsync(cancellationToken); - _workItems.TryDequeue(out Func? workItem); + _workItems.Enqueue(workItem); + _signal.Release(); + } - return workItem; - } + /// + public async Task?> DequeueAsync(CancellationToken cancellationToken) + { + await _signal.WaitAsync(cancellationToken); + _workItems.TryDequeue(out Func? workItem); + + return workItem; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs index 5fdd69035d..1b62e8e31d 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -8,89 +6,89 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Recurring hosted service that executes the content history cleanup. +/// +public class ContentVersionCleanup : RecurringHostedServiceBase { + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRoleAccessor; + private readonly IContentVersionService _service; + private readonly IOptionsMonitor _settingsMonitor; + /// - /// Recurring hosted service that executes the content history cleanup. + /// Initializes a new instance of the class. /// - public class ContentVersionCleanup : RecurringHostedServiceBase + public ContentVersionCleanup( + IRuntimeState runtimeState, + ILogger logger, + IOptionsMonitor settingsMonitor, + IContentVersionService service, + IMainDom mainDom, + IServerRoleAccessor serverRoleAccessor) + : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) { - private readonly IRuntimeState _runtimeState; - private readonly ILogger _logger; - private readonly IOptionsMonitor _settingsMonitor; - private readonly IContentVersionService _service; - private readonly IMainDom _mainDom; - private readonly IServerRoleAccessor _serverRoleAccessor; + _runtimeState = runtimeState; + _logger = logger; + _settingsMonitor = settingsMonitor; + _service = service; + _mainDom = mainDom; + _serverRoleAccessor = serverRoleAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public ContentVersionCleanup( - IRuntimeState runtimeState, - ILogger logger, - IOptionsMonitor settingsMonitor, - IContentVersionService service, - IMainDom mainDom, - IServerRoleAccessor serverRoleAccessor) - : base(logger, TimeSpan.FromHours(1), TimeSpan.FromMinutes(3)) + /// + public override Task PerformExecuteAsync(object? state) + { + // Globally disabled by feature flag + if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup) { - _runtimeState = runtimeState; - _logger = logger; - _settingsMonitor = settingsMonitor; - _service = service; - _mainDom = mainDom; - _serverRoleAccessor = serverRoleAccessor; + _logger.LogInformation( + "ContentVersionCleanup task will not run as it has been globally disabled via configuration"); + return Task.CompletedTask; } - /// - public override Task PerformExecuteAsync(object? state) + if (_runtimeState.Level != RuntimeLevel.Run) { - // Globally disabled by feature flag - if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup) - { - _logger.LogInformation("ContentVersionCleanup task will not run as it has been globally disabled via configuration"); + return Task.FromResult(true); // repeat... + } + + // Don't run on replicas nor unknown role servers + switch (_serverRoleAccessor.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers"); return Task.CompletedTask; - } - - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.FromResult(true); // repeat... - } - - // Don't run on replicas nor unknown role servers - switch (_serverRoleAccessor.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers"); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role"); - return Task.CompletedTask; - case ServerRole.Single: - case ServerRole.SchedulingPublisher: - default: - break; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (!_mainDom.IsMainDom) - { - _logger.LogDebug("Does not run if not MainDom"); - return Task.FromResult(false); // do NOT repeat, going down - } - - var count = _service.PerformContentVersionCleanup(DateTime.Now).Count; - - if (count > 0) - { - _logger.LogInformation("Deleted {count} ContentVersion(s)", count); - } - else - { - _logger.LogDebug("Task complete, no items were Deleted"); - } - - return Task.FromResult(true); + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role"); + return Task.CompletedTask; + case ServerRole.Single: + case ServerRole.SchedulingPublisher: + default: + break; } + + // Ensure we do not run if not main domain, but do NOT lock it + if (!_mainDom.IsMainDom) + { + _logger.LogDebug("Does not run if not MainDom"); + return Task.FromResult(false); // do NOT repeat, going down + } + + var count = _service.PerformContentVersionCleanup(DateTime.Now).Count; + + if (count > 0) + { + _logger.LogInformation("Deleted {count} ContentVersion(s)", count); + } + else + { + _logger.LogDebug("Task complete, no items were Deleted"); + } + + return Task.FromResult(true); } } diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs index efbb30291e..47e72cc8c5 100644 --- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs +++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -19,123 +15,122 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for recurring health check notifications. +/// +public class HealthCheckNotifier : RecurringHostedServiceBase { + private readonly HealthCheckCollection _healthChecks; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly HealthCheckNotificationMethodCollection _notifications; + private readonly IProfilingLogger _profilingLogger; + private readonly IRuntimeState _runtimeState; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerRoleAccessor _serverRegistrar; + private HealthChecksSettings _healthChecksSettings; + /// - /// Hosted service implementation for recurring health check notifications. + /// Initializes a new instance of the class. /// - public class HealthCheckNotifier : RecurringHostedServiceBase + /// The configuration for health check settings. + /// The collection of healthchecks. + /// The collection of healthcheck notification methods. + /// Representation of the state of the Umbraco runtime. + /// Provider of server registrations to the distributed cache. + /// Representation of the main application domain. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + /// Parser of crontab expressions. + public HealthCheckNotifier( + IOptionsMonitor healthChecksSettings, + HealthCheckCollection healthChecks, + HealthCheckNotificationMethodCollection notifications, + IRuntimeState runtimeState, + IServerRoleAccessor serverRegistrar, + IMainDom mainDom, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger, + ICronTabParser cronTabParser) + : base( + logger, + healthChecksSettings.CurrentValue.Notification.Period, + healthChecksSettings.CurrentValue.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay)) { - private HealthChecksSettings _healthChecksSettings; - private readonly HealthCheckCollection _healthChecks; - private readonly HealthCheckNotificationMethodCollection _notifications; - private readonly IRuntimeState _runtimeState; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IMainDom _mainDom; - private readonly ICoreScopeProvider _scopeProvider; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; + _healthChecksSettings = healthChecksSettings.CurrentValue; + _healthChecks = healthChecks; + _notifications = notifications; + _runtimeState = runtimeState; + _serverRegistrar = serverRegistrar; + _mainDom = mainDom; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; - /// - /// Initializes a new instance of the class. - /// - /// The configuration for health check settings. - /// The collection of healthchecks. - /// The collection of healthcheck notification methods. - /// Representation of the state of the Umbraco runtime. - /// Provider of server registrations to the distributed cache. - /// Representation of the main application domain. - /// Provides scopes for database operations. - /// The typed logger. - /// The profiling logger. - /// Parser of crontab expressions. - public HealthCheckNotifier( - IOptionsMonitor healthChecksSettings, - HealthCheckCollection healthChecks, - HealthCheckNotificationMethodCollection notifications, - IRuntimeState runtimeState, - IServerRoleAccessor serverRegistrar, - IMainDom mainDom, - ICoreScopeProvider scopeProvider, - ILogger logger, - IProfilingLogger profilingLogger, - ICronTabParser cronTabParser) - : base( - logger, - healthChecksSettings.CurrentValue.Notification.Period, - healthChecksSettings.CurrentValue.GetNotificationDelay(cronTabParser, DateTime.Now, DefaultDelay)) + healthChecksSettings.OnChange(x => { - _healthChecksSettings = healthChecksSettings.CurrentValue; - _healthChecks = healthChecks; - _notifications = notifications; - _runtimeState = runtimeState; - _serverRegistrar = serverRegistrar; - _mainDom = mainDom; - _scopeProvider = scopeProvider; - _logger = logger; - _profilingLogger = profilingLogger; + _healthChecksSettings = x; + ChangePeriod(x.Notification.Period); + }); + } - healthChecksSettings.OnChange(x => - { - _healthChecksSettings = x; - ChangePeriod(x.Notification.Period); - }); + public override async Task PerformExecuteAsync(object? state) + { + if (_healthChecksSettings.Notification.Enabled == false) + { + return; } - public override async Task PerformExecuteAsync(object? state) + if (_runtimeState.Level != RuntimeLevel.Run) { - if (_healthChecksSettings.Notification.Enabled == false) - { + return; + } + + switch (_serverRegistrar.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); return; - } - - if (_runtimeState.Level != RuntimeLevel.Run) - { + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); return; - } + } - switch (_serverRegistrar.CurrentServerRole) + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return; + } + + // Ensure we use an explicit scope since we are running on a background thread and plugin health + // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope + // isn't used since that can be problematic. + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete")) + { + // Don't notify for any checks that are disabled, nor for any disabled just for notifications. + Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks + .Select(x => x.Id) + .Union(_healthChecksSettings.DisabledChecks + .Select(x => x.Id)) + .Distinct() + .ToArray(); + + IEnumerable checks = _healthChecks + .Where(x => disabledCheckIds.Contains(x.Id) == false); + + HealthCheckResults results = await HealthCheckResults.Create(checks); + results.LogResults(); + + // Send using registered notification methods that are enabled. + foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled)) { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return; - } - - // Ensure we use an explicit scope since we are running on a background thread and plugin health - // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope - // isn't used since that can be problematic. - using (ICoreScope scope = _scopeProvider.CreateCoreScope()) - using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete")) - { - // Don't notify for any checks that are disabled, nor for any disabled just for notifications. - Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks - .Select(x => x.Id) - .Union(_healthChecksSettings.DisabledChecks - .Select(x => x.Id)) - .Distinct() - .ToArray(); - - IEnumerable checks = _healthChecks - .Where(x => disabledCheckIds.Contains(x.Id) == false); - - var results = await HealthCheckResults.Create(checks); - results.LogResults(); - - // Send using registered notification methods that are enabled. - foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled)) - { - await notificationMethod.SendAsync(results); - } + await notificationMethod.SendAsync(results); } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs index 4e3052dd9e..aa89d59d77 100644 --- a/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs +++ b/src/Umbraco.Infrastructure/HostedServices/IBackgroundTaskQueue.cs @@ -1,25 +1,20 @@ -using System; -using System.Threading; -using System.Threading.Tasks; +namespace Umbraco.Cms.Infrastructure.HostedServices; -namespace Umbraco.Cms.Infrastructure.HostedServices +/// +/// A Background Task Queue, to enqueue tasks for executing in the background. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public interface IBackgroundTaskQueue { /// - /// A Background Task Queue, to enqueue tasks for executing in the background. + /// Enqueue a work item to be executed on in the background. /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public interface IBackgroundTaskQueue - { - /// - /// Enqueue a work item to be executed on in the background. - /// - void QueueBackgroundWorkItem(Func workItem); + void QueueBackgroundWorkItem(Func workItem); - /// - /// Dequeue the first item on the queue. - /// - Task?> DequeueAsync(CancellationToken cancellationToken); - } + /// + /// Dequeue the first item on the queue. + /// + Task?> DequeueAsync(CancellationToken cancellationToken); } diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index e6b8ce47eb..b10f56cc74 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -16,99 +12,100 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Sync; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for keep alive feature. +/// +public class KeepAlive : RecurringHostedServiceBase { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _profilingLogger; + private readonly IServerRoleAccessor _serverRegistrar; + private KeepAliveSettings _keepAliveSettings; + /// - /// Hosted service implementation for keep alive feature. + /// Initializes a new instance of the class. /// - public class KeepAlive : RecurringHostedServiceBase + /// The current hosting environment + /// Representation of the main application domain. + /// The configuration for keep alive settings. + /// The typed logger. + /// The profiling logger. + /// Provider of server registrations to the distributed cache. + /// Factory for instances. + public KeepAlive( + IHostingEnvironment hostingEnvironment, + IMainDom mainDom, + IOptionsMonitor keepAliveSettings, + ILogger logger, + IProfilingLogger profilingLogger, + IServerRoleAccessor serverRegistrar, + IHttpClientFactory httpClientFactory) + : base(logger, TimeSpan.FromMinutes(5), DefaultDelay) { - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IMainDom _mainDom; - private KeepAliveSettings _keepAliveSettings; - private readonly ILogger _logger; - private readonly IProfilingLogger _profilingLogger; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IHttpClientFactory _httpClientFactory; + _hostingEnvironment = hostingEnvironment; + _mainDom = mainDom; + _keepAliveSettings = keepAliveSettings.CurrentValue; + _logger = logger; + _profilingLogger = profilingLogger; + _serverRegistrar = serverRegistrar; + _httpClientFactory = httpClientFactory; - /// - /// Initializes a new instance of the class. - /// - /// The current hosting environment - /// Representation of the main application domain. - /// The configuration for keep alive settings. - /// The typed logger. - /// The profiling logger. - /// Provider of server registrations to the distributed cache. - /// Factory for instances. - public KeepAlive( - IHostingEnvironment hostingEnvironment, - IMainDom mainDom, - IOptionsMonitor keepAliveSettings, - ILogger logger, - IProfilingLogger profilingLogger, - IServerRoleAccessor serverRegistrar, - IHttpClientFactory httpClientFactory) - : base(logger, TimeSpan.FromMinutes(5), DefaultDelay) + keepAliveSettings.OnChange(x => _keepAliveSettings = x); + } + + public override async Task PerformExecuteAsync(object? state) + { + if (_keepAliveSettings.DisableKeepAliveTask) { - _hostingEnvironment = hostingEnvironment; - _mainDom = mainDom; - _keepAliveSettings = keepAliveSettings.CurrentValue; - _logger = logger; - _profilingLogger = profilingLogger; - _serverRegistrar = serverRegistrar; - _httpClientFactory = httpClientFactory; - - keepAliveSettings.OnChange(x => _keepAliveSettings = x); + return; } - public override async Task PerformExecuteAsync(object? state) + // Don't run on replicas nor unknown role servers + switch (_serverRegistrar.CurrentServerRole) { - if (_keepAliveSettings.DisableKeepAliveTask) + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return; + } + + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return; + } + + using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) + { + var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString(); + if (umbracoAppUrl.IsNullOrWhiteSpace()) { + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); return; } - // Don't run on replicas nor unknown role servers - switch (_serverRegistrar.CurrentServerRole) + // If the config is an absolute path, just use it + var keepAlivePingUrl = WebPath.Combine( + umbracoAppUrl!, + _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); + + try { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return; + var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); + HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors); + _ = await httpClient.SendAsync(request); } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) + catch (Exception ex) { - _logger.LogDebug("Does not run if not MainDom."); - return; - } - - using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete")) - { - var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString(); - if (umbracoAppUrl.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return; - } - - // If the config is an absolute path, just use it - string keepAlivePingUrl = WebPath.Combine(umbracoAppUrl!, _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl)); - - try - { - var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl); - HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors); - _ = await httpClient.SendAsync(request); - } - catch (Exception ex) - { - _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); - } + _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl); } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs index 4877c4cb25..b69342d25b 100644 --- a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs +++ b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -12,82 +10,81 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices -{ - /// - /// Log scrubbing hosted service. - /// - /// - /// Will only run on non-replica servers. - /// - public class LogScrubber : RecurringHostedServiceBase - { - private readonly IMainDom _mainDom; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IAuditService _auditService; - private LoggingSettings _settings; - private readonly IProfilingLogger _profilingLogger; - private readonly ILogger _logger; - private readonly ICoreScopeProvider _scopeProvider; +namespace Umbraco.Cms.Infrastructure.HostedServices; - /// - /// Initializes a new instance of the class. - /// - /// Representation of the main application domain. - /// Provider of server registrations to the distributed cache. - /// Service for handling audit operations. - /// The configuration for logging settings. - /// Provides scopes for database operations. - /// The typed logger. - /// The profiling logger. - public LogScrubber( - IMainDom mainDom, - IServerRoleAccessor serverRegistrar, - IAuditService auditService, - IOptionsMonitor settings, - ICoreScopeProvider scopeProvider, - ILogger logger, - IProfilingLogger profilingLogger) - : base(logger, TimeSpan.FromHours(4), DefaultDelay) +/// +/// Log scrubbing hosted service. +/// +/// +/// Will only run on non-replica servers. +/// +public class LogScrubber : RecurringHostedServiceBase +{ + private readonly IAuditService _auditService; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerRoleAccessor _serverRegistrar; + private LoggingSettings _settings; + + /// + /// Initializes a new instance of the class. + /// + /// Representation of the main application domain. + /// Provider of server registrations to the distributed cache. + /// Service for handling audit operations. + /// The configuration for logging settings. + /// Provides scopes for database operations. + /// The typed logger. + /// The profiling logger. + public LogScrubber( + IMainDom mainDom, + IServerRoleAccessor serverRegistrar, + IAuditService auditService, + IOptionsMonitor settings, + ICoreScopeProvider scopeProvider, + ILogger logger, + IProfilingLogger profilingLogger) + : base(logger, TimeSpan.FromHours(4), DefaultDelay) + { + _mainDom = mainDom; + _serverRegistrar = serverRegistrar; + _auditService = auditService; + _settings = settings.CurrentValue; + _scopeProvider = scopeProvider; + _logger = logger; + _profilingLogger = profilingLogger; + settings.OnChange(x => _settings = x); + } + + public override Task PerformExecuteAsync(object? state) + { + switch (_serverRegistrar.CurrentServerRole) { - _mainDom = mainDom; - _serverRegistrar = serverRegistrar; - _auditService = auditService; - _settings = settings.CurrentValue; - _scopeProvider = scopeProvider; - _logger = logger; - _profilingLogger = profilingLogger; - settings.OnChange(x => _settings = x); + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return Task.CompletedTask; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return Task.CompletedTask; } - public override Task PerformExecuteAsync(object? state) + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) { - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return Task.CompletedTask; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - // Ensure we use an explicit scope since we are running on a background thread. - using (ICoreScope scope = _scopeProvider.CreateCoreScope()) - using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) - { - _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes); - _ = scope.Complete(); - } - + _logger.LogDebug("Does not run if not MainDom."); return Task.CompletedTask; } + + // Ensure we use an explicit scope since we are running on a background thread. + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) + using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) + { + _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes); + _ = scope.Complete(); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs index e271c98324..79d93a928f 100644 --- a/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs +++ b/src/Umbraco.Infrastructure/HostedServices/QueuedHostedService.cs @@ -1,62 +1,57 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// A queue based hosted service used to executing tasks on a background thread. +/// +/// +/// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 +/// +public class QueuedHostedService : BackgroundService { + private readonly ILogger _logger; - /// - /// A queue based hosted service used to executing tasks on a background thread. - /// - /// - /// Borrowed from https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-5.0 - /// - public class QueuedHostedService : BackgroundService + public QueuedHostedService( + IBackgroundTaskQueue taskQueue, + ILogger logger) { - private readonly ILogger _logger; + TaskQueue = taskQueue; + _logger = logger; + } - public QueuedHostedService(IBackgroundTaskQueue taskQueue, - ILogger logger) + public IBackgroundTaskQueue TaskQueue { get; } + + public override async Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Queued Hosted Service is stopping."); + + await base.StopAsync(stoppingToken); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) => + await BackgroundProcessing(stoppingToken); + + private async Task BackgroundProcessing(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) { - TaskQueue = taskQueue; - _logger = logger; - } + Func? workItem = await TaskQueue.DequeueAsync(stoppingToken); - public IBackgroundTaskQueue TaskQueue { get; } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await BackgroundProcessing(stoppingToken); - } - - private async Task BackgroundProcessing(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) + try { - Func? workItem = await TaskQueue.DequeueAsync(stoppingToken); - - try + if (workItem is not null) { - if (workItem is not null) - { - await workItem(stoppingToken); - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error occurred executing {WorkItem}.", nameof(workItem)); + await workItem(stoppingToken); } } - } - - public override async Task StopAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Queued Hosted Service is stopping."); - - await base.StopAsync(stoppingToken); + catch (Exception ex) + { + _logger.LogError( + ex, + "Error occurred executing {WorkItem}.", nameof(workItem)); + } } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index 4888d173d7..ab807b5f97 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -1,128 +1,130 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Provides a base class for recurring background tasks implemented as hosted services. +/// +/// +/// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks +/// +public abstract class RecurringHostedServiceBase : IHostedService, IDisposable { /// - /// Provides a base class for recurring background tasks implemented as hosted services. + /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is + /// configured. /// - /// - /// See: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks - /// - public abstract class RecurringHostedServiceBase : IHostedService, IDisposable + protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); + + private readonly TimeSpan _delay; + + private readonly ILogger? _logger; + private bool _disposedValue; + private TimeSpan _period; + private Timer? _timer; + + /// + /// Initializes a new instance of the class. + /// + /// Logger. + /// Timespan representing how often the task should recur. + /// + /// Timespan representing the initial delay after application start-up before the first run of the task + /// occurs. + /// + protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay) { - /// - /// The default delay to use for recurring tasks for the first run after application start-up if no alternative is configured. - /// - protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); + _logger = logger; + _period = period; + _delay = delay; + } - private readonly ILogger? _logger; - private TimeSpan _period; - private readonly TimeSpan _delay; - private Timer? _timer; - private bool _disposedValue; + // Scheduled for removal in V11 + [Obsolete("Please use constructor that takes an ILogger instead")] + protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay) + : this(null, period, delay) + { + } - /// - /// Initializes a new instance of the class. - /// - /// Logger. - /// Timespan representing how often the task should recur. - /// Timespan representing the initial delay after application start-up before the first run of the task occurs. - protected RecurringHostedServiceBase(ILogger? logger, TimeSpan period, TimeSpan delay) + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) { - _logger = logger; - _period = period; - _delay = delay; + _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); } - // Scheduled for removal in V11 - [Obsolete("Please use constructor that takes an ILogger instead")] - protected RecurringHostedServiceBase(TimeSpan period, TimeSpan delay) - : this(null, period, delay) - { } + return Task.CompletedTask; + } - /// - /// Change the period between operations. - /// - /// The new period between tasks - protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod; + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _period = Timeout.InfiniteTimeSpan; + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } - /// - public Task StartAsync(CancellationToken cancellationToken) + /// + /// Executes the task. + /// + /// The task state. + public async void ExecuteAsync(object? state) + { + try { - using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null) - { - _timer = new Timer(ExecuteAsync, null, (int)_delay.TotalMilliseconds, (int)_period.TotalMilliseconds); - } - - return Task.CompletedTask; - } - - /// - /// Executes the task. - /// - /// The task state. - public async void ExecuteAsync(object? state) - { - try - { - // First, stop the timer, we do not want tasks to execute in parallel - _timer?.Change(Timeout.Infinite, 0); - - // Delegate work to method returning a task, that can be called and asserted in a unit test. - // Without this there can be behaviour where tests pass, but an error within them causes the test - // running process to crash. - // Hat-tip: https://stackoverflow.com/a/14207615/489433 - await PerformExecuteAsync(state); - } - catch (Exception ex) - { - ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType()); - logger.LogError(ex, "Unhandled exception in recurring hosted service."); - } - finally - { - // Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay. - // So first execution is after _delay, and the we wait _period between each - _timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds); - } - } - - public abstract Task PerformExecuteAsync(object? state); - - /// - public Task StopAsync(CancellationToken cancellationToken) - { - _period = Timeout.InfiniteTimeSpan; + // First, stop the timer, we do not want tasks to execute in parallel _timer?.Change(Timeout.Infinite, 0); - return Task.CompletedTask; - } - protected virtual void Dispose(bool disposing) + // Delegate work to method returning a task, that can be called and asserted in a unit test. + // Without this there can be behaviour where tests pass, but an error within them causes the test + // running process to crash. + // Hat-tip: https://stackoverflow.com/a/14207615/489433 + await PerformExecuteAsync(state); + } + catch (Exception ex) { - if (!_disposedValue) + ILogger logger = _logger ?? StaticApplicationLogging.CreateLogger(GetType()); + logger.LogError(ex, "Unhandled exception in recurring hosted service."); + } + finally + { + // Resume now that the task is complete - Note we use period in both because we don't want to execute again after the delay. + // So first execution is after _delay, and the we wait _period between each + _timer?.Change((int)_period.TotalMilliseconds, (int)_period.TotalMilliseconds); + } + } + + public abstract Task PerformExecuteAsync(object? state); + + /// + /// Change the period between operations. + /// + /// The new period between tasks + protected void ChangePeriod(TimeSpan newPeriod) => _period = newPeriod; + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) { - if (disposing) - { - _timer?.Dispose(); - } - - _disposedValue = true; + _timer?.Dispose(); } - } - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposedValue = true; } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 54137fad99..658b8dd47d 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -1,7 +1,4 @@ -using System; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,81 +9,79 @@ using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +public class ReportSiteTask : RecurringHostedServiceBase { - public class ReportSiteTask : RecurringHostedServiceBase + private static HttpClient _httpClient = new(); + private readonly ILogger _logger; + private readonly ITelemetryService _telemetryService; + + public ReportSiteTask( + ILogger logger, + ITelemetryService telemetryService) + : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) { - private readonly ILogger _logger; - private readonly ITelemetryService _telemetryService; - private static HttpClient s_httpClient = new(); + _logger = logger; + _telemetryService = telemetryService; + _httpClient = new HttpClient(); + } - public ReportSiteTask( - ILogger logger, - ITelemetryService telemetryService) - : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) + [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] + public ReportSiteTask( + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptions globalSettings) + : this(logger, StaticServiceProvider.Instance.GetRequiredService()) + { + } + + /// + /// Runs the background task to send the anonymous ID + /// to telemetry service + /// + public override async Task PerformExecuteAsync(object? state) + { + if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) { - _logger = logger; - _telemetryService = telemetryService; - s_httpClient = new HttpClient(); + _logger.LogWarning("No telemetry marker found"); + + return; } - [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] - public ReportSiteTask( - ILogger logger, - IUmbracoVersion umbracoVersion, - IOptions globalSettings) - : this(logger, StaticServiceProvider.Instance.GetRequiredService()) + try { - } - - /// - /// Runs the background task to send the anonymous ID - /// to telemetry service - /// - public override async Task PerformExecuteAsync(object? state) - { - if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) + if (_httpClient.BaseAddress is null) { - _logger.LogWarning("No telemetry marker found"); - - return; - } - - try - { - if (s_httpClient.BaseAddress is null) - { - // Send data to LIVE telemetry - s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); + // Send data to LIVE telemetry + _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); #if DEBUG - // Send data to DEBUG telemetry service - s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); + // Send data to DEBUG telemetry service + _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); #endif - } - - - s_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); - - using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) - { - request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header - - // Make a HTTP Post to telemetry service - // https://telemetry.umbraco.com/installs/ - // Fire & Forget, do not need to know if its a 200, 500 etc - using (HttpResponseMessage response = await s_httpClient.SendAsync(request)) - { - } - } } - catch + + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + + using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) { - // Silently swallow - // The user does not need the logs being polluted if our service has fallen over or is down etc - // Hence only logging this at a more verbose level (which users should not be using in production) - _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); + request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); + + // Make a HTTP Post to telemetry service + // https://telemetry.umbraco.com/installs/ + // Fire & Forget, do not need to know if its a 200, 500 etc + using (await _httpClient.SendAsync(request)) + { + } } } + catch + { + // Silently swallow + // The user does not need the logs being polluted if our service has fallen over or is down etc + // Hence only logging this at a more verbose level (which users should not be using in production) + _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); + } } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs index 6d659425e0..d593124ccb 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs @@ -1,11 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Runtime; @@ -13,129 +8,127 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Hosted service implementation for scheduled publishing feature. +/// +/// +/// Runs only on non-replica servers. +/// +public class ScheduledPublishing : RecurringHostedServiceBase { + private readonly IContentService _contentService; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly IRuntimeState _runtimeState; + private readonly ICoreScopeProvider _scopeProvider; + private readonly IServerMessenger _serverMessenger; + private readonly IServerRoleAccessor _serverRegistrar; + private readonly IUmbracoContextFactory _umbracoContextFactory; + /// - /// Hosted service implementation for scheduled publishing feature. + /// Initializes a new instance of the class. /// - /// - /// Runs only on non-replica servers. - public class ScheduledPublishing : RecurringHostedServiceBase + public ScheduledPublishing( + IRuntimeState runtimeState, + IMainDom mainDom, + IServerRoleAccessor serverRegistrar, + IContentService contentService, + IUmbracoContextFactory umbracoContextFactory, + ILogger logger, + IServerMessenger serverMessenger, + ICoreScopeProvider scopeProvider) + : base(logger, TimeSpan.FromMinutes(1), DefaultDelay) { - private readonly IContentService _contentService; - private readonly ILogger _logger; - private readonly IMainDom _mainDom; - private readonly IRuntimeState _runtimeState; - private readonly IServerMessenger _serverMessenger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly IServerRoleAccessor _serverRegistrar; - private readonly IUmbracoContextFactory _umbracoContextFactory; + _runtimeState = runtimeState; + _mainDom = mainDom; + _serverRegistrar = serverRegistrar; + _contentService = contentService; + _umbracoContextFactory = umbracoContextFactory; + _logger = logger; + _serverMessenger = serverMessenger; + _scopeProvider = scopeProvider; + } - /// - /// Initializes a new instance of the class. - /// - public ScheduledPublishing( - IRuntimeState runtimeState, - IMainDom mainDom, - IServerRoleAccessor serverRegistrar, - IContentService contentService, - IUmbracoContextFactory umbracoContextFactory, - ILogger logger, - IServerMessenger serverMessenger, - ICoreScopeProvider scopeProvider) - : base(logger, TimeSpan.FromMinutes(1), DefaultDelay) + public override Task PerformExecuteAsync(object? state) + { + if (Suspendable.ScheduledPublishing.CanRun == false) { - _runtimeState = runtimeState; - _mainDom = mainDom; - _serverRegistrar = serverRegistrar; - _contentService = contentService; - _umbracoContextFactory = umbracoContextFactory; - _logger = logger; - _serverMessenger = serverMessenger; - _scopeProvider = scopeProvider; - } - - public override Task PerformExecuteAsync(object? state) - { - if (Suspendable.ScheduledPublishing.CanRun == false) - { - return Task.CompletedTask; - } - - switch (_serverRegistrar.CurrentServerRole) - { - case ServerRole.Subscriber: - _logger.LogDebug("Does not run on subscriber servers."); - return Task.CompletedTask; - case ServerRole.Unknown: - _logger.LogDebug("Does not run on servers with unknown role."); - return Task.CompletedTask; - } - - // Ensure we do not run if not main domain, but do NOT lock it - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - // Do NOT run publishing if not properly running - if (_runtimeState.Level != RuntimeLevel.Run) - { - _logger.LogDebug("Does not run if run level is not Run."); - return Task.CompletedTask; - } - - try - { - // Ensure we run with an UmbracoContext, because this will run in a background task, - // and developers may be using the UmbracoContext in the event handlers. - - // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events - // - UmbracoContext 'current' needs to be refactored and cleaned up - // - batched messenger should not depend on a current HttpContext - // but then what should be its "scope"? could we attach it to scopes? - // - and we should definitively *not* have to flush it here (should be auto) - - using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - - /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) - * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. - * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel. - * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's - * only until the old SchedulingPublisher shuts down. */ - scope.EagerWriteLock(Constants.Locks.ScheduledPublishing); - try - { - // Run - IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now); - foreach (IGrouping grouped in result.GroupBy(x => x.Result)) - { - _logger.LogInformation( - "Scheduled publishing result: '{StatusCount}' items with status {Status}", - grouped.Count(), - grouped.Key); - } - } - finally - { - // If running on a temp context, we have to flush the messenger - if (contextReference.IsRoot) - { - _serverMessenger.SendMessages(); - } - } - } - catch (Exception ex) - { - // important to catch *everything* to ensure the task repeats - _logger.LogError(ex, "Failed."); - } - return Task.CompletedTask; } + + switch (_serverRegistrar.CurrentServerRole) + { + case ServerRole.Subscriber: + _logger.LogDebug("Does not run on subscriber servers."); + return Task.CompletedTask; + case ServerRole.Unknown: + _logger.LogDebug("Does not run on servers with unknown role."); + return Task.CompletedTask; + } + + // Ensure we do not run if not main domain, but do NOT lock it + if (_mainDom.IsMainDom == false) + { + _logger.LogDebug("Does not run if not MainDom."); + return Task.CompletedTask; + } + + // Do NOT run publishing if not properly running + if (_runtimeState.Level != RuntimeLevel.Run) + { + _logger.LogDebug("Does not run if run level is not Run."); + return Task.CompletedTask; + } + + try + { + // Ensure we run with an UmbracoContext, because this will run in a background task, + // and developers may be using the UmbracoContext in the event handlers. + + // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events + // - UmbracoContext 'current' needs to be refactored and cleaned up + // - batched messenger should not depend on a current HttpContext + // but then what should be its "scope"? could we attach it to scopes? + // - and we should definitively *not* have to flush it here (should be auto) + using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher) + * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments. + * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel. + * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's + * only until the old SchedulingPublisher shuts down. */ + scope.EagerWriteLock(Constants.Locks.ScheduledPublishing); + try + { + // Run + IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (IGrouping grouped in result.GroupBy(x => x.Result)) + { + _logger.LogInformation( + "Scheduled publishing result: '{StatusCount}' items with status {Status}", + grouped.Count(), + grouped.Key); + } + } + finally + { + // If running on a temp context, we have to flush the messenger + if (contextReference.IsRoot) + { + _serverMessenger.SendMessages(); + } + } + } + catch (Exception ex) + { + // important to catch *everything* to ensure the task repeats + _logger.LogError(ex, "Failed."); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs index d153366949..e4e5700496 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -10,65 +8,64 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration +namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; + +/// +/// Implements periodic database instruction processing as a hosted service. +/// +public class InstructionProcessTask : RecurringHostedServiceBase { + private readonly ILogger _logger; + private readonly IServerMessenger _messenger; + private readonly IRuntimeState _runtimeState; + private bool _disposedValue; + /// - /// Implements periodic database instruction processing as a hosted service. + /// Initializes a new instance of the class. /// - public class InstructionProcessTask : RecurringHostedServiceBase + /// Representation of the state of the Umbraco runtime. + /// Service broadcasting cache notifications to registered servers. + /// The typed logger. + /// The configuration for global settings. + public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings) + : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) { - private readonly IRuntimeState _runtimeState; - private readonly IServerMessenger _messenger; - private readonly ILogger _logger; - private bool _disposedValue; + _runtimeState = runtimeState; + _messenger = messenger; + _logger = logger; + } - /// - /// Initializes a new instance of the class. - /// - /// Representation of the state of the Umbraco runtime. - /// Service broadcasting cache notifications to registered servers. - /// The typed logger. - /// The configuration for global settings. - public InstructionProcessTask(IRuntimeState runtimeState, IServerMessenger messenger, ILogger logger, IOptions globalSettings) - : base(logger, globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations, TimeSpan.FromMinutes(1)) + public override Task PerformExecuteAsync(object? state) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - _runtimeState = runtimeState; - _messenger = messenger; - _logger = logger; - } - - public override Task PerformExecuteAsync(object? state) - { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.CompletedTask; - } - - try - { - _messenger.Sync(); - } - catch (Exception e) - { - _logger.LogError(e, "Failed (will repeat)."); - } - return Task.CompletedTask; } - protected override void Dispose(bool disposing) + try { - if (!_disposedValue) - { - if (disposing && _messenger is IDisposable disposable) - { - disposable.Dispose(); - } + _messenger.Sync(); + } + catch (Exception e) + { + _logger.LogError(e, "Failed (will repeat)."); + } - _disposedValue = true; + return Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing && _messenger is IDisposable disposable) + { + disposable.Dispose(); } - base.Dispose(disposing); + _disposedValue = true; } + + base.Dispose(disposing); } } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index d755324878..730282c6b0 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -1,10 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,85 +8,87 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; -using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration +namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; + +/// +/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. +/// +public class TouchServerTask : RecurringHostedServiceBase { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IServerRegistrationService _serverRegistrationService; + private readonly IServerRoleAccessor _serverRoleAccessor; + private GlobalSettings _globalSettings; + /// - /// Implements periodic server "touching" (to mark as active/deactive) as a hosted service. + /// Initializes a new instance of the class. /// - public class TouchServerTask : RecurringHostedServiceBase + /// Representation of the state of the Umbraco runtime. + /// Services for server registrations. + /// The typed logger. + /// The configuration for global settings. + /// The hostingEnviroment. + /// The accessor for the server role + public TouchServerTask( + IRuntimeState runtimeState, + IServerRegistrationService serverRegistrationService, + IHostingEnvironment hostingEnvironment, + ILogger logger, + IOptionsMonitor globalSettings, + IServerRoleAccessor serverRoleAccessor) + : base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) { - private readonly IRuntimeState _runtimeState; - private readonly IServerRegistrationService _serverRegistrationService; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly ILogger _logger; - private readonly IServerRoleAccessor _serverRoleAccessor; - private GlobalSettings _globalSettings; - - /// - /// Initializes a new instance of the class. - /// - /// Representation of the state of the Umbraco runtime. - /// Services for server registrations. - /// Accessor for the current request. - /// The typed logger. - /// The configuration for global settings. - public TouchServerTask( - IRuntimeState runtimeState, - IServerRegistrationService serverRegistrationService, - IHostingEnvironment hostingEnvironment, - ILogger logger, - IOptionsMonitor globalSettings, - IServerRoleAccessor serverRoleAccessor) - : base(logger, globalSettings.CurrentValue.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) + _runtimeState = runtimeState; + _serverRegistrationService = serverRegistrationService ?? + throw new ArgumentNullException(nameof(serverRegistrationService)); + _hostingEnvironment = hostingEnvironment; + _logger = logger; + _globalSettings = globalSettings.CurrentValue; + globalSettings.OnChange(x => { - _runtimeState = runtimeState; - _serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService)); - _hostingEnvironment = hostingEnvironment; - _logger = logger; - _globalSettings = globalSettings.CurrentValue; - globalSettings.OnChange(x => - { - _globalSettings = x; - ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls); - }); - _serverRoleAccessor = serverRoleAccessor; - } + _globalSettings = x; + ChangePeriod(x.DatabaseServerRegistrar.WaitTimeBetweenCalls); + }); + _serverRoleAccessor = serverRoleAccessor; + } - public override Task PerformExecuteAsync(object? state) + public override Task PerformExecuteAsync(object? state) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return Task.CompletedTask; - } - - // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, - // since all it's used for is to allow the ElectedServerRoleAccessor - // to figure out what role a given server has, so we just stop this task. - if (_serverRoleAccessor is not ElectedServerRoleAccessor) - { - return StopAsync(CancellationToken.None); - } - - var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); - if (serverAddress.IsNullOrWhiteSpace()) - { - _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); - return Task.CompletedTask; - } - - try - { - _serverRegistrationService.TouchServer(serverAddress!, _globalSettings.DatabaseServerRegistrar.StaleServerTimeout); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update server record in database."); - } - return Task.CompletedTask; } + + // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, + // since all it's used for is to allow the ElectedServerRoleAccessor + // to figure out what role a given server has, so we just stop this task. + if (_serverRoleAccessor is not ElectedServerRoleAccessor) + { + return StopAsync(CancellationToken.None); + } + + var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); + if (serverAddress.IsNullOrWhiteSpace()) + { + _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); + return Task.CompletedTask; + } + + try + { + _serverRegistrationService.TouchServer( + serverAddress!, + _globalSettings.DatabaseServerRegistrar.StaleServerTimeout); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update server record in database."); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs index ec8d48bca3..663a89b05a 100644 --- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs +++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs @@ -1,102 +1,99 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Runtime; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +/// +/// Used to cleanup temporary file locations. +/// +/// +/// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will +/// ensure that in the case it happens on subscribers that they are cleaned up too. +/// +public class TempFileCleanup : RecurringHostedServiceBase { + private readonly TimeSpan _age = TimeSpan.FromDays(1); + private readonly IIOHelper _ioHelper; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + + private readonly DirectoryInfo[] _tempFolders; + /// - /// Used to cleanup temporary file locations. + /// Initializes a new instance of the class. /// - /// - /// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will - /// ensure that in the case it happens on subscribers that they are cleaned up too. - /// - public class TempFileCleanup : RecurringHostedServiceBase + /// Helper service for IO operations. + /// Representation of the main application domain. + /// The typed logger. + public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) + : base(logger, TimeSpan.FromMinutes(60), DefaultDelay) { - private readonly IIOHelper _ioHelper; - private readonly IMainDom _mainDom; - private readonly ILogger _logger; + _ioHelper = ioHelper; + _mainDom = mainDom; + _logger = logger; - private readonly DirectoryInfo[] _tempFolders; - private readonly TimeSpan _age = TimeSpan.FromDays(1); + _tempFolders = _ioHelper.GetTempFolders(); + } - /// - /// Initializes a new instance of the class. - /// - /// Helper service for IO operations. - /// Representation of the main application domain. - /// The typed logger. - public TempFileCleanup(IIOHelper ioHelper, IMainDom mainDom, ILogger logger) - : base(logger, TimeSpan.FromMinutes(60), DefaultDelay) + public override Task PerformExecuteAsync(object? state) + { + // Ensure we do not run if not main domain + if (_mainDom.IsMainDom == false) { - _ioHelper = ioHelper; - _mainDom = mainDom; - _logger = logger; - - _tempFolders = _ioHelper.GetTempFolders(); - } - - public override Task PerformExecuteAsync(object? state) - { - // Ensure we do not run if not main domain - if (_mainDom.IsMainDom == false) - { - _logger.LogDebug("Does not run if not MainDom."); - return Task.CompletedTask; - } - - foreach (DirectoryInfo folder in _tempFolders) - { - CleanupFolder(folder); - } - + _logger.LogDebug("Does not run if not MainDom."); return Task.CompletedTask; } - private void CleanupFolder(DirectoryInfo folder) + foreach (DirectoryInfo folder in _tempFolders) { - CleanFolderResult result = _ioHelper.CleanFolder(folder, _age); - switch (result.Status) - { - case CleanFolderResultStatus.FailedAsDoesNotExist: - _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); - break; - case CleanFolderResultStatus.FailedWithException: - foreach (CleanFolderResult.Error error in result.Errors!) - { - _logger.LogError(error.Exception, "Could not delete temp file {FileName}", error.ErroringFile.FullName); - } + CleanupFolder(folder); + } - break; - } + return Task.CompletedTask; + } - folder.Refresh(); // In case it's changed during runtime - if (!folder.Exists) - { + private void CleanupFolder(DirectoryInfo folder) + { + CleanFolderResult result = _ioHelper.CleanFolder(folder, _age); + switch (result.Status) + { + case CleanFolderResultStatus.FailedAsDoesNotExist: _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); - return; - } - - FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); - foreach (FileInfo file in files) - { - if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) + break; + case CleanFolderResultStatus.FailedWithException: + foreach (CleanFolderResult.Error error in result.Errors!) { - try - { - file.IsReadOnly = false; - file.Delete(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); - } + _logger.LogError(error.Exception, "Could not delete temp file {FileName}", + error.ErroringFile.FullName); + } + + break; + } + + folder.Refresh(); // In case it's changed during runtime + if (!folder.Exists) + { + _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName); + return; + } + + FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories); + foreach (FileInfo file in files) + { + if (DateTime.UtcNow - file.LastWriteTimeUtc > _age) + { + try + { + file.IsReadOnly = false; + file.Delete(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName); } } } diff --git a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs index ab71edf650..cc034e5768 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQuery.cs @@ -1,116 +1,131 @@ -using System; -using System.Collections.Generic; using System.Xml.XPath; using Examine.Search; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Query methods used for accessing strongly typed content in templates. +/// +public interface IPublishedContentQuery { + IPublishedContent? Content(int id); + + IPublishedContent? Content(Guid id); + + IPublishedContent? Content(Udi id); + + IPublishedContent? Content(object id); + + IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars); + + IEnumerable Content(IEnumerable ids); + + IEnumerable Content(IEnumerable ids); + + IEnumerable Content(IEnumerable ids); + + IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars); + + IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); + + IEnumerable ContentAtRoot(); + + IPublishedContent? Media(int id); + + IPublishedContent? Media(Guid id); + + IPublishedContent? Media(Udi id); + + IPublishedContent? Media(object id); + + IEnumerable Media(IEnumerable ids); + + IEnumerable Media(IEnumerable ids); + + IEnumerable Media(IEnumerable ids); + + IEnumerable MediaAtRoot(); + /// - /// Query methods used for accessing strongly typed content in templates. + /// Searches content. /// - public interface IPublishedContentQuery - { - IPublishedContent? Content(int id); + /// The term to search. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// The culture (defaults to a culture insensitive search). + /// + /// The name of the index to search (defaults to + /// ). + /// + /// + /// This parameter is no longer used, because the results are loaded from the published snapshot + /// using the single item ID field. + /// + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all + /// invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search( + string term, + int skip, + int take, + out long totalRecords, + string culture = "*", + string indexName = Constants.UmbracoIndexes.ExternalIndexName, + ISet? loadedFields = null); - IPublishedContent? Content(Guid id); + /// + /// Searches content. + /// + /// The term to search. + /// The culture (defaults to a culture insensitive search). + /// + /// The name of the index to search (defaults to + /// ). + /// + /// + /// The search results. + /// + /// + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all + /// invariant fields for all documents. + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// + IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); - IPublishedContent? Content(Udi id); + /// + /// Executes the query and converts the results to . + /// + /// The query. + /// + /// The search results. + /// + IEnumerable Search(IQueryExecutor query); - IPublishedContent? Content(object id); - - IPublishedContent? ContentSingleAtXPath(string xpath, params XPathVariable[] vars); - - IEnumerable Content(IEnumerable ids); - - IEnumerable Content(IEnumerable ids); - - IEnumerable Content(IEnumerable ids); - - IEnumerable ContentAtXPath(string xpath, params XPathVariable[] vars); - - IEnumerable ContentAtXPath(XPathExpression xpath, params XPathVariable[] vars); - - IEnumerable ContentAtRoot(); - - IPublishedContent? Media(int id); - - IPublishedContent? Media(Guid id); - - IPublishedContent? Media(Udi id); - - IPublishedContent? Media(object id); - - IEnumerable Media(IEnumerable ids); - - IEnumerable Media(IEnumerable ids); - - IEnumerable Media(IEnumerable ids); - - IEnumerable MediaAtRoot(); - - /// - /// Searches content. - /// - /// The term to search. - /// The amount of results to skip. - /// The amount of results to take/return. - /// The total amount of records. - /// The culture (defaults to a culture insensitive search). - /// The name of the index to search (defaults to ). - /// This parameter is no longer used, because the results are loaded from the published snapshot using the single item ID field. - /// - /// The search results. - /// - /// - /// - /// When the is not specified or is *, all cultures are searched. - /// To search for only invariant documents and fields use null. - /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName, ISet? loadedFields = null); - - /// - /// Searches content. - /// - /// The term to search. - /// The culture (defaults to a culture insensitive search). - /// The name of the index to search (defaults to ). - /// - /// The search results. - /// - /// - /// - /// When the is not specified or is *, all cultures are searched. - /// To search for only invariant documents and fields use null. - /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. - /// - /// While enumerating results, the ambient culture is changed to be the searched culture. - /// - IEnumerable Search(string term, string culture = "*", string indexName = Constants.UmbracoIndexes.ExternalIndexName); - - /// - /// Executes the query and converts the results to . - /// - /// The query. - /// - /// The search results. - /// - IEnumerable Search(IQueryExecutor query); - - /// - /// Executes the query and converts the results to . - /// - /// The query. - /// The amount of results to skip. - /// The amount of results to take/return. - /// The total amount of records. - /// - /// The search results. - /// - IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); - } + /// + /// Executes the query and converts the results to . + /// + /// The query. + /// The amount of results to skip. + /// The amount of results to take/return. + /// The total amount of records. + /// + /// The search results. + /// + IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } diff --git a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs index 01aea4c48f..9e96466377 100644 --- a/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs +++ b/src/Umbraco.Infrastructure/IPublishedContentQueryAccessor.cs @@ -1,22 +1,23 @@ using System.Diagnostics.CodeAnalysis; -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core; + +/// +/// Not intended for use in background threads where you should make use of +/// +/// and instead resolve IPublishedContentQuery from a +/// +/// e.g. using +/// +/// +/// // Background thread example +/// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext(); +/// using IServiceScope serviceScope = _serviceProvider.CreateScope(); +/// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>(); +/// +/// +/// +public interface IPublishedContentQueryAccessor { - /// - /// Not intended for use in background threads where you should make use of - /// and instead resolve IPublishedContentQuery from a - /// e.g. using - /// - /// - /// // Background thread example - /// using UmbracoContextReference _ = _umbracoContextFactory.EnsureUmbracoContext(); - /// using IServiceScope serviceScope = _serviceProvider.CreateScope(); - /// IPublishedContentQuery query = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>(); - /// - /// - /// - public interface IPublishedContentQueryAccessor - { - bool TryGetValue([MaybeNullWhen(false)]out IPublishedContentQuery publishedContentQuery); - } + bool TryGetValue([MaybeNullWhen(false)] out IPublishedContentQuery publishedContentQuery); } diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 0324595133..ccb3e4a0da 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -1,11 +1,8 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Security.AccessControl; +using System.Security.Principal; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -14,252 +11,258 @@ using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.IO; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +public class FilePermissionHelper : IFilePermissionHelper { - /// - public class FilePermissionHelper : IFilePermissionHelper + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IIOHelper _ioHelper; + + private readonly string[] _packagesPermissionsDirs; + + // ensure that these directories exist and Umbraco can write to them + private readonly string[] _permissionDirs; + + // ensure Umbraco can write to these files (the directories must exist) + private readonly string[] _permissionFiles = Array.Empty(); + private readonly string _basePath; + + /// + /// Initializes a new instance of the class. + /// + public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, + IHostingEnvironment hostingEnvironment) { - // ensure that these directories exist and Umbraco can write to them - private readonly string[] _permissionDirs; - private readonly string[] _packagesPermissionsDirs; - - // ensure Umbraco can write to these files (the directories must exist) - private readonly string[] _permissionFiles = Array.Empty(); - private readonly GlobalSettings _globalSettings; - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private string _basePath; - - /// - /// Initializes a new instance of the class. - /// - public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) + _globalSettings = globalSettings.Value; + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + _basePath = hostingEnvironment.MapPathContentRoot("/"); + _permissionDirs = new[] { - _globalSettings = globalSettings.Value; - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - _basePath = hostingEnvironment.MapPathContentRoot("/"); - _permissionDirs = new[] - { - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) - }; - _packagesPermissionsDirs = new[] - { - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), - hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages) - }; - } - - /// - public bool RunFilePermissionTestSuite(out Dictionary> report) + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview), + }; + _packagesPermissionsDirs = new[] { - report = new Dictionary>(); + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Umbraco), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages), + }; + } - EnsureDirectories(_permissionDirs, out IEnumerable errors); - report[FilePermissionTest.FolderCreation] = errors.ToList(); + /// + public bool RunFilePermissionTestSuite(out Dictionary> report) + { + report = new Dictionary>(); - EnsureDirectories(_packagesPermissionsDirs, out errors); - report[FilePermissionTest.FileWritingForPackages] = errors.ToList(); + EnsureDirectories(_permissionDirs, out IEnumerable errors); + report[FilePermissionTest.FolderCreation] = errors.ToList(); - EnsureFiles(_permissionFiles, out errors); - report[FilePermissionTest.FileWriting] = errors.ToList(); + EnsureDirectories(_packagesPermissionsDirs, out errors); + report[FilePermissionTest.FileWritingForPackages] = errors.ToList(); - EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors); - report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); + EnsureFiles(_permissionFiles, out errors); + report[FilePermissionTest.FileWriting] = errors.ToList(); - return report.Sum(x => x.Value.Count()) == 0; - } + EnsureCanCreateSubDirectory( + _hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), + out errors); + report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); - private bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) + return report.Sum(x => x.Value.Count()) == 0; + } + + private bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) + { + List? temp = null; + var success = true; + foreach (var dir in dirs) { - List? temp = null; - var success = true; - foreach (var dir in dirs) + // we don't want to create/ship unnecessary directories, so + // here we just ensure we can access the directory, not create it + var tryAccess = TryAccessDirectory(dir, !writeCausesRestart); + if (tryAccess) { - // we don't want to create/ship unnecessary directories, so - // here we just ensure we can access the directory, not create it - var tryAccess = TryAccessDirectory(dir, !writeCausesRestart); - if (tryAccess) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(dir.TrimStart(_basePath)); - success = false; + continue; } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; - } - - private bool EnsureFiles(string[] files, out IEnumerable errors) - { - List? temp = null; - var success = true; - foreach (var file in files) + if (temp == null) { - var canWrite = TryWriteFile(file); - if (canWrite) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(file.TrimStart(_basePath)); - success = false; + temp = new List(); } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; + temp.Add(dir.TrimStart(_basePath)); + success = false; } - private bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) - => EnsureCanCreateSubDirectories(new[] { dir }, out errors); + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } - private bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) + private bool EnsureFiles(string[] files, out IEnumerable errors) + { + List? temp = null; + var success = true; + foreach (var file in files) { - List? temp = null; - var success = true; - foreach (var dir in dirs) + var canWrite = TryWriteFile(file); + if (canWrite) { - var canCreate = TryCreateSubDirectory(dir); - if (canCreate) - { - continue; - } - - if (temp == null) - { - temp = new List(); - } - - temp.Add(dir); - success = false; + continue; } - errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); - return success; + if (temp == null) + { + temp = new List(); + } + + temp.Add(file.TrimStart(_basePath)); + success = false; } - // tries to create a sub-directory - // if successful, the sub-directory is deleted - // creates the directory if needed - does not delete it - private bool TryCreateSubDirectory(string dir) + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } + + private bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) + => EnsureCanCreateSubDirectories(new[] { dir }, out errors); + + private bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) + { + List? temp = null; + var success = true; + foreach (var dir in dirs) { - try + var canCreate = TryCreateSubDirectory(dir); + if (canCreate) + { + continue; + } + + if (temp == null) + { + temp = new List(); + } + + temp.Add(dir); + success = false; + } + + errors = success ? Enumerable.Empty() : temp ?? Enumerable.Empty(); + return success; + } + + // tries to create a sub-directory + // if successful, the sub-directory is deleted + // creates the directory if needed - does not delete it + private bool TryCreateSubDirectory(string dir) + { + try + { + var path = Path.Combine(dir, _ioHelper.CreateRandomFileName()); + Directory.CreateDirectory(path); + Directory.Delete(path); + return true; + } + catch + { + return false; + } + } + + // tries to create a file + // if successful, the file is deleted + // + // or + // + // use the ACL APIs to avoid creating files + // + // if the directory does not exist, do nothing & success + private bool TryAccessDirectory(string dirPath, bool canWrite) + { + try + { + if (Directory.Exists(dirPath) == false) { - var path = Path.Combine(dir, _ioHelper.CreateRandomFileName()); - Directory.CreateDirectory(path); - Directory.Delete(path); return true; } - catch + + if (canWrite) { - return false; - } - } - - // tries to create a file - // if successful, the file is deleted - // - // or - // - // use the ACL APIs to avoid creating files - // - // if the directory does not exist, do nothing & success - private bool TryAccessDirectory(string dirPath, bool canWrite) - { - try - { - if (Directory.Exists(dirPath) == false) - { - return true; - } - - if (canWrite) - { - var filePath = dirPath + "/" + _ioHelper.CreateRandomFileName() + ".tmp"; - File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); - File.Delete(filePath); - return true; - } - - return HasWritePermission(dirPath); - } - catch - { - return false; - } - } - - private bool HasWritePermission(string path) - { - var writeAllow = false; - var writeDeny = false; - var accessControlList = new DirectorySecurity(path, AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); - - AuthorizationRuleCollection accessRules; - try - { - accessRules = accessControlList.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier)); - } - catch (Exception) - { - // This is not 100% accurate because it could turn out that the current user doesn't - // have access to read the current permissions but does have write access. - // I think this is an edge case however - return false; - } - - foreach (FileSystemAccessRule rule in accessRules) - { - if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) - { - continue; - } - - if (rule.AccessControlType == AccessControlType.Allow) - { - writeAllow = true; - } - else if (rule.AccessControlType == AccessControlType.Deny) - { - writeDeny = true; - } - } - - return writeAllow && writeDeny == false; - } - - // tries to write into a file - // fails if the directory does not exist - private bool TryWriteFile(string file) - { - try - { - var path = file; - File.AppendText(path).Close(); + var filePath = dirPath + "/" + _ioHelper.CreateRandomFileName() + ".tmp"; + File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it."); + File.Delete(filePath); return true; } - catch + + return HasWritePermission(dirPath); + } + catch + { + return false; + } + } + + private bool HasWritePermission(string path) + { + var writeAllow = false; + var writeDeny = false; + var accessControlList = new DirectorySecurity( + path, + AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); + + AuthorizationRuleCollection accessRules; + try + { + accessRules = accessControlList.GetAccessRules(true, true, typeof(SecurityIdentifier)); + } + catch (Exception) + { + // This is not 100% accurate because it could turn out that the current user doesn't + // have access to read the current permissions but does have write access. + // I think this is an edge case however + return false; + } + + foreach (FileSystemAccessRule rule in accessRules) + { + if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) { - return false; + continue; } + + if (rule.AccessControlType == AccessControlType.Allow) + { + writeAllow = true; + } + else if (rule.AccessControlType == AccessControlType.Deny) + { + writeDeny = true; + } + } + + return writeAllow && writeDeny == false; + } + + // tries to write into a file + // fails if the directory does not exist + private bool TryWriteFile(string file) + { + try + { + var path = file; + File.AppendText(path).Close(); + return true; + } + catch + { + return false; } } } diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 0b6c5cc32b..41e9d13ed8 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -59,7 +59,7 @@ namespace Umbraco.Cms.Infrastructure.Install // Check for current install ID var installCookie = _cookieManager.GetCookieValue(Constants.Web.InstallerCookieName); - if (!Guid.TryParse(installCookie, out var installId)) + if (!Guid.TryParse(installCookie, out Guid installId)) { installId = Guid.NewGuid(); diff --git a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs index 2a9c303349..7b711f8750 100644 --- a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs +++ b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs @@ -1,61 +1,48 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Install.InstallSteps; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Infrastructure.Install.InstallSteps; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +public sealed class InstallStepCollection { - public sealed class InstallStepCollection + private readonly InstallHelper _installHelper; + private readonly IEnumerable _orderedInstallerSteps; + + public InstallStepCollection(InstallHelper installHelper, IEnumerable installerSteps) { - private readonly InstallHelper _installHelper; - private readonly IEnumerable _orderedInstallerSteps; + _installHelper = installHelper; - public InstallStepCollection(InstallHelper installHelper, IEnumerable installerSteps) + // TODO: this is ugly but I have a branch where it's nicely refactored - for now we just want to manage ordering + InstallSetupStep[] a = installerSteps.ToArray(); + _orderedInstallerSteps = new InstallSetupStep[] { - _installHelper = installHelper; + a.OfType().First(), a.OfType().First(), + a.OfType().First(), a.OfType().First(), + a.OfType().First(), a.OfType().First(), + a.OfType().First(), - // TODO: this is ugly but I have a branch where it's nicely refactored - for now we just want to manage ordering - var a = installerSteps.ToArray(); - _orderedInstallerSteps = new InstallSetupStep[] - { - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - a.OfType().First(), - - // TODO: Add these back once we have a compatible Starter kit - // a.OfType().First(), - // a.OfType().First(), - // a.OfType().First(), - - a.OfType().First(), - }; - } - - - /// - /// Get the installer steps - /// - /// - /// - /// The step order returned here is how they will appear on the front-end if they have views assigned - /// - public IEnumerable GetAllSteps() - { - return _orderedInstallerSteps; - } - - /// - /// Returns the steps that are used only for the current installation type - /// - /// - public IEnumerable GetStepsForCurrentInstallType() - { - return GetAllSteps().Where(x => x.InstallTypeTarget.HasFlag(_installHelper.GetInstallationType())); - } + // TODO: Add these back once we have a compatible Starter kit + // a.OfType().First(), + // a.OfType().First(), + // a.OfType().First(), + a.OfType().First(), + }; } + + /// + /// Get the installer steps + /// + /// + /// + /// The step order returned here is how they will appear on the front-end if they have views assigned + /// + public IEnumerable GetAllSteps() => _orderedInstallerSteps; + + /// + /// Returns the steps that are used only for the current installation type + /// + /// + public IEnumerable GetStepsForCurrentInstallType() => GetAllSteps() + .Where(x => x.InstallTypeTarget.HasFlag(_installHelper.GetInstallationType())); } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs index 0666a3eee5..d212909a9f 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/CompleteInstallStep.cs @@ -1,31 +1,26 @@ -using System.Threading.Tasks; using Umbraco.Cms.Core.Install.Models; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep( + InstallationType.NewInstall | InstallationType.Upgrade, + "UmbracoVersion", + 50, + "Installation is complete! Get ready to be redirected to your new CMS.", + PerformsAppRestart = true)] +public class CompleteInstallStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.", - PerformsAppRestart = true)] - public class CompleteInstallStep : InstallSetupStep + private readonly InstallHelper _installHelper; + + public CompleteInstallStep(InstallHelper installHelper) => _installHelper = installHelper; + + public override async Task ExecuteAsync(object model) { - private readonly InstallHelper _installHelper; + // reports the ended install + await _installHelper.SetInstallStatusAsync(true, string.Empty); - public CompleteInstallStep(InstallHelper installHelper) - { - _installHelper = installHelper; - } - - public override async Task ExecuteAsync(object model) - { - //reports the ended install - await _installHelper.SetInstallStatusAsync(true, ""); - - return null; - } - - public override bool RequiresExecution(object model) - { - return true; - } + return null; } + + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs index 2a9dacfe5e..87be3c6e8f 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseConfigureStep.cs @@ -7,69 +7,65 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)] +public class DatabaseConfigureStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall, "DatabaseConfigure", "database", 10, "Setting up a database, so Umbraco has a place to store your website", PerformsAppRestart = true)] - public class DatabaseConfigureStep : InstallSetupStep + private readonly IOptionsMonitor _connectionStrings; + private readonly DatabaseBuilder _databaseBuilder; + private readonly IEnumerable _databaseProviderMetadata; + private readonly ILogger _logger; + + public DatabaseConfigureStep( + DatabaseBuilder databaseBuilder, + IOptionsMonitor connectionStrings, + ILogger logger, + IEnumerable databaseProviderMetadata) { - private readonly DatabaseBuilder _databaseBuilder; - private readonly ILogger _logger; - private readonly IEnumerable _databaseProviderMetadata; - private readonly IOptionsMonitor _connectionStrings; + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _logger = logger; + _databaseProviderMetadata = databaseProviderMetadata; + } - public DatabaseConfigureStep( - DatabaseBuilder databaseBuilder, - IOptionsMonitor connectionStrings, - ILogger logger, - IEnumerable databaseProviderMetadata) + public override object ViewModel => new {databases = _databaseProviderMetadata.GetAvailable().ToList()}; + + public override string View => ShouldDisplayView() ? base.View : string.Empty; + + public override Task ExecuteAsync(DatabaseModel databaseSettings) + { + if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, false)) { - _databaseBuilder = databaseBuilder; - _connectionStrings = connectionStrings; - _logger = logger; - _databaseProviderMetadata = databaseProviderMetadata; + throw new InstallException("Could not connect to the database"); } - public override Task ExecuteAsync(DatabaseModel databaseSettings) + return Task.FromResult(null); + } + + public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); + + private bool ShouldDisplayView() + { + // If the connection string is already present in config we don't need to show the settings page and we jump to installing/upgrading. + if (_connectionStrings.CurrentValue.IsConnectionStringConfigured()) { - if (!_databaseBuilder.ConfigureDatabaseConnection(databaseSettings, isTrialRun: false)) + try { - throw new InstallException("Could not connect to the database"); + // Since a connection string was present we verify the db can connect and query + _databaseBuilder.ValidateSchema(); + + return false; } - - return Task.FromResult(null); - } - - public override object ViewModel => new - { - databases = _databaseProviderMetadata.GetAvailable().ToList() - }; - - public override string View => ShouldDisplayView() ? base.View : string.Empty; - - public override bool RequiresExecution(DatabaseModel model) => ShouldDisplayView(); - - private bool ShouldDisplayView() - { - // If the connection string is already present in config we don't need to show the settings page and we jump to installing/upgrading. - if (_connectionStrings.CurrentValue.IsConnectionStringConfigured()) + catch (Exception ex) { - try - { - // Since a connection string was present we verify the db can connect and query - _databaseBuilder.ValidateSchema(); + // Something went wrong, could not connect so probably need to reconfigure + _logger.LogError(ex, "An error occurred, reconfiguring..."); - return false; - } - catch (Exception ex) - { - // Something went wrong, could not connect so probably need to reconfigure - _logger.LogError(ex, "An error occurred, reconfiguring..."); - - return true; - } + return true; } - - return true; } + + return true; } } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs index 21da2f797a..42712f20bd 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseInstallStep.cs @@ -1,55 +1,50 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Install.InstallSteps +namespace Umbraco.Cms.Infrastructure.Install.InstallSteps; + +[InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] +public class DatabaseInstallStep : InstallSetupStep { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "DatabaseInstall", 11, "")] - public class DatabaseInstallStep : InstallSetupStep + private readonly DatabaseBuilder _databaseBuilder; + private readonly IRuntimeState _runtime; + + public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) { - private readonly IRuntimeState _runtime; - private readonly DatabaseBuilder _databaseBuilder; - - public DatabaseInstallStep(IRuntimeState runtime, DatabaseBuilder databaseBuilder) - { - _runtime = runtime; - _databaseBuilder = databaseBuilder; - } - - public override Task ExecuteAsync(object model) - { - if (_runtime.Level == RuntimeLevel.Run) - throw new Exception("Umbraco is already configured!"); - - if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) - { - _databaseBuilder.CreateDatabase(); - } - - var result = _databaseBuilder.CreateSchemaAndData(); - - if (result?.Success == false) - { - throw new InstallException("The database failed to install. ERROR: " + result.Message); - } - - if (result?.RequiresUpgrade == false) - { - return Task.FromResult(null); - } - - // Upgrade is required, so set the flag for the next step - return Task.FromResult(new InstallSetupResult(new Dictionary - { - { "upgrade", true} - }))!; - } - - public override bool RequiresExecution(object model) => true; + _runtime = runtime; + _databaseBuilder = databaseBuilder; } + + public override Task ExecuteAsync(object model) + { + if (_runtime.Level == RuntimeLevel.Run) + { + throw new Exception("Umbraco is already configured!"); + } + + if (_runtime.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _databaseBuilder.CreateDatabase(); + } + + DatabaseBuilder.Result? result = _databaseBuilder.CreateSchemaAndData(); + + if (result?.Success == false) + { + throw new InstallException("The database failed to install. ERROR: " + result.Message); + } + + if (result?.RequiresUpgrade == false) + { + return Task.FromResult(null); + } + + // Upgrade is required, so set the flag for the next step + return Task.FromResult(new InstallSetupResult(new Dictionary { { "upgrade", true } }))!; + } + + public override bool RequiresExecution(object model) => true; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs index 1322aaa3b8..fa35ee5b07 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/DatabaseUpgradeStep.cs @@ -38,8 +38,8 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps public override Task ExecuteAsync(object model) { - var installSteps = InstallStatusTracker.GetStatus().ToArray(); - var previousStep = installSteps.Single(x => x.Name == "DatabaseInstall"); + InstallTrackingItem[] installSteps = InstallStatusTracker.GetStatus().ToArray(); + InstallTrackingItem previousStep = installSteps.Single(x => x.Name == "DatabaseInstall"); var upgrade = previousStep.AdditionalData.ContainsKey("upgrade"); if (upgrade) @@ -49,7 +49,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps var plan = new UmbracoPlan(_umbracoVersion); plan.AddPostMigration(); // needed when running installer (back-office) - var result = _databaseBuilder.UpgradeSchemaAndData(plan); + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); if (result?.Success == false) { @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps } // This step relies on the previous one completed - because it has stored some information we need - var installSteps = InstallStatusTracker.GetStatus().ToArray(); + InstallTrackingItem[] installSteps = InstallStatusTracker.GetStatus().ToArray(); if (installSteps.Any(x => x.Name == "DatabaseInstall" && x.AdditionalData.ContainsKey("upgrade")) == false) { return false; @@ -79,7 +79,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { // A connection string was present, determine whether this is an install/upgrade // Return true (upgrade) if there is an installed version, else false (install) - var result = _databaseBuilder.ValidateSchema(); + DatabaseSchemaResult? result = _databaseBuilder.ValidateSchema(); return result?.DetermineHasInstalledVersion() ?? false; } diff --git a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs index 55b56febe6..2ebc756dc2 100644 --- a/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs @@ -1,11 +1,14 @@ using System.Collections.Specialized; +using System.Data.Common; using System.Text; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -14,6 +17,7 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; +using HttpResponseMessage = System.Net.Http.HttpResponseMessage; namespace Umbraco.Cms.Infrastructure.Install.InstallSteps { @@ -100,18 +104,18 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps public override async Task ExecuteAsync(UserModel user) { - var admin = _userService.GetUserById(Constants.Security.SuperUserId); + IUser? admin = _userService.GetUserById(Constants.Security.SuperUserId); if (admin == null) { throw new InvalidOperationException("Could not find the super user!"); } admin.Email = user.Email.Trim(); - admin.Name = user.Name!.Trim(); + admin.Name = user.Name.Trim(); admin.Username = user.Email.Trim(); _userService.Save(admin); - var membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); + BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); if (membershipUser == null) { throw new InvalidOperationException( @@ -121,11 +125,15 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps //To change the password here we actually need to reset it since we don't have an old one to use to change var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); if (string.IsNullOrWhiteSpace(resetToken)) + { throw new InvalidOperationException("Could not reset password: unable to generate internal reset token"); + } - var resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); if (!resetResult.Succeeded) + { throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); + } _metricsConsentService.SetConsentLevel(user.TelemetryLevel); @@ -138,7 +146,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps try { - var response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; + HttpResponseMessage response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result; } catch { /* fail in silence */ } } @@ -192,14 +200,14 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps private InstallState GetInstallState() { - var installState = InstallState.Unknown; + InstallState installState = InstallState.Unknown; if (_databaseBuilder.IsDatabaseConfigured) { installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; } - var umbracoConnectionString = _connectionStrings.CurrentValue; + ConnectionStrings? umbracoConnectionString = _connectionStrings.CurrentValue; var isConnectionStringConfigured = umbracoConnectionString.IsConnectionStringConfigured(); if (isConnectionStringConfigured) @@ -207,7 +215,7 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; } - var factory = _dbProviderFactoryCreator.CreateFactory(umbracoConnectionString.ProviderName); + DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(umbracoConnectionString.ProviderName); var isConnectionAvailable = isConnectionStringConfigured && DbConnectionExtensions.IsConnectionAvailable(umbracoConnectionString.ConnectionString, factory); if (isConnectionAvailable) { @@ -231,14 +239,14 @@ namespace Umbraco.Cms.Infrastructure.Install.InstallSteps private bool ShowView() { - var installState = GetInstallState(); + InstallState installState = GetInstallState(); return installState.HasFlag(InstallState.Unknown) || !installState.HasFlag(InstallState.UmbracoInstalled); } public override bool RequiresExecution(UserModel model) { - var installState = GetInstallState(); + InstallState installState = GetInstallState(); if (installState.HasFlag(InstallState.Unknown)) { // In this one case when it's a brand new install and nothing has been configured, make sure the diff --git a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs index 100e3ee26f..db054b2dc5 100644 --- a/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs +++ b/src/Umbraco.Infrastructure/Install/PackageMigrationRunner.cs @@ -1,108 +1,106 @@ -using System; -using System.Linq; -using System.Collections.Generic; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Packaging; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Migrations.Upgrade; -using Umbraco.Extensions; using Umbraco.Cms.Core.Migrations; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Notifications; -using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade; +using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +/// Runs the package migration plans +/// +public class PackageMigrationRunner { - /// - /// Runs the package migration plans - /// - public class PackageMigrationRunner + private readonly IEventAggregator _eventAggregator; + private readonly IKeyValueService _keyValueService; + private readonly IMigrationPlanExecutor _migrationPlanExecutor; + private readonly Dictionary _packageMigrationPlans; + private readonly PendingPackageMigrations _pendingPackageMigrations; + private readonly IProfilingLogger _profilingLogger; + private readonly ICoreScopeProvider _scopeProvider; + + public PackageMigrationRunner( + IProfilingLogger profilingLogger, + ICoreScopeProvider scopeProvider, + PendingPackageMigrations pendingPackageMigrations, + PackageMigrationPlanCollection packageMigrationPlans, + IMigrationPlanExecutor migrationPlanExecutor, + IKeyValueService keyValueService, + IEventAggregator eventAggregator) { - private readonly IProfilingLogger _profilingLogger; - private readonly ICoreScopeProvider _scopeProvider; - private readonly PendingPackageMigrations _pendingPackageMigrations; - private readonly IMigrationPlanExecutor _migrationPlanExecutor; - private readonly IKeyValueService _keyValueService; - private readonly IEventAggregator _eventAggregator; - private readonly Dictionary _packageMigrationPlans; + _profilingLogger = profilingLogger; + _scopeProvider = scopeProvider; + _pendingPackageMigrations = pendingPackageMigrations; + _migrationPlanExecutor = migrationPlanExecutor; + _keyValueService = keyValueService; + _eventAggregator = eventAggregator; + _packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name); + } - public PackageMigrationRunner( - IProfilingLogger profilingLogger, - ICoreScopeProvider scopeProvider, - PendingPackageMigrations pendingPackageMigrations, - PackageMigrationPlanCollection packageMigrationPlans, - IMigrationPlanExecutor migrationPlanExecutor, - IKeyValueService keyValueService, - IEventAggregator eventAggregator) + /// + /// Runs all migration plans for a package name if any are pending. + /// + /// + /// + public IEnumerable RunPackageMigrationsIfPending(string packageName) + { + IReadOnlyDictionary? keyValues = + _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); + IReadOnlyList pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues); + + IEnumerable packagePlans = _packageMigrationPlans.Values + .Where(x => x.PackageName.InvariantEquals(packageName)) + .Where(x => pendingMigrations.Contains(x.Name)) + .Select(x => x.Name); + + return RunPackagePlans(packagePlans); + } + + /// + /// Runs the all specified package migration plans and publishes a + /// if all are successful. + /// + /// + /// + /// If any plan fails it will throw an exception. + public IEnumerable RunPackagePlans(IEnumerable plansToRun) + { + var results = new List(); + + // Create an explicit scope around all package migrations so they are + // all executed in a single transaction. If one package migration fails, + // none of them will be committed. This is intended behavior so we can + // ensure when we publish the success notification that is is done when they all succeed. + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - _profilingLogger = profilingLogger; - _scopeProvider = scopeProvider; - _pendingPackageMigrations = pendingPackageMigrations; - _migrationPlanExecutor = migrationPlanExecutor; - _keyValueService = keyValueService; - _eventAggregator = eventAggregator; - _packageMigrationPlans = packageMigrationPlans.ToDictionary(x => x.Name); - } - - /// - /// Runs all migration plans for a package name if any are pending. - /// - /// - /// - public IEnumerable RunPackageMigrationsIfPending(string packageName) - { - IReadOnlyDictionary? keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix); - IReadOnlyList pendingMigrations = _pendingPackageMigrations.GetPendingPackageMigrations(keyValues); - - IEnumerable packagePlans = _packageMigrationPlans.Values - .Where(x => x.PackageName.InvariantEquals(packageName)) - .Where(x => pendingMigrations.Contains(x.Name)) - .Select(x => x.Name); - - return RunPackagePlans(packagePlans); - } - - /// - /// Runs the all specified package migration plans and publishes a - /// if all are successful. - /// - /// - /// - /// If any plan fails it will throw an exception. - public IEnumerable RunPackagePlans(IEnumerable plansToRun) - { - var results = new List(); - - // Create an explicit scope around all package migrations so they are - // all executed in a single transaction. If one package migration fails, - // none of them will be committed. This is intended behavior so we can - // ensure when we publish the success notification that is is done when they all succeed. - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + foreach (var migrationName in plansToRun) { - foreach (var migrationName in plansToRun) + if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan)) { - if (!_packageMigrationPlans.TryGetValue(migrationName, out PackageMigrationPlan? plan)) - { - throw new InvalidOperationException("Cannot find package migration plan " + migrationName); - } + throw new InvalidOperationException("Cannot find package migration plan " + migrationName); + } - using (_profilingLogger.TraceDuration( - "Starting unattended package migration for " + migrationName, - "Unattended upgrade completed for " + migrationName)) - { - var upgrader = new Upgrader(plan); - // This may throw, if so the transaction will be rolled back - results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); - } + using (_profilingLogger.TraceDuration( + "Starting unattended package migration for " + migrationName, + "Unattended upgrade completed for " + migrationName)) + { + var upgrader = new Upgrader(plan); + + // This may throw, if so the transaction will be rolled back + results.Add(upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService)); } } - - var executedPlansNotification = new MigrationPlansExecutedNotification(results); - _eventAggregator.Publish(executedPlansNotification); - - return results; } + + var executedPlansNotification = new MigrationPlansExecutedNotification(results); + _eventAggregator.Publish(executedPlansNotification); + + return results; } } diff --git a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs index bf38c2b664..bf9817ca94 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedInstaller.cs @@ -1,6 +1,3 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,129 +9,131 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +public class UnattendedInstaller : INotificationAsyncHandler { - public class UnattendedInstaller : INotificationAsyncHandler + private readonly IUmbracoDatabaseFactory _databaseFactory; + + private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly IRuntimeState _runtimeState; + private readonly IOptions _unattendedSettings; + + public UnattendedInstaller( + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEventAggregator eventAggregator, + IOptions unattendedSettings, + IUmbracoDatabaseFactory databaseFactory, + IDbProviderFactoryCreator dbProviderFactoryCreator, + ILogger logger, + IRuntimeState runtimeState) { - private readonly IOptions _unattendedSettings; + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? + throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _unattendedSettings = unattendedSettings; + _databaseFactory = databaseFactory; + _dbProviderFactoryCreator = dbProviderFactoryCreator; + _logger = logger; + _runtimeState = runtimeState; + } - private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly IEventAggregator _eventAggregator; - private readonly IUmbracoDatabaseFactory _databaseFactory; - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly ILogger _logger; - private readonly IRuntimeState _runtimeState; - - public UnattendedInstaller( - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEventAggregator eventAggregator, - IOptions unattendedSettings, - IUmbracoDatabaseFactory databaseFactory, - IDbProviderFactoryCreator dbProviderFactoryCreator, - ILogger logger, - IRuntimeState runtimeState) + public Task HandleAsync(RuntimeUnattendedInstallNotification notification, CancellationToken cancellationToken) + { + // unattended install is not enabled + if (_unattendedSettings.Value.InstallUnattended == false) { - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory ?? throw new ArgumentNullException(nameof(databaseSchemaCreatorFactory)); - _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); - _unattendedSettings = unattendedSettings; - _databaseFactory = databaseFactory; - _dbProviderFactoryCreator = dbProviderFactoryCreator; - _logger = logger; - _runtimeState = runtimeState; - } - - public Task HandleAsync(RuntimeUnattendedInstallNotification notification, CancellationToken cancellationToken) - { - // unattended install is not enabled - if (_unattendedSettings.Value.InstallUnattended == false) - { - return Task.CompletedTask; - } - - // no connection string set - if (_databaseFactory.Configured == false) - { - return Task.CompletedTask; - } - - _runtimeState.DetermineRuntimeLevel(); - if (_runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) - { - _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, _databaseFactory.ConnectionString!); - } - - bool connect; - try - { - for (var i = 0; ;) - { - connect = _databaseFactory.CanConnect; - if (connect || ++i == 5) - { - break; - } - - _logger.LogDebug("Could not immediately connect to database, trying again."); - - Thread.Sleep(1000); - } - } - catch (Exception ex) - { - _logger.LogInformation(ex, "Error during unattended install."); - - var innerException = new UnattendedInstallException("Unattended installation failed.", ex); - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - return Task.CompletedTask; - } - - // could not connect to the database - if (connect == false) - { - return Task.CompletedTask; - } - - IUmbracoDatabase? database = null; - try - { - using (database = _databaseFactory.CreateDatabase()) - { - var hasUmbracoTables = database?.IsUmbracoInstalled() ?? false; - - // database has umbraco tables, assume Umbraco is already installed - if (hasUmbracoTables) - { - return Task.CompletedTask; - } - - // all conditions fulfilled, do the install - _logger.LogInformation("Starting unattended install."); - - database?.BeginTransaction(); - DatabaseSchemaCreator creator = _databaseSchemaCreatorFactory.Create(database); - creator.InitializeDatabaseSchema(); - database?.CompleteTransaction(); - _logger.LogInformation("Unattended install completed."); - - // Emit an event with EventAggregator that unattended install completed - // Then this event can be listened for and create an unattended user - _eventAggregator.Publish(new UnattendedInstallNotification()); - } - } - catch (Exception ex) - { - _logger.LogInformation(ex, "Error during unattended install."); - database?.AbortTransaction(); - - var innerException = new UnattendedInstallException( - "The database configuration failed." - + "\n Please check log file for additional information (can be found in '/Umbraco/Data/Logs/')", - ex); - - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - } - return Task.CompletedTask; } + + // no connection string set + if (_databaseFactory.Configured == false) + { + return Task.CompletedTask; + } + + _runtimeState.DetermineRuntimeLevel(); + if (_runtimeState.Reason == RuntimeLevelReason.InstallMissingDatabase) + { + _dbProviderFactoryCreator.CreateDatabase( + _databaseFactory.ProviderName!, + _databaseFactory.ConnectionString!); + } + + bool connect; + try + { + for (var i = 0; ;) + { + connect = _databaseFactory.CanConnect; + if (connect || ++i == 5) + { + break; + } + + _logger.LogDebug("Could not immediately connect to database, trying again."); + + Thread.Sleep(1000); + } + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Error during unattended install."); + + var innerException = new UnattendedInstallException("Unattended installation failed.", ex); + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + return Task.CompletedTask; + } + + // could not connect to the database + if (connect == false) + { + return Task.CompletedTask; + } + + IUmbracoDatabase? database = null; + try + { + using (database = _databaseFactory.CreateDatabase()) + { + var hasUmbracoTables = database.IsUmbracoInstalled(); + + // database has umbraco tables, assume Umbraco is already installed + if (hasUmbracoTables) + { + return Task.CompletedTask; + } + + // all conditions fulfilled, do the install + _logger.LogInformation("Starting unattended install."); + + database.BeginTransaction(); + DatabaseSchemaCreator creator = _databaseSchemaCreatorFactory.Create(database); + creator.InitializeDatabaseSchema(); + database.CompleteTransaction(); + _logger.LogInformation("Unattended install completed."); + + // Emit an event with EventAggregator that unattended install completed + // Then this event can be listened for and create an unattended user + _eventAggregator.Publish(new UnattendedInstallNotification()); + } + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Error during unattended install."); + database?.AbortTransaction(); + + var innerException = new UnattendedInstallException( + "The database configuration failed." + + "\n Please check log file for additional information (can be found in '/Umbraco/Data/Logs/')", + ex); + + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + } + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs index 3b891b88c4..fb0b389b47 100644 --- a/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs +++ b/src/Umbraco.Infrastructure/Install/UnattendedUpgrader.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; @@ -12,98 +9,106 @@ using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Extensions; -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Install +namespace Umbraco.Cms.Infrastructure.Install; + +/// +/// Handles to execute the unattended Umbraco upgrader +/// or the unattended Package migrations runner. +/// +public class UnattendedUpgrader : INotificationAsyncHandler { - /// - /// Handles to execute the unattended Umbraco upgrader - /// or the unattended Package migrations runner. - /// - public class UnattendedUpgrader : INotificationAsyncHandler + private readonly DatabaseBuilder _databaseBuilder; + private readonly PackageMigrationRunner _packageMigrationRunner; + private readonly IProfilingLogger _profilingLogger; + private readonly IRuntimeState _runtimeState; + private readonly IUmbracoVersion _umbracoVersion; + + public UnattendedUpgrader( + IProfilingLogger profilingLogger, + IUmbracoVersion umbracoVersion, + DatabaseBuilder databaseBuilder, + IRuntimeState runtimeState, + PackageMigrationRunner packageMigrationRunner) { - private readonly IProfilingLogger _profilingLogger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly DatabaseBuilder _databaseBuilder; - private readonly IRuntimeState _runtimeState; - private readonly PackageMigrationRunner _packageMigrationRunner; - - public UnattendedUpgrader( - IProfilingLogger profilingLogger, - IUmbracoVersion umbracoVersion, - DatabaseBuilder databaseBuilder, - IRuntimeState runtimeState, - PackageMigrationRunner packageMigrationRunner) - { - _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); - _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); - _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); - _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); - _packageMigrationRunner = packageMigrationRunner; - } - - public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken) - { - if (_runtimeState.RunUnattendedBootLogic()) - { - switch (_runtimeState.Reason) - { - case RuntimeLevelReason.UpgradeMigrations: - { - var plan = new UmbracoPlan(_umbracoVersion); - using (_profilingLogger.TraceDuration( - "Starting unattended upgrade.", - "Unattended upgrade completed.")) - { - DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); - if (result?.Success == false) - { - var innerException = new UnattendedInstallException("An error occurred while running the unattended upgrade.\n" + result.Message); - _runtimeState.Configure(Core.RuntimeLevel.BootFailed, Core.RuntimeLevelReason.BootFailedOnException, innerException); - } - - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; - } - } - break; - case RuntimeLevelReason.UpgradePackageMigrations: - { - if (!_runtimeState.StartupState.TryGetValue(RuntimeState.PendingPackageMigrationsStateKey, out var pm) - || pm is not IReadOnlyList pendingMigrations) - { - throw new InvalidOperationException($"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state"); - } - - if (pendingMigrations.Count == 0) - { - throw new InvalidOperationException("No pending migrations found but the runtime level reason is " + Core.RuntimeLevelReason.UpgradePackageMigrations); - } - - try - { - IEnumerable result = _packageMigrationRunner.RunPackagePlans(pendingMigrations); - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete; - } - catch (Exception ex ) - { - SetRuntimeError(ex); - notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; - } - } - break; - default: - throw new InvalidOperationException("Invalid reason " + _runtimeState.Reason); - } - } - - return Task.CompletedTask; - } - - private void SetRuntimeError(Exception exception) - => _runtimeState.Configure( - RuntimeLevel.BootFailed, - RuntimeLevelReason.BootFailedOnException, - exception); + _profilingLogger = profilingLogger ?? throw new ArgumentNullException(nameof(profilingLogger)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); + _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); + _packageMigrationRunner = packageMigrationRunner; } + + public Task HandleAsync(RuntimeUnattendedUpgradeNotification notification, CancellationToken cancellationToken) + { + if (_runtimeState.RunUnattendedBootLogic()) + { + switch (_runtimeState.Reason) + { + case RuntimeLevelReason.UpgradeMigrations: + { + var plan = new UmbracoPlan(_umbracoVersion); + using (_profilingLogger.TraceDuration( + "Starting unattended upgrade.", + "Unattended upgrade completed.")) + { + DatabaseBuilder.Result? result = _databaseBuilder.UpgradeSchemaAndData(plan); + if (result?.Success == false) + { + var innerException = new UnattendedInstallException( + "An error occurred while running the unattended upgrade.\n" + result.Message); + _runtimeState.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException, innerException); + } + + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete; + } + } + + break; + case RuntimeLevelReason.UpgradePackageMigrations: + { + if (!_runtimeState.StartupState.TryGetValue( + RuntimeState.PendingPackageMigrationsStateKey, + out var pm) + || pm is not IReadOnlyList pendingMigrations) + { + throw new InvalidOperationException( + $"The required key {RuntimeState.PendingPackageMigrationsStateKey} does not exist in startup state"); + } + + if (pendingMigrations.Count == 0) + { + throw new InvalidOperationException( + "No pending migrations found but the runtime level reason is " + + RuntimeLevelReason.UpgradePackageMigrations); + } + + try + { + _packageMigrationRunner.RunPackagePlans(pendingMigrations); + notification.UnattendedUpgradeResult = RuntimeUnattendedUpgradeNotification.UpgradeResult + .PackageMigrationComplete; + } + catch (Exception ex) + { + SetRuntimeError(ex); + notification.UnattendedUpgradeResult = + RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors; + } + } + + break; + default: + throw new InvalidOperationException("Invalid reason " + _runtimeState.Reason); + } + } + + return Task.CompletedTask; + } + + private void SetRuntimeError(Exception exception) + => _runtimeState.Configure( + RuntimeLevel.BootFailed, + RuntimeLevelReason.BootFailedOnException, + exception); } diff --git a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs index 305844d7d6..f58d9d8854 100644 --- a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs +++ b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs @@ -1,52 +1,53 @@ -using System; -using System.IO; -using System.Linq; using Serilog; using Serilog.Events; using Serilog.Parsing; -using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Core.Logging +namespace Umbraco.Cms.Core.Logging; + +public class MessageTemplates : IMessageTemplates { - public class MessageTemplates : IMessageTemplates + // Umbraco now uses Message Templates (https://messagetemplates.org/) for logging, which means + // we cannot plainly use string.Format() to format them. There is a work-in-progress C# lib, + // derived from Serilog, which should help (https://github.com/messagetemplates/messagetemplates-csharp) + // but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which + // means we cannot get rid of Serilog entirely. We may want to revisit this at some point. + + // TODO: Do we still need this, is there a non-pre release package shipped? + private static readonly Lazy _minimalLogger = new(() => new LoggerConfiguration().CreateLogger()); + + public string Render(string messageTemplate, params object[] args) { - // Umbraco now uses Message Templates (https://messagetemplates.org/) for logging, which means - // we cannot plainly use string.Format() to format them. There is a work-in-progress C# lib, - // derived from Serilog, which should help (https://github.com/messagetemplates/messagetemplates-csharp) - // but it only has a pre-release NuGet package. So, we've got to use Serilog's code, which - // means we cannot get rid of Serilog entirely. We may want to revisit this at some point. + // resolve a minimal logger instance which is used to bind message templates + ILogger logger = _minimalLogger.Value; - // TODO: Do we still need this, is there a non-pre release package shipped? + var bound = logger.BindMessageTemplate(messageTemplate, args, out MessageTemplate? parsedTemplate, out IEnumerable? boundProperties); - private static readonly Lazy MinimalLogger = new Lazy(() => new LoggerConfiguration().CreateLogger()); - - public string Render(string messageTemplate, params object[] args) + if (!bound) { - // resolve a minimal logger instance which is used to bind message templates - var logger = MinimalLogger.Value; - - var bound = logger.BindMessageTemplate(messageTemplate, args, out var parsedTemplate, out var boundProperties); - - if (!bound) - throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); - - var values = boundProperties.ToDictionary(x => x.Name, x => x.Value); - - // this ends up putting every string parameter between quotes - //return parsedTemplate.Render(values); - - // this does not - var tw = new StringWriter(); - foreach (var t in parsedTemplate.Tokens) - { - if (t is PropertyToken pt && - values.TryGetValue(pt.PropertyName, out var propVal) && - (propVal as ScalarValue)?.Value is string s) - tw.Write(s); - else - t.Render(values, tw); - } - return tw.ToString(); + throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); } + + var values = boundProperties.ToDictionary(x => x.Name, x => x.Value); + + // this ends up putting every string parameter between quotes + // return parsedTemplate.Render(values); + + // this does not + var tw = new StringWriter(); + foreach (MessageTemplateToken? t in parsedTemplate.Tokens) + { + if (t is PropertyToken pt && + values.TryGetValue(pt.PropertyName, out LogEventPropertyValue? propVal) && + (propVal as ScalarValue)?.Value is string s) + { + tw.Write(s); + } + else + { + t.Render(values, tw); + } + } + + return tw.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs index 3d9182cb96..5abadf4489 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs @@ -1,45 +1,46 @@ -using System; using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Cache; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with a HttpRequestId GUID. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpRequestIdEnricher : ILogEventEnricher { /// - /// Enrich log events with a HttpRequestId GUID. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpRequestIdEnricher : ILogEventEnricher + public const string HttpRequestIdPropertyName = "HttpRequestId"; + + private readonly IRequestCache _requestCache; + + public HttpRequestIdEnricher(IRequestCache requestCache) => + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + + /// + /// Enrich the log event with an id assigned to the currently-executing HTTP request, if any. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - private readonly IRequestCache _requestCache; - - public HttpRequestIdEnricher(IRequestCache requestCache) + if (logEvent == null) { - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + throw new ArgumentNullException(nameof(logEvent)); } - /// - /// The property name added to enriched log events. - /// - public const string HttpRequestIdPropertyName = "HttpRequestId"; - - /// - /// Enrich the log event with an id assigned to the currently-executing HTTP request, if any. - /// - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + if (!LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? requestId, _requestCache)) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - Guid? requestId; - if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId, _requestCache)) - return; - - var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId)); - logEvent.AddPropertyIfAbsent(requestIdProperty); + return; } + + var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId)); + logEvent.AddPropertyIfAbsent(requestIdProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs index ee041f7abb..6f22d3e3b1 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs @@ -1,48 +1,48 @@ -using System; -using System.Threading; using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Cache; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with a HttpRequestNumber unique within the current +/// logging session. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpRequestNumberEnricher : ILogEventEnricher { /// - /// Enrich log events with a HttpRequestNumber unique within the current - /// logging session. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpRequestNumberEnricher : ILogEventEnricher + private const string HttpRequestNumberPropertyName = "HttpRequestNumber"; + private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber"; + + private static int _lastRequestNumber; + private readonly IRequestCache _requestCache; + + public HttpRequestNumberEnricher(IRequestCache requestCache) => + _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + + /// + /// Enrich the log event with the number assigned to the currently-executing HTTP request, if any. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - private readonly IRequestCache _requestCache; - private static int _lastRequestNumber; - private static readonly string _requestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber"; - - /// - /// The property name added to enriched log events. - /// - private const string _httpRequestNumberPropertyName = "HttpRequestNumber"; - - - public HttpRequestNumberEnricher(IRequestCache requestCache) + if (logEvent == null) { - _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); + throw new ArgumentNullException(nameof(logEvent)); } - /// - /// Enrich the log event with the number assigned to the currently-executing HTTP request, if any. - /// - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); + var requestNumber = _requestCache.Get( + _requestNumberItemName, + () => Interlocked.Increment(ref _lastRequestNumber)); - var requestNumber = _requestCache.Get(_requestNumberItemName, - () => Interlocked.Increment(ref _lastRequestNumber)); - - var requestNumberProperty = new LogEventProperty(_httpRequestNumberPropertyName, new ScalarValue(requestNumber)); - logEvent.AddPropertyIfAbsent(requestNumberProperty); - } + var requestNumberProperty = + new LogEventProperty(HttpRequestNumberPropertyName, new ScalarValue(requestNumber)); + logEvent.AddPropertyIfAbsent(requestNumberProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs index e2b4f59065..49c9506237 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs @@ -1,43 +1,45 @@ -using System; using Serilog.Core; using Serilog.Events; using Umbraco.Cms.Core.Net; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enrich log events with the HttpSessionId property. +/// Original source - +/// https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs +/// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want +/// +public class HttpSessionIdEnricher : ILogEventEnricher { /// - /// Enrich log events with the HttpSessionId property. - /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs - /// Nupkg: 'Serilog.Web.Classic' contains handlers and extra bits we do not want + /// The property name added to enriched log events. /// - public class HttpSessionIdEnricher : ILogEventEnricher + public const string HttpSessionIdPropertyName = "HttpSessionId"; + + private readonly ISessionIdResolver _sessionIdResolver; + + public HttpSessionIdEnricher(ISessionIdResolver sessionIdResolver) => _sessionIdResolver = sessionIdResolver; + + /// + /// Enrich the log event with the current ASP.NET session id, if sessions are enabled. + /// + /// The log event to enrich. + /// Factory for creating new properties to add to the event. + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - private readonly ISessionIdResolver _sessionIdResolver; - - public HttpSessionIdEnricher(ISessionIdResolver sessionIdResolver) + if (logEvent == null) { - _sessionIdResolver = sessionIdResolver; + throw new ArgumentNullException(nameof(logEvent)); } - /// - /// The property name added to enriched log events. - /// - public const string HttpSessionIdPropertyName = "HttpSessionId"; - - /// - /// Enrich the log event with the current ASP.NET session id, if sessions are enabled. - /// The log event to enrich. - /// Factory for creating new properties to add to the event. - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + var sessionId = _sessionIdResolver.SessionId; + if (sessionId is null) { - if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); - - var sessionId = _sessionIdResolver.SessionId; - if (sessionId is null) - return; - - var sessionIdProperty = new LogEventProperty(HttpSessionIdPropertyName, new ScalarValue(sessionId)); - logEvent.AddPropertyIfAbsent(sessionIdProperty); + return; } + + var sessionIdProperty = new LogEventProperty(HttpSessionIdPropertyName, new ScalarValue(sessionId)); + logEvent.AddPropertyIfAbsent(sessionIdProperty); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs index a9ae85e2f0..a8a0610d2c 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs @@ -1,49 +1,51 @@ -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// This is used to create a new property in Logs called 'Log4NetLevel' +/// So that we can map Serilog levels to Log4Net levels - so log files stay consistent +/// +internal class Log4NetLevelMapperEnricher : ILogEventEnricher { - /// - /// This is used to create a new property in Logs called 'Log4NetLevel' - /// So that we can map Serilog levels to Log4Net levels - so log files stay consistent - /// - internal class Log4NetLevelMapperEnricher : ILogEventEnricher + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + string log4NetLevel; + + switch (logEvent.Level) { - string log4NetLevel; + case LogEventLevel.Debug: + log4NetLevel = "DEBUG"; + break; - switch (logEvent.Level) - { - case LogEventLevel.Debug: - log4NetLevel = "DEBUG"; - break; + case LogEventLevel.Error: + log4NetLevel = "ERROR"; + break; - case LogEventLevel.Error: - log4NetLevel = "ERROR"; - break; + case LogEventLevel.Fatal: + log4NetLevel = "FATAL"; + break; - case LogEventLevel.Fatal: - log4NetLevel = "FATAL"; - break; + case LogEventLevel.Information: + log4NetLevel = + "INFO "; // Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; - case LogEventLevel.Information: - log4NetLevel = "INFO "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; + case LogEventLevel.Verbose: + log4NetLevel = + "ALL "; // Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; - case LogEventLevel.Verbose: - log4NetLevel = "ALL "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; - - case LogEventLevel.Warning: - log4NetLevel = "WARN "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) - break; - default: - log4NetLevel = string.Empty; - break; - } - - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Log4NetLevel", log4NetLevel)); + case LogEventLevel.Warning: + log4NetLevel = + "WARN "; // Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely) + break; + default: + log4NetLevel = string.Empty; + break; } + + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Log4NetLevel", log4NetLevel)); } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs index bfe7c4172b..45495de9e8 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/ThreadAbortExceptionEnricher.cs @@ -1,100 +1,117 @@ -using System; using System.Reflection; -using System.Threading; using Microsoft.Extensions.Options; using Serilog.Core; using Serilog.Events; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Diagnostics; using Umbraco.Cms.Core.Hosting; -using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings; -namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers +namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers; + +/// +/// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can +/// +public class ThreadAbortExceptionEnricher : ILogEventEnricher { - /// - /// Enriches the log if there are ThreadAbort exceptions and will automatically create a minidump if it can - /// - public class ThreadAbortExceptionEnricher : ILogEventEnricher + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IMarchal _marchal; + private CoreDebugSettings _coreDebugSettings; + + public ThreadAbortExceptionEnricher( + IOptionsMonitor coreDebugSettings, + IHostingEnvironment hostingEnvironment, IMarchal marchal) { - private CoreDebugSettings _coreDebugSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IMarchal _marchal; + _coreDebugSettings = coreDebugSettings.CurrentValue; + _hostingEnvironment = hostingEnvironment; + _marchal = marchal; + coreDebugSettings.OnChange(x => _coreDebugSettings = x); + } - public ThreadAbortExceptionEnricher(IOptionsMonitor coreDebugSettings, IHostingEnvironment hostingEnvironment, IMarchal marchal) + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + switch (logEvent.Level) { - _coreDebugSettings = coreDebugSettings.CurrentValue; - _hostingEnvironment = hostingEnvironment; - _marchal = marchal; - coreDebugSettings.OnChange(x => _coreDebugSettings = x); + case LogEventLevel.Error: + case LogEventLevel.Fatal: + DumpThreadAborts(logEvent, propertyFactory); + break; + } + } + + private static bool IsTimeoutThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) + { + return false; } - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + if (abort.ExceptionState == null) { - switch (logEvent.Level) - { - case LogEventLevel.Error: - case LogEventLevel.Fatal: - DumpThreadAborts(logEvent, propertyFactory); - break; - } + return false; } - private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + Type stateType = abort.ExceptionState.GetType(); + if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") { - if (!IsTimeoutThreadAbortException(logEvent.Exception)) return; + return false; + } - var message = "The thread has been aborted, because the request has timed out."; + FieldInfo? timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); + if (timeoutField == null) + { + return false; + } - // dump if configured, or if stacktrace contains Monitor.ReliableEnter - var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || IsMonitorEnterThreadAbortException(logEvent.Exception); + return (bool?)timeoutField.GetValue(abort.ExceptionState) ?? false; + } - // dump if it is ok to dump (might have a cap on number of dump...) - dump &= MiniDump.OkToDump(_hostingEnvironment); + private void DumpThreadAborts(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (!IsTimeoutThreadAbortException(logEvent.Exception)) + { + return; + } - if (!dump) + var message = "The thread has been aborted, because the request has timed out."; + + // dump if configured, or if stacktrace contains Monitor.ReliableEnter + var dump = _coreDebugSettings.DumpOnTimeoutThreadAbort || + IsMonitorEnterThreadAbortException(logEvent.Exception); + + // dump if it is ok to dump (might have a cap on number of dump...) + dump &= MiniDump.OkToDump(_hostingEnvironment); + + if (!dump) + { + message += ". No minidump was created."; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); + } + else + { + try { - message += ". No minidump was created."; + var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); + message += dumped + ? ". A minidump was created in App_Data/MiniDump." + : ". Failed to create a minidump."; logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } - else + catch (Exception ex) { - try - { - var dumped = MiniDump.Dump(_marchal, _hostingEnvironment, withException: true); - message += dumped - ? ". A minidump was created in App_Data/MiniDump." - : ". Failed to create a minidump."; - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); - } - catch (Exception ex) - { - message = "Failed to create a minidump. " + ex; - logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); - } + message = "Failed to create a minidump. " + ex; + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ThreadAbortExceptionInfo", message)); } } + } - private static bool IsTimeoutThreadAbortException(Exception exception) + private static bool IsMonitorEnterThreadAbortException(Exception exception) + { + if (!(exception is ThreadAbortException abort)) { - if (!(exception is ThreadAbortException abort)) return false; - if (abort.ExceptionState == null) return false; - - var stateType = abort.ExceptionState.GetType(); - if (stateType.FullName != "System.Web.HttpApplication+CancelModuleException") return false; - - var timeoutField = stateType.GetField("_timeout", BindingFlags.Instance | BindingFlags.NonPublic); - if (timeoutField == null) return false; - - return (bool?)timeoutField.GetValue(abort.ExceptionState) ?? false; + return false; } - private static bool IsMonitorEnterThreadAbortException(Exception exception) - { - if (!(exception is ThreadAbortException abort)) return false; - - var stacktrace = abort.StackTrace; - return stacktrace?.Contains("System.Threading.Monitor.ReliableEnter") ?? false; - } - - + var stacktrace = abort.StackTrace; + return stacktrace?.Contains("System.Threading.Monitor.ReliableEnter") ?? false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs index 733410cf91..d8c6d1ff8f 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/LoggerConfigExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -46,7 +44,7 @@ namespace Umbraco.Extensions IConfiguration configuration, out UmbracoFileConfiguration umbFileConfiguration) { - global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); + Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); //Set this environment variable - so that it can be used in external config file //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> @@ -75,8 +73,7 @@ namespace Umbraco.Extensions rollingInterval: umbracoFileConfiguration.RollingInterval, flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, - retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit - ); + retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit); return logConfig; } @@ -93,7 +90,7 @@ namespace Umbraco.Extensions ILoggingConfiguration loggingConfiguration, UmbracoFileConfiguration umbracoFileConfiguration) { - global::Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); + Serilog.Debugging.SelfLog.Enable(msg => System.Diagnostics.Debug.WriteLine(msg)); //Set this environment variable - so that it can be used in external config file //add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" /> @@ -117,8 +114,7 @@ namespace Umbraco.Extensions rollingInterval: umbracoFileConfiguration.RollingInterval, flushToDiskInterval: umbracoFileConfiguration.FlushToDiskInterval, rollOnFileSizeLimit: umbracoFileConfiguration.RollOnFileSizeLimit, - retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit - ); + retainedFileCountLimit: umbracoFileConfiguration.RetainedFileCountLimit); return logConfig; } @@ -137,7 +133,8 @@ namespace Umbraco.Extensions { //Main .txt logfile - in similar format to older Log4Net output //Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..txt"), + logConfig.WriteTo.File( + Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles), $"UmbracoTraceLog.{Environment.MachineName}..txt"), shared: true, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: minimumLevel, @@ -162,8 +159,7 @@ namespace Umbraco.Extensions RollingInterval rollingInterval = RollingInterval.Day, bool rollOnFileSizeLimit = false, int? retainedFileCountLimit = 31, - Encoding? encoding = null - ) + Encoding? encoding = null) { formatter ??= new CompactJsonFormatter(); @@ -197,15 +193,19 @@ namespace Umbraco.Extensions /// A Serilog LoggerConfiguration /// The logging configuration /// The log level you wish the JSON file to collect - default is Verbose (highest) + /// /// The number of days to keep log files. Default is set to null which means all logs are kept public static LoggerConfiguration OutputDefaultJsonFile( this LoggerConfiguration logConfig, Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, LogEventLevel minimumLevel = LogEventLevel.Verbose, int? retainedFileCount = null) + ILoggingConfiguration loggingConfiguration, + LogEventLevel minimumLevel = LogEventLevel.Verbose, + int? retainedFileCount = null) { // .clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier) // Ends with ..txt as Date is inserted before file extension substring - logConfig.WriteTo.File(new CompactJsonFormatter(), + logConfig.WriteTo.File( + new CompactJsonFormatter(), Path.Combine(hostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles) ,$"UmbracoTraceLog.{Environment.MachineName}..json"), shared: true, rollingInterval: RollingInterval.Day, // Create a new JSON file every day diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs index 2ca43efd0c..4eb054b2a5 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/SerilogLogger.cs @@ -1,219 +1,155 @@ -using System; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Events; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Infrastructure.Logging.Serilog; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Serilog +namespace Umbraco.Cms.Core.Logging.Serilog; + +/// +/// Implements MS ILogger on top of Serilog. +/// +public class SerilogLogger : IDisposable { - /// - /// Implements MS ILogger on top of Serilog. - /// - public class SerilogLogger : IDisposable + public SerilogLogger(LoggerConfiguration logConfig) => + + // Configure Serilog static global logger with config passed in + SerilogLog = logConfig.CreateLogger(); + + public ILogger SerilogLog { get; } + + [Obsolete] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration) => + CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); + + public void Dispose() => SerilogLog.DisposeIfDisposable(); + + /// + /// Creates a logger with some pre-defined configuration and remainder from config file + /// + /// Used by UmbracoApplicationBase to get its logger. + [Obsolete] + public static SerilogLogger CreateWithDefaultConfiguration( + IHostingEnvironment hostingEnvironment, + ILoggingConfiguration loggingConfiguration, + IConfiguration configuration, + out UmbracoFileConfiguration umbracoFileConfig) { - public global::Serilog.ILogger SerilogLog { get; } - - public SerilogLogger(LoggerConfiguration logConfig) - { - //Configure Serilog static global logger with config passed in - SerilogLog = logConfig.CreateLogger(); - } - - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration) - { - return CreateWithDefaultConfiguration(hostingEnvironment, loggingConfiguration, configuration, out _); - } - - /// - /// Creates a logger with some pre-defined configuration and remainder from config file - /// - /// Used by UmbracoApplicationBase to get its logger. - [Obsolete] - public static SerilogLogger CreateWithDefaultConfiguration( - Umbraco.Cms.Core.Hosting.IHostingEnvironment hostingEnvironment, - ILoggingConfiguration loggingConfiguration, - IConfiguration configuration, - out UmbracoFileConfiguration umbracoFileConfig) - { - var serilogConfig = new LoggerConfiguration() - .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) - .ReadFrom.Configuration(configuration); - - return new SerilogLogger(serilogConfig); - } - - /// - /// Gets a contextualized logger. - /// - private global::Serilog.ILogger LoggerFor(Type reporting) - => SerilogLog.ForContext(reporting); - - /// - /// Maps Umbraco's log level to Serilog's. - /// - private LogEventLevel MapLevel(LogLevel level) - { - switch (level) - { - case LogLevel.Debug: - return LogEventLevel.Debug; - case LogLevel.Error: - return LogEventLevel.Error; - case LogLevel.Fatal: - return LogEventLevel.Fatal; - case LogLevel.Information: - return LogEventLevel.Information; - case LogLevel.Verbose: - return LogEventLevel.Verbose; - case LogLevel.Warning: - return LogEventLevel.Warning; - } - - throw new NotSupportedException($"LogLevel \"{level}\" is not supported."); - } - - /// - public bool IsEnabled(Type reporting, LogLevel level) - => LoggerFor(reporting).IsEnabled(MapLevel(level)); - - /// - public void Fatal(Type reporting, Exception exception, string message) - { - var logger = LoggerFor(reporting); - logger.Fatal(exception, message); - } - - /// - public void Fatal(Type reporting, Exception exception) - { - var logger = LoggerFor(reporting); - var message = "Exception."; - logger.Fatal(exception, message); - } - - /// - public void Fatal(Type reporting, string message) - { - LoggerFor(reporting).Fatal(message); - } - - /// - public void Fatal(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Fatal(messageTemplate, propertyValues); - } - - /// - public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - var logger = LoggerFor(reporting); - logger.Fatal(exception, messageTemplate, propertyValues); - } - - /// - public void Error(Type reporting, Exception exception, string message) - { - var logger = LoggerFor(reporting); - logger.Error(exception, message); - } - - /// - public void Error(Type reporting, Exception exception) - { - var logger = LoggerFor(reporting); - var message = "Exception"; - logger.Error(exception, message); - } - - /// - public void Error(Type reporting, string message) - { - LoggerFor(reporting).Error(message); - } - - /// - public void Error(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Error(messageTemplate, propertyValues); - } - - /// - public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - var logger = LoggerFor(reporting); - logger.Error(exception, messageTemplate, propertyValues); - } - - /// - public void Warn(Type reporting, string message) - { - LoggerFor(reporting).Warning(message); - } - - /// - public void Warn(Type reporting, string message, params object[] propertyValues) - { - LoggerFor(reporting).Warning(message, propertyValues); - } - - /// - public void Warn(Type reporting, Exception exception, string message) - { - LoggerFor(reporting).Warning(exception, message); - } - - /// - public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Warning(exception, messageTemplate, propertyValues); - } - - /// - public void Info(Type reporting, string message) - { - LoggerFor(reporting).Information(message); - } - - /// - public void Info(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Information(messageTemplate, propertyValues); - } - - /// - public void Debug(Type reporting, string message) - { - LoggerFor(reporting).Debug(message); - } - - /// - public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Debug(messageTemplate, propertyValues); - } - - /// - public void Verbose(Type reporting, string message) - { - LoggerFor(reporting).Verbose(message); - } - - /// - public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) - { - LoggerFor(reporting).Verbose(messageTemplate, propertyValues); - } - - public void Dispose() - { - SerilogLog.DisposeIfDisposable(); - } + LoggerConfiguration? serilogConfig = new LoggerConfiguration() + .MinimalConfiguration(hostingEnvironment, loggingConfiguration, configuration, out umbracoFileConfig) + .ReadFrom.Configuration(configuration); + return new SerilogLogger(serilogConfig); } + + public bool IsEnabled(Type reporting, LogLevel level) + => LoggerFor(reporting).IsEnabled(MapLevel(level)); + + /// + /// Gets a contextualized logger. + /// + private ILogger LoggerFor(Type reporting) + => SerilogLog.ForContext(reporting); + + /// + /// Maps Umbraco's log level to Serilog's. + /// + private LogEventLevel MapLevel(LogLevel level) + { + switch (level) + { + case LogLevel.Debug: + return LogEventLevel.Debug; + case LogLevel.Error: + return LogEventLevel.Error; + case LogLevel.Fatal: + return LogEventLevel.Fatal; + case LogLevel.Information: + return LogEventLevel.Information; + case LogLevel.Verbose: + return LogEventLevel.Verbose; + case LogLevel.Warning: + return LogEventLevel.Warning; + } + + throw new NotSupportedException($"LogLevel \"{level}\" is not supported."); + } + + public void Fatal(Type reporting, Exception exception, string message) + { + ILogger logger = LoggerFor(reporting); + logger.Fatal(exception, message); + } + + public void Fatal(Type reporting, Exception exception) + { + ILogger logger = LoggerFor(reporting); + var message = "Exception."; + logger.Fatal(exception, message); + } + + public void Fatal(Type reporting, string message) => LoggerFor(reporting).Fatal(message); + + public void Fatal(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Fatal(messageTemplate, propertyValues); + + public void Fatal(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + { + ILogger logger = LoggerFor(reporting); + logger.Fatal(exception, messageTemplate, propertyValues); + } + + public void Error(Type reporting, Exception exception, string message) + { + ILogger logger = LoggerFor(reporting); + logger.Error(exception, message); + } + + public void Error(Type reporting, Exception exception) + { + ILogger logger = LoggerFor(reporting); + var message = "Exception"; + logger.Error(exception, message); + } + + public void Error(Type reporting, string message) => LoggerFor(reporting).Error(message); + + public void Error(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Error(messageTemplate, propertyValues); + + public void Error(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) + { + ILogger logger = LoggerFor(reporting); + logger.Error(exception, messageTemplate, propertyValues); + } + + public void Warn(Type reporting, string message) => LoggerFor(reporting).Warning(message); + + public void Warn(Type reporting, string message, params object[] propertyValues) => + LoggerFor(reporting).Warning(message, propertyValues); + + public void Warn(Type reporting, Exception exception, string message) => + LoggerFor(reporting).Warning(exception, message); + + public void Warn(Type reporting, Exception exception, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Warning(exception, messageTemplate, propertyValues); + + public void Info(Type reporting, string message) => LoggerFor(reporting).Information(message); + + public void Info(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Information(messageTemplate, propertyValues); + + public void Debug(Type reporting, string message) => LoggerFor(reporting).Debug(message); + + public void Debug(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Debug(messageTemplate, propertyValues); + + public void Verbose(Type reporting, string message) => LoggerFor(reporting).Verbose(message); + + public void Verbose(Type reporting, string messageTemplate, params object[] propertyValues) => + LoggerFor(reporting).Verbose(messageTemplate, propertyValues); } diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs index f306416cf2..b83c76fbd3 100644 --- a/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs +++ b/src/Umbraco.Infrastructure/Logging/Serilog/UmbracoFileConfiguration.cs @@ -1,47 +1,47 @@ -using System; -using System.IO; -using System.Linq; using Microsoft.Extensions.Configuration; using Serilog; using Serilog.Events; -namespace Umbraco.Cms.Infrastructure.Logging.Serilog +namespace Umbraco.Cms.Infrastructure.Logging.Serilog; + +public class UmbracoFileConfiguration { - public class UmbracoFileConfiguration + public UmbracoFileConfiguration(IConfiguration configuration) { - public UmbracoFileConfiguration(IConfiguration configuration) + if (configuration == null) { - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - var appSettings = configuration.GetSection("Serilog:WriteTo"); - var umbracoFileAppSettings = appSettings.GetChildren().LastOrDefault(x => x.GetValue("Name") == "UmbracoFile"); - - if (umbracoFileAppSettings is not null) - { - var args = umbracoFileAppSettings.GetSection("Args"); - - RestrictedToMinimumLevel = args.GetValue(nameof(RestrictedToMinimumLevel), RestrictedToMinimumLevel); - FileSizeLimitBytes = args.GetValue(nameof(FileSizeLimitBytes), FileSizeLimitBytes); - RollingInterval = args.GetValue(nameof(RollingInterval), RollingInterval); - FlushToDiskInterval = args.GetValue(nameof(FlushToDiskInterval), FlushToDiskInterval); - RollOnFileSizeLimit = args.GetValue(nameof(RollOnFileSizeLimit), RollOnFileSizeLimit); - RetainedFileCountLimit = args.GetValue(nameof(RetainedFileCountLimit), RetainedFileCountLimit); - } + throw new ArgumentNullException(nameof(configuration)); } - public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose; - public long FileSizeLimitBytes { get; set; } = 1073741824; - public RollingInterval RollingInterval { get; set; } = RollingInterval.Day; - public TimeSpan? FlushToDiskInterval { get; set; } = null; - public bool RollOnFileSizeLimit { get; set; } = false; - public int RetainedFileCountLimit { get; set; } = 31; + IConfigurationSection? appSettings = configuration.GetSection("Serilog:WriteTo"); + IConfigurationSection? umbracoFileAppSettings = + appSettings.GetChildren().LastOrDefault(x => x.GetValue("Name") == "UmbracoFile"); - public string GetPath(string logDirectory) + if (umbracoFileAppSettings is not null) { - return Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json"); + IConfigurationSection? args = umbracoFileAppSettings.GetSection("Args"); + + RestrictedToMinimumLevel = args.GetValue(nameof(RestrictedToMinimumLevel), RestrictedToMinimumLevel); + FileSizeLimitBytes = args.GetValue(nameof(FileSizeLimitBytes), FileSizeLimitBytes); + RollingInterval = args.GetValue(nameof(RollingInterval), RollingInterval); + FlushToDiskInterval = args.GetValue(nameof(FlushToDiskInterval), FlushToDiskInterval); + RollOnFileSizeLimit = args.GetValue(nameof(RollOnFileSizeLimit), RollOnFileSizeLimit); + RetainedFileCountLimit = args.GetValue(nameof(RetainedFileCountLimit), RetainedFileCountLimit); } } + + public LogEventLevel RestrictedToMinimumLevel { get; set; } = LogEventLevel.Verbose; + + public long FileSizeLimitBytes { get; set; } = 1073741824; + + public RollingInterval RollingInterval { get; set; } = RollingInterval.Day; + + public TimeSpan? FlushToDiskInterval { get; set; } + + public bool RollOnFileSizeLimit { get; set; } + + public int RetainedFileCountLimit { get; set; } = 31; + + public string GetPath(string logDirectory) => + Path.Combine(logDirectory, $"UmbracoTraceLog.{Environment.MachineName}..json"); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs index 36d12dee0d..c5692384f6 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/CountingFilter.cs @@ -1,49 +1,43 @@ -using System; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class CountingFilter : ILogFilter { - internal class CountingFilter : ILogFilter + public CountingFilter() => Counts = new LogLevelCounts(); + + public LogLevelCounts Counts { get; } + + public bool TakeLogEvent(LogEvent e) { - public CountingFilter() + switch (e.Level) { - Counts = new LogLevelCounts(); + case LogEventLevel.Debug: + Counts.Debug++; + break; + + case LogEventLevel.Information: + Counts.Information++; + break; + + case LogEventLevel.Warning: + Counts.Warning++; + break; + + case LogEventLevel.Error: + Counts.Error++; + break; + + case LogEventLevel.Fatal: + Counts.Fatal++; + break; + case LogEventLevel.Verbose: + break; + default: + throw new ArgumentOutOfRangeException(); } - public LogLevelCounts Counts { get; } - - public bool TakeLogEvent(LogEvent e) - { - - switch (e.Level) - { - case LogEventLevel.Debug: - Counts.Debug++; - break; - - case LogEventLevel.Information: - Counts.Information++; - break; - - case LogEventLevel.Warning: - Counts.Warning++; - break; - - case LogEventLevel.Error: - Counts.Error++; - break; - - case LogEventLevel.Fatal: - Counts.Fatal++; - break; - case LogEventLevel.Verbose: - break; - default: - throw new ArgumentOutOfRangeException(); - } - - //Don't add it to the list - return false; - } + // Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs index 1a4ececff6..834da2952e 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ErrorCounterFilter.cs @@ -1,18 +1,19 @@ -using Serilog.Events; +using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class ErrorCounterFilter : ILogFilter { - internal class ErrorCounterFilter : ILogFilter + public int Count { get; private set; } + + public bool TakeLogEvent(LogEvent e) { - public int Count { get; private set; } - - public bool TakeLogEvent(LogEvent e) + if (e.Level == LogEventLevel.Fatal || e.Level == LogEventLevel.Error || e.Exception != null) { - if (e.Level == LogEventLevel.Fatal || e.Level == LogEventLevel.Error || e.Exception != null) - Count++; - - //Don't add it to the list - return false; + Count++; } + + // Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs index a1c549add8..a8444f4276 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs @@ -1,79 +1,75 @@ -using System; -using System.Linq; using Serilog.Events; using Serilog.Expressions; using Umbraco.Cms.Infrastructure.Logging.Viewer; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +// Log Expression Filters (pass in filter exp string) +internal class ExpressionFilter : ILogFilter { - //Log Expression Filters (pass in filter exp string) - internal class ExpressionFilter : ILogFilter + private const string ExpressionOperators = "()+=*<>%-"; + private readonly Func? _filter; + + public ExpressionFilter(string? filterExpression) { - private readonly Func? _filter; - private const string s_expressionOperators = "()+=*<>%-"; + Func? filter; - public ExpressionFilter(string? filterExpression) + // Our custom Serilog Functions to extend Serilog.Expressions + // In this case we are plugging the gap for the missing Has() + // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions + // Along with patching support for the more verbose built in property names + var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); + + if (string.IsNullOrEmpty(filterExpression)) { - Func? filter; - - // Our custom Serilog Functions to extend Serilog.Expressions - // In this case we are plugging the gap for the missing Has() - // function from porting away from Serilog.Filters.Expressions to Serilog.Expressions - // Along with patching support for the more verbose built in property names - var customSerilogFunctions = new SerilogLegacyNameResolver(typeof(SerilogExpressionsFunctions)); - - if (string.IsNullOrEmpty(filterExpression)) - { - return; - } - - // If the expression is one word and doesn't contain a serilog operator then we can perform a like search - if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(s_expressionOperators.Select(c => c))) - { - filter = PerformMessageLikeFilter(filterExpression); - } - else // check if it's a valid expression - { - // If the expression evaluates then make it into a filter - if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression? compiled, out var error)) - { - filter = evt => - { - LogEventPropertyValue? result = compiled(evt); - return ExpressionResult.IsTrue(result); - }; - } - else - { - // 'error' describes a syntax error, where it was unable to compile an expression - // Assume the expression was a search string and make a Like filter from that - filter = PerformMessageLikeFilter(filterExpression); - } - } - - _filter = filter; + return; } - public bool TakeLogEvent(LogEvent e) + // If the expression is one word and doesn't contain a serilog operator then we can perform a like search + if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(ExpressionOperators.Select(c => c))) { - return _filter == null || _filter(e); + filter = PerformMessageLikeFilter(filterExpression); } - private Func? PerformMessageLikeFilter(string filterExpression) + // check if it's a valid expression + else { - var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; - if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression? compiled, out var error)) + // If the expression evaluates then make it into a filter + if (SerilogExpression.TryCompile(filterExpression, null, customSerilogFunctions, out CompiledExpression? compiled, out var error)) { - // `compiled` is a function that can be executed against `LogEvent`s: - return evt => + filter = evt => { LogEventPropertyValue? result = compiled(evt); return ExpressionResult.IsTrue(result); }; } - - return null; + else + { + // 'error' describes a syntax error, where it was unable to compile an expression + // Assume the expression was a search string and make a Like filter from that + filter = PerformMessageLikeFilter(filterExpression); + } } + + _filter = filter; + } + + public bool TakeLogEvent(LogEvent e) => _filter == null || _filter(e); + + private Func? PerformMessageLikeFilter(string filterExpression) + { + var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'"; + if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression? compiled, out var error)) + { + // `compiled` is a function that can be executed against `LogEvent`s: + return evt => + { + LogEventPropertyValue? result = compiled(evt); + return ExpressionResult.IsTrue(result); + }; + } + + return null; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs index 4619df2b13..e276bdfa5a 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogFilter.cs @@ -1,9 +1,8 @@ -using Serilog.Events; +using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public interface ILogFilter { - public interface ILogFilter - { - bool TakeLogEvent(LogEvent e); - } + bool TakeLogEvent(LogEvent e); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs index 1566b96282..25576b88da 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogLevelLoader.cs @@ -1,18 +1,17 @@ using System.Collections.ObjectModel; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer -{ - public interface ILogLevelLoader - { - /// - /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. - /// - ReadOnlyDictionary GetLogLevelsFromSinks(); +namespace Umbraco.Cms.Core.Logging.Viewer; - /// - /// Get the Serilog minimum-level value from the config file. - /// - LogEventLevel? GetGlobalMinLogLevel(); - } +public interface ILogLevelLoader +{ + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + ReadOnlyDictionary GetLogLevelsFromSinks(); + + /// + /// Get the Serilog minimum-level value from the config file. + /// + LogEventLevel? GetGlobalMinLogLevel(); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs index 2bda63c96b..df1457d419 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewer.cs @@ -1,64 +1,59 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using Serilog.Events; using Umbraco.Cms.Core.Models; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public interface ILogViewer { - public interface ILogViewer - { - /// - /// Get all saved searches from your chosen data source - /// - IReadOnlyList? GetSavedSearches(); + bool CanHandleLargeLogs { get; } - /// - /// Adds a new saved search to chosen data source and returns the updated searches - /// - IReadOnlyList? AddSavedSearch(string? name, string? query); + /// + /// Get all saved searches from your chosen data source + /// + IReadOnlyList? GetSavedSearches(); - /// - /// Deletes a saved search to chosen data source and returns the remaining searches - /// - IReadOnlyList? DeleteSavedSearch(string? name, string? query); + /// + /// Adds a new saved search to chosen data source and returns the updated searches + /// + IReadOnlyList? AddSavedSearch(string? name, string? query); - /// - /// A count of number of errors - /// By counting Warnings with Exceptions, Errors & Fatal messages - /// - int GetNumberOfErrors(LogTimePeriod logTimePeriod); + /// + /// Deletes a saved search to chosen data source and returns the remaining searches + /// + IReadOnlyList? DeleteSavedSearch(string? name, string? query); - /// - /// Returns a number of the different log level entries - /// - LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); + /// + /// A count of number of errors + /// By counting Warnings with Exceptions, Errors & Fatal messages + /// + int GetNumberOfErrors(LogTimePeriod logTimePeriod); - /// - /// Returns a list of all unique message templates and their counts - /// - IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); + /// + /// Returns a number of the different log level entries + /// + LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod); - bool CanHandleLargeLogs { get; } + /// + /// Returns a list of all unique message templates and their counts + /// + IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod); - bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); - /// - /// Gets the current Serilog minimum log level - /// - /// - [Obsolete("Please use GetLogLevels() instead. Scheduled for removal in V11.")] - string GetLogLevel(); + /// + /// Gets the current Serilog minimum log level + /// + /// + [Obsolete("Please use GetLogLevels() instead. Scheduled for removal in V11.")] + string GetLogLevel(); - /// - /// Returns the collection of logs - /// - PagedResult GetLogs(LogTimePeriod logTimePeriod, - int pageNumber = 1, - int pageSize = 100, - Direction orderDirection = Direction.Descending, - string? filterExpression = null, - string[]? logLevels = null); - - } + /// + /// Returns the collection of logs + /// + PagedResult GetLogs( + LogTimePeriod logTimePeriod, + int pageNumber = 1, + int pageSize = 100, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs index 5be26a1099..bdcbf64a94 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/ILogViewerConfig.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Core.Logging.Viewer; -namespace Umbraco.Cms.Core.Logging.Viewer +public interface ILogViewerConfig { - public interface ILogViewerConfig - { - IReadOnlyList? GetSavedSearches(); - IReadOnlyList? AddSavedSearch(string? name, string? query); - IReadOnlyList? DeleteSavedSearch(string? name, string? query); - } + IReadOnlyList? GetSavedSearches(); + + IReadOnlyList? AddSavedSearch(string? name, string? query); + + IReadOnlyList? DeleteSavedSearch(string? name, string? query); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs index f397c1ab7c..6f03135c1f 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelCounts.cs @@ -1,15 +1,14 @@ -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogLevelCounts { - public class LogLevelCounts - { - public int Information { get; set; } + public int Information { get; set; } - public int Debug { get; set; } + public int Debug { get; set; } - public int Warning { get; set; } + public int Warning { get; set; } - public int Error { get; set; } + public int Error { get; set; } - public int Fatal { get; set; } - } + public int Fatal { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs index 37c7923cca..a0f6927ef7 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogLevelLoader.cs @@ -1,41 +1,36 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; using Serilog; using Serilog.Events; using Umbraco.Cms.Infrastructure.Logging.Serilog; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogLevelLoader : ILogLevelLoader { - public class LogLevelLoader : ILogLevelLoader + private readonly UmbracoFileConfiguration _umbracoFileConfig; + + public LogLevelLoader(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; + + /// + /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. + /// + public ReadOnlyDictionary GetLogLevelsFromSinks() { - private readonly UmbracoFileConfiguration _umbracoFileConfig; - - public LogLevelLoader(UmbracoFileConfiguration umbracoFileConfig) => _umbracoFileConfig = umbracoFileConfig; - - /// - /// Get the Serilog level values of the global minimum and the UmbracoFile one from the config file. - /// - public ReadOnlyDictionary GetLogLevelsFromSinks() + var configuredLogLevels = new Dictionary { - var configuredLogLevels = new Dictionary - { - { "Global", GetGlobalMinLogLevel() }, - { "UmbracoFile", _umbracoFileConfig.RestrictedToMinimumLevel } - }; + { "Global", GetGlobalMinLogLevel() }, { "UmbracoFile", _umbracoFileConfig.RestrictedToMinimumLevel }, + }; - return new ReadOnlyDictionary(configuredLogLevels); - } + return new ReadOnlyDictionary(configuredLogLevels); + } - /// - /// Get the Serilog minimum-level value from the config file. - /// - public LogEventLevel? GetGlobalMinLogLevel() - { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; - return (LogEventLevel?)logLevel; - } + /// + /// Get the Serilog minimum-level value from the config file. + /// + public LogEventLevel? GetGlobalMinLogLevel() + { + LogEventLevel? logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.IsEnabled) + .DefaultIfEmpty(LogEventLevel.Information).Min(); + return logLevel; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs index 9bdea3f650..3974a8da1e 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogMessage.cs @@ -1,43 +1,40 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Serilog.Events; -using System; -using System.Collections.Generic; // ReSharper disable UnusedAutoPropertyAccessor.Global -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogMessage { - public class LogMessage - { - /// - /// The time at which the log event occurred. - /// - public DateTimeOffset Timestamp { get; set; } + /// + /// The time at which the log event occurred. + /// + public DateTimeOffset Timestamp { get; set; } - /// - /// The level of the event. - /// - [JsonConverter(typeof(StringEnumConverter))] - public LogEventLevel Level { get; set; } + /// + /// The level of the event. + /// + [JsonConverter(typeof(StringEnumConverter))] + public LogEventLevel Level { get; set; } - /// - /// The message template describing the log event. - /// - public string? MessageTemplateText { get; set; } + /// + /// The message template describing the log event. + /// + public string? MessageTemplateText { get; set; } - /// - /// The message template filled with the log event properties. - /// - public string? RenderedMessage { get; set; } + /// + /// The message template filled with the log event properties. + /// + public string? RenderedMessage { get; set; } - /// - /// Properties associated with the log event, including those presented in Serilog.Events.LogEvent.MessageTemplate. - /// - public IReadOnlyDictionary? Properties { get; set; } + /// + /// Properties associated with the log event, including those presented in Serilog.Events.LogEvent.MessageTemplate. + /// + public IReadOnlyDictionary? Properties { get; set; } - /// - /// An exception associated with the log event, or null. - /// - public string? Exception { get; set; } - } + /// + /// An exception associated with the log event, or null. + /// + public string? Exception { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs index ecded4d35b..821115ff11 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogTemplate.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Core.Logging.Viewer -{ - public class LogTemplate - { - public string? MessageTemplate { get; set; } +namespace Umbraco.Cms.Core.Logging.Viewer; - public int Count { get; set; } - } +public class LogTemplate +{ + public string? MessageTemplate { get; set; } + + public int Count { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs index 446f7bf160..67533ef4f1 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogTimePeriod.cs @@ -1,16 +1,14 @@ -using System; +namespace Umbraco.Cms.Core.Logging.Viewer; -namespace Umbraco.Cms.Core.Logging.Viewer +public class LogTimePeriod { - public class LogTimePeriod + public LogTimePeriod(DateTime startTime, DateTime endTime) { - public LogTimePeriod(DateTime startTime, DateTime endTime) - { - StartTime = startTime; - EndTime = endTime; - } - - public DateTime StartTime { get; } - public DateTime EndTime { get; } + StartTime = startTime; + EndTime = endTime; } + + public DateTime StartTime { get; } + + public DateTime EndTime { get; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs index 8b11639696..e8b9de36d7 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/LogViewerConfig.cs @@ -1,49 +1,47 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; +using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class LogViewerConfig : ILogViewerConfig { - public class LogViewerConfig : ILogViewerConfig + private readonly ILogViewerQueryRepository _logViewerQueryRepository; + private readonly IScopeProvider _scopeProvider; + + public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) { - private readonly ILogViewerQueryRepository _logViewerQueryRepository; - private readonly IScopeProvider _scopeProvider; + _logViewerQueryRepository = logViewerQueryRepository; + _scopeProvider = scopeProvider; + } - public LogViewerConfig(ILogViewerQueryRepository logViewerQueryRepository, IScopeProvider scopeProvider) + public IReadOnlyList? GetSavedSearches() + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + IEnumerable? logViewerQueries = _logViewerQueryRepository.GetMany(); + SavedLogSearch[]? result = logViewerQueries?.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); + return result; + } + + public IReadOnlyList? AddSavedSearch(string? name, string? query) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); + + return GetSavedSearches(); + } + + public IReadOnlyList? DeleteSavedSearch(string? name, string? query) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + ILogViewerQuery? item = name is null ? null : _logViewerQueryRepository.GetByName(name); + if (item is not null) { - _logViewerQueryRepository = logViewerQueryRepository; - _scopeProvider = scopeProvider; + _logViewerQueryRepository.Delete(item); } - public IReadOnlyList? GetSavedSearches() - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - var logViewerQueries = _logViewerQueryRepository.GetMany(); - var result = logViewerQueries?.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray(); - return result; - } - - public IReadOnlyList? AddSavedSearch(string? name, string? query) - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - _logViewerQueryRepository.Save(new LogViewerQuery(name, query)); - - return GetSavedSearches(); - } - - public IReadOnlyList? DeleteSavedSearch(string? name, string? query) - { - using var scope = _scopeProvider.CreateScope(autoComplete: true); - var item = name is null ? null : _logViewerQueryRepository.GetByName(name); - if (item is not null) - { - _logViewerQueryRepository.Delete(item); - } - - //Return the updated object - so we can instantly reset the entire array from the API response - return GetSavedSearches(); - } + // Return the updated object - so we can instantly reset the entire array from the API response + return GetSavedSearches(); } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs index 1b89716256..62b040f5f1 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/MessageTemplateFilter.cs @@ -1,28 +1,26 @@ -using System.Collections.Generic; using Serilog.Events; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class MessageTemplateFilter : ILogFilter { - internal class MessageTemplateFilter : ILogFilter + public readonly Dictionary Counts = new(); + + public bool TakeLogEvent(LogEvent e) { - public readonly Dictionary Counts = new Dictionary(); - - public bool TakeLogEvent(LogEvent e) + var templateText = e.MessageTemplate.Text; + if (Counts.TryGetValue(templateText, out var count)) { - var templateText = e.MessageTemplate.Text; - if (Counts.TryGetValue(templateText, out var count)) - { - count++; - } - else - { - count = 1; - } - - Counts[templateText] = count; - - //Don't add it to the list - return false; + count++; } + else + { + count = 1; + } + + Counts[templateText] = count; + + // Don't add it to the list + return false; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs index adbd1a6431..320f121890 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SavedLogSearch.cs @@ -1,13 +1,12 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public class SavedLogSearch { - public class SavedLogSearch - { - [JsonProperty("name")] - public string? Name { get; set; } + [JsonProperty("name")] + public string? Name { get; set; } - [JsonProperty("query")] - public string? Query { get; set; } - } + [JsonProperty("query")] + public string? Query { get; set; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs index 92b16b9729..9cf9cc7307 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogExpressionsFunctions.cs @@ -1,14 +1,10 @@ using Serilog.Events; -namespace Umbraco.Cms.Infrastructure.Logging.Viewer +namespace Umbraco.Cms.Infrastructure.Logging.Viewer; + +public class SerilogExpressionsFunctions { - public class SerilogExpressionsFunctions - { - // This Has() code is the same as the renamed IsDefined() function - // Added this to help backport and ensure saved queries continue to work if using Has() - public static LogEventPropertyValue? Has(LogEventPropertyValue? value) - { - return new ScalarValue(value != null); - } - } + // This Has() code is the same as the renamed IsDefined() function + // Added this to help backport and ensure saved queries continue to work if using Has() + public static LogEventPropertyValue? Has(LogEventPropertyValue? value) => new ScalarValue(value != null); } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs index ba148a1bda..c573f237b2 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogJsonLogViewer.cs @@ -1,140 +1,133 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Serilog.Events; using Serilog.Formatting.Compact.Reader; +using ILogger = Serilog.ILogger; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase { - internal class SerilogJsonLogViewer : SerilogLogViewerSourceBase + private const int FileSizeCap = 100; + private readonly ILogger _logger; + private readonly string _logsPath; + + public SerilogJsonLogViewer( + ILogger logger, + ILogViewerConfig logViewerConfig, + ILoggingConfiguration loggingConfiguration, + ILogLevelLoader logLevelLoader, + ILogger serilogLog) + : base(logViewerConfig, logLevelLoader, serilogLog) { - private readonly string _logsPath; - private readonly ILogger _logger; + _logger = logger; + _logsPath = loggingConfiguration.LogDirectory; + } - public SerilogJsonLogViewer( - ILogger logger, - ILogViewerConfig logViewerConfig, - ILoggingConfiguration loggingConfiguration, - ILogLevelLoader logLevelLoader, - global::Serilog.ILogger serilogLog) - : base(logViewerConfig, logLevelLoader, serilogLog) + public override bool CanHandleLargeLogs => false; + + public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) + { + // Log Directory + var logDirectory = _logsPath; + + // Number of entries + long fileSizeCount = 0; + + // foreach full day in the range - see if we can find one or more filenames that end with + // yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing + for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { - _logger = logger; - _logsPath = loggingConfiguration.LogDirectory; + // Filename ending to search for (As could be multiple) + var filesToFind = GetSearchPattern(day); + + var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind); + + fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length); } - private const int FileSizeCap = 100; + // The GetLogSize call on JsonLogViewer returns the total file size in bytes + // Check if the log size is not greater than 100Mb (FileSizeCap) + var logSizeAsMegabytes = fileSizeCount / 1024 / 1024; + return logSizeAsMegabytes <= FileSizeCap; + } - public override bool CanHandleLargeLogs => false; + protected override IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, + int take) + { + var logs = new List(); - public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) + var count = 0; + + // foreach full day in the range - see if we can find one or more filenames that end with + // yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing + for (DateTime day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) { - //Log Directory - var logDirectory = _logsPath; + // Filename ending to search for (As could be multiple) + var filesToFind = GetSearchPattern(day); - //Number of entries - long fileSizeCount = 0; + var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind); - //foreach full day in the range - see if we can find one or more filenames that end with - //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) + // Foreach file we find - open it + foreach (var filePath in filesForCurrentDay) { - //Filename ending to search for (As could be multiple) - var filesToFind = GetSearchPattern(day); - - var filesForCurrentDay = Directory.GetFiles(logDirectory, filesToFind); - - fileSizeCount += filesForCurrentDay.Sum(x => new FileInfo(x).Length); - } - - //The GetLogSize call on JsonLogViewer returns the total file size in bytes - //Check if the log size is not greater than 100Mb (FileSizeCap) - var logSizeAsMegabytes = fileSizeCount / 1024 / 1024; - return logSizeAsMegabytes <= FileSizeCap; - } - - private string GetSearchPattern(DateTime day) - { - return $"*{day:yyyyMMdd}*.json"; - } - - protected override IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take) - { - var logs = new List(); - - var count = 0; - - //foreach full day in the range - see if we can find one or more filenames that end with - //yyyyMMdd.json - Ends with due to MachineName in filenames - could be 1 or more due to load balancing - for (var day = logTimePeriod.StartTime.Date; day.Date <= logTimePeriod.EndTime.Date; day = day.AddDays(1)) - { - //Filename ending to search for (As could be multiple) - var filesToFind = GetSearchPattern(day); - - var filesForCurrentDay = Directory.GetFiles(_logsPath, filesToFind); - - //Foreach file we find - open it - foreach (var filePath in filesForCurrentDay) + // Open log file & add contents to the log collection + // Which we then use LINQ to page over + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { - //Open log file & add contents to the log collection - //Which we then use LINQ to page over - using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + using (var stream = new StreamReader(fs)) { - using (var stream = new StreamReader(fs)) + var reader = new LogEventReader(stream); + while (TryRead(reader, out LogEvent? evt)) { - var reader = new LogEventReader(stream); - while (TryRead(reader, out var evt)) + // We may get a null if log line is malformed + if (evt == null) { - //We may get a null if log line is malformed - if (evt == null) - { - continue; - } - - if (count > skip + take) - { - break; - } - - if (count < skip) - { - count++; - continue; - } - - if (filter.TakeLogEvent(evt)) - { - logs.Add(evt); - } - - count++; + continue; } + + if (count > skip + take) + { + break; + } + + if (count < skip) + { + count++; + continue; + } + + if (filter.TakeLogEvent(evt)) + { + logs.Add(evt); + } + + count++; } } } } - - return logs; } - private bool TryRead(LogEventReader reader, out LogEvent? evt) - { - try - { - return reader.TryRead(out evt); - } - catch (JsonReaderException ex) - { - // As we are reading/streaming one line at a time in the JSON file - // Thus we can not report the line number, as it will always be 1 - _logger.LogError(ex, "Unable to parse a line in the JSON log file"); + return logs; + } - evt = null; - return true; - } + private string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json"; + + private bool TryRead(LogEventReader reader, out LogEvent? evt) + { + try + { + return reader.TryRead(out evt); + } + catch (JsonReaderException ex) + { + // As we are reading/streaming one line at a time in the JSON file + // Thus we can not report the line number, as it will always be 1 + _logger.LogError(ex, "Unable to parse a line in the JSON log file"); + + evt = null; + return true; } } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs index 8e24f40b6c..deac7b3a5a 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLegacyNameResolver.cs @@ -1,39 +1,38 @@ -using System; using System.Diagnostics.CodeAnalysis; using Serilog.Expressions; -namespace Umbraco.Cms.Infrastructure.Logging.Viewer +namespace Umbraco.Cms.Infrastructure.Logging.Viewer; + +/// +/// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality +/// Of easily allowing any static methods definied in the passed in class/type +/// To extend as functions to use for filtering logs such as Has() and any other custom ones +/// +public class SerilogLegacyNameResolver : StaticMemberNameResolver { - /// - /// Inherits Serilog's StaticMemberNameResolver to ensure we get same functionality - /// Of easily allowing any static methods definied in the passed in class/type - /// To extend as functions to use for filtering logs such as Has() and any other custom ones - /// - public class SerilogLegacyNameResolver : StaticMemberNameResolver + public SerilogLegacyNameResolver(Type type) + : base(type) { - public SerilogLegacyNameResolver(Type type) : base(type) - { - } + } - /// - /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions - /// So we can still support the more verbose built in property names such as - /// Exception, Level, MessageTemplate etc - /// - public override bool TryResolveBuiltInPropertyName(string alias, [MaybeNullWhen(false)] out string target) + /// + /// Allows us to fix the gap from migrating away from Serilog.Filters.Expressions + /// So we can still support the more verbose built in property names such as + /// Exception, Level, MessageTemplate etc + /// + public override bool TryResolveBuiltInPropertyName(string alias, [MaybeNullWhen(false)] out string target) + { + target = alias switch { - target = alias switch - { - "Exception" => "x", - "Level" => "l", - "Message" => "m", - "MessageTemplate" => "mt", - "Properties" => "p", - "Timestamp" => "t", - _ => null - }; + "Exception" => "x", + "Level" => "l", + "Message" => "m", + "MessageTemplate" => "mt", + "Properties" => "p", + "Timestamp" => "t", + _ => null, + }; - return target != null; - } + return target != null; } } diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs index ca5c5c3a46..9dae731af3 100644 --- a/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs +++ b/src/Umbraco.Infrastructure/Logging/Viewer/SerilogLogViewerSourceBase.cs @@ -1,154 +1,149 @@ -using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Serilog; using Serilog.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Logging.Viewer +namespace Umbraco.Cms.Core.Logging.Viewer; + +public abstract class SerilogLogViewerSourceBase : ILogViewer { - public abstract class SerilogLogViewerSourceBase : ILogViewer + private readonly ILogLevelLoader _logLevelLoader; + private readonly ILogViewerConfig _logViewerConfig; + private readonly ILogger _serilogLog; + + [Obsolete("Please use ctor with all params instead. Scheduled for removal in V11.")] + protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogger serilogLog) { - private readonly ILogViewerConfig _logViewerConfig; - private readonly ILogLevelLoader _logLevelLoader; - private readonly global::Serilog.ILogger _serilogLog; + _logViewerConfig = logViewerConfig; + _logLevelLoader = StaticServiceProvider.Instance.GetRequiredService(); + _serilogLog = serilogLog; + } - [Obsolete("Please use ctor with all params instead. Scheduled for removal in V11.")] - protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, global::Serilog.ILogger serilogLog) + protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, ILogger serilogLog) + { + _logViewerConfig = logViewerConfig; + _logLevelLoader = logLevelLoader; + _serilogLog = serilogLog; + } + + public abstract bool CanHandleLargeLogs { get; } + + public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + + public virtual IReadOnlyList? GetSavedSearches() + => _logViewerConfig.GetSavedSearches(); + + public virtual IReadOnlyList? AddSavedSearch(string? name, string? query) + => _logViewerConfig.AddSavedSearch(name, query); + + public virtual IReadOnlyList? DeleteSavedSearch(string? name, string? query) + => _logViewerConfig.DeleteSavedSearch(name, query); + + public int GetNumberOfErrors(LogTimePeriod logTimePeriod) + { + var errorCounter = new ErrorCounterFilter(); + GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue); + return errorCounter.Count; + } + + /// + /// Get the Serilog minimum-level value from the config file. + /// + [Obsolete("Please use LogLevelLoader.GetGlobalMinLogLevel() instead. Scheduled for removal in V11.")] + public string GetLogLevel() + { + LogEventLevel? logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast() + .Where(_serilogLog.IsEnabled).DefaultIfEmpty(LogEventLevel.Information).Min(); + return logLevel?.ToString() ?? string.Empty; + } + + public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) + { + var counter = new CountingFilter(); + GetLogs(logTimePeriod, counter, 0, int.MaxValue); + return counter.Counts; + } + + public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) + { + var messageTemplates = new MessageTemplateFilter(); + GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue); + + IOrderedEnumerable templates = messageTemplates.Counts + .Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value }) + .OrderByDescending(x => x.Count); + + return templates; + } + + public PagedResult GetLogs( + LogTimePeriod logTimePeriod, + int pageNumber = 1, + int pageSize = 100, + Direction orderDirection = Direction.Descending, + string? filterExpression = null, + string[]? logLevels = null) + { + var expression = new ExpressionFilter(filterExpression); + IReadOnlyList filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); + + // This is user used the checkbox UI to toggle which log levels they wish to see + // If an empty array or null - its implied all levels to be viewed + if (logLevels?.Length > 0) { - _logViewerConfig = logViewerConfig; - _logLevelLoader = StaticServiceProvider.Instance.GetRequiredService(); - _serilogLog = serilogLog; - } - - protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, global::Serilog.ILogger serilogLog) - { - _logViewerConfig = logViewerConfig; - _logLevelLoader = logLevelLoader; - _serilogLog = serilogLog; - } - - public abstract bool CanHandleLargeLogs { get; } - - /// - /// Get all logs from your chosen data source back as Serilog LogEvents - /// - protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); - - public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); - - public virtual IReadOnlyList? GetSavedSearches() - => _logViewerConfig.GetSavedSearches(); - - public virtual IReadOnlyList? AddSavedSearch(string? name, string? query) - => _logViewerConfig.AddSavedSearch(name, query); - - public virtual IReadOnlyList? DeleteSavedSearch(string? name, string? query) - => _logViewerConfig.DeleteSavedSearch(name, query); - - public int GetNumberOfErrors(LogTimePeriod logTimePeriod) - { - var errorCounter = new ErrorCounterFilter(); - GetLogs(logTimePeriod, errorCounter, 0, int.MaxValue); - return errorCounter.Count; - } - - /// - /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. - /// - public ReadOnlyDictionary GetLogLevels() - { - return _logLevelLoader.GetLogLevelsFromSinks(); - } - - /// - /// Get the Serilog minimum-level value from the config file. - /// - [Obsolete("Please use LogLevelLoader.GetGlobalMinLogLevel() instead. Scheduled for removal in V11.")] - public string GetLogLevel() - { - var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(_serilogLog.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null; - return logLevel?.ToString() ?? string.Empty; - } - - public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) - { - var counter = new CountingFilter(); - GetLogs(logTimePeriod, counter, 0, int.MaxValue); - return counter.Counts; - } - - public IEnumerable GetMessageTemplates(LogTimePeriod logTimePeriod) - { - var messageTemplates = new MessageTemplateFilter(); - GetLogs(logTimePeriod, messageTemplates, 0, int.MaxValue); - - var templates = messageTemplates.Counts. - Select(x => new LogTemplate { MessageTemplate = x.Key, Count = x.Value }) - .OrderByDescending(x=> x.Count); - - return templates; - } - - public PagedResult GetLogs(LogTimePeriod logTimePeriod, - int pageNumber = 1, int pageSize = 100, - Direction orderDirection = Direction.Descending, - string? filterExpression = null, - string[]? logLevels = null) - { - var expression = new ExpressionFilter(filterExpression); - var filteredLogs = GetLogs(logTimePeriod, expression, 0, int.MaxValue); - - //This is user used the checkbox UI to toggle which log levels they wish to see - //If an empty array or null - its implied all levels to be viewed - if (logLevels?.Length > 0) + var logsAfterLevelFilters = new List(); + var validLogType = true; + foreach (var level in logLevels) { - var logsAfterLevelFilters = new List(); - var validLogType = true; - foreach (var level in logLevels) + // Check if level string is part of the LogEventLevel enum + if (Enum.IsDefined(typeof(LogEventLevel), level)) { - //Check if level string is part of the LogEventLevel enum - if(Enum.IsDefined(typeof(LogEventLevel), level)) - { - validLogType = true; - logsAfterLevelFilters.AddRange(filteredLogs.Where(x => string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase))); - } - else - { - validLogType = false; - } + validLogType = true; + logsAfterLevelFilters.AddRange(filteredLogs.Where(x => + string.Equals(x.Level.ToString(), level, StringComparison.InvariantCultureIgnoreCase))); } - - if (validLogType) + else { - filteredLogs = logsAfterLevelFilters; + validLogType = false; } } - long totalRecords = filteredLogs.Count; - - //Order By, Skip, Take & Select - var logMessages = filteredLogs - .OrderBy(l => l.Timestamp, orderDirection) - .Skip(pageSize * (pageNumber - 1)) - .Take(pageSize) - .Select(x => new LogMessage - { - Timestamp = x.Timestamp, - Level = x.Level, - MessageTemplateText = x.MessageTemplate.Text, - Exception = x.Exception?.ToString(), - Properties = x.Properties, - RenderedMessage = x.RenderMessage() - }); - - return new PagedResult(totalRecords, pageNumber, pageSize) + if (validLogType) { - Items = logMessages - }; + filteredLogs = logsAfterLevelFilters; + } } + + long totalRecords = filteredLogs.Count; + + // Order By, Skip, Take & Select + IEnumerable logMessages = filteredLogs + .OrderBy(l => l.Timestamp, orderDirection) + .Skip(pageSize * (pageNumber - 1)) + .Take(pageSize) + .Select(x => new LogMessage + { + Timestamp = x.Timestamp, + Level = x.Level, + MessageTemplateText = x.MessageTemplate.Text, + Exception = x.Exception?.ToString(), + Properties = x.Properties, + RenderedMessage = x.RenderMessage(), + }); + + return new PagedResult(totalRecords, pageNumber, pageSize) { Items = logMessages }; } + + /// + /// Get the Serilog minimum-level and UmbracoFile-level values from the config file. + /// + public ReadOnlyDictionary GetLogLevels() => _logLevelLoader.GetLogLevelsFromSinks(); + + /// + /// Get all logs from your chosen data source back as Serilog LogEvents + /// + protected abstract IReadOnlyList GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take); } diff --git a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs index 13b0333984..07109729b6 100644 --- a/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs +++ b/src/Umbraco.Infrastructure/Macros/MacroTagParser.cs @@ -1,208 +1,215 @@ -using System; -using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using HtmlAgilityPack; using Umbraco.Cms.Core.Xml; -namespace Umbraco.Cms.Infrastructure.Macros +namespace Umbraco.Cms.Infrastructure.Macros; + +/// +/// Parses the macro syntax in a string and renders out it's contents +/// +public class MacroTagParser { - /// - /// Parses the macro syntax in a string and renders out it's contents - /// - public class MacroTagParser - { - private static readonly Regex MacroRteContent = new Regex(@"()", + private static readonly Regex _macroRteContent = new( + @"()", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); + + private static readonly Regex _macroPersistedFormat = + new( + @"(<\?UMBRACO_MACRO (?:.+?)??macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); - private static readonly Regex MacroPersistedFormat = - new Regex(@"(<\?UMBRACO_MACRO (?:.+?)??macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); - - /// - /// This formats the persisted string to something useful for the rte so that the macro renders properly since we - /// persist all macro formats like {?UMBRACO_MACRO macroAlias=\"myMacro\" /} - /// - /// - /// The HTML attributes to be added to the div - /// - /// - /// This converts the persisted macro format to this: - /// - /// {div class='umb-macro-holder'} - /// - /// {ins}Macro alias: {strong}My Macro{/strong}{/ins} - /// {/div} - /// - /// - public static string FormatRichTextPersistedDataForEditor(string persistedContent, IDictionary htmlAttributes) + /// + /// This formats the persisted string to something useful for the rte so that the macro renders properly since we + /// persist all macro formats like {?UMBRACO_MACRO macroAlias=\"myMacro\" /} + /// + /// + /// The HTML attributes to be added to the div + /// + /// + /// This converts the persisted macro format to this: + /// {div class='umb-macro-holder'} + /// + /// {ins}Macro alias: {strong}My Macro{/strong}{/ins} + /// {/div} + /// + public static string FormatRichTextPersistedDataForEditor( + string persistedContent, + IDictionary htmlAttributes) => + _macroPersistedFormat.Replace(persistedContent, match => { - return MacroPersistedFormat.Replace(persistedContent, match => + if (match.Groups.Count >= 3) { - if (match.Groups.Count >= 3) + //
+ var alias = match.Groups[2].Value; + var sb = new StringBuilder("
htmlAttribute in htmlAttributes) { - //
- var alias = match.Groups[2].Value; - var sb = new StringBuilder("
"); - sb.Append(""); - sb.Append(""); - sb.Append("Macro alias: "); - sb.Append(""); - sb.Append(alias); - sb.Append("
"); - return sb.ToString(); + sb.Append(" "); + sb.Append(htmlAttribute.Key); + sb.Append("=\""); + sb.Append(htmlAttribute.Value); + sb.Append("\""); } - //replace with nothing if we couldn't find the syntax for whatever reason - return ""; - }); + + sb.AppendLine(">"); + sb.Append(""); + sb.Append(""); + sb.Append("Macro alias: "); + sb.Append(""); + sb.Append(alias); + sb.Append("
"); + return sb.ToString(); + } + + // replace with nothing if we couldn't find the syntax for whatever reason + return string.Empty; + }); + + /// + /// This formats the string content posted from a rich text editor that contains macro contents to be persisted. + /// + /// + /// + /// This is required because when editors are using the rte, the HTML that is contained in the editor might actually be + /// displaying + /// the entire macro content, when the data is submitted the editor will clear most of this data out but we'll still + /// need to parse it properly + /// and ensure the correct syntax is persisted to the db. + /// When a macro is inserted into the rte editor, the HTML will be: + /// {div class='umb-macro-holder'} + /// + /// This could be some macro content + /// {/div} + /// What this method will do is remove the {div} and parse out the commented special macro syntax: {?UMBRACO_MACRO + /// macroAlias=\"myMacro\" /} + /// since this is exactly how we need to persist it to the db. + /// + public static string FormatRichTextContentForPersistence(string rteContent) + { + if (string.IsNullOrEmpty(rteContent)) + { + return string.Empty; } - /// - /// This formats the string content posted from a rich text editor that contains macro contents to be persisted. - /// - /// - /// - /// - /// This is required because when editors are using the rte, the HTML that is contained in the editor might actually be displaying - /// the entire macro content, when the data is submitted the editor will clear most of this data out but we'll still need to parse it properly - /// and ensure the correct syntax is persisted to the db. - /// - /// When a macro is inserted into the rte editor, the HTML will be: - /// - /// {div class='umb-macro-holder'} - /// - /// This could be some macro content - /// {/div} - /// - /// What this method will do is remove the {div} and parse out the commented special macro syntax: {?UMBRACO_MACRO macroAlias=\"myMacro\" /} - /// since this is exactly how we need to persist it to the db. - /// - /// - public static string FormatRichTextContentForPersistence(string rteContent) + var html = new HtmlDocument(); + html.LoadHtml(rteContent); + + // get all the comment nodes we want + HtmlNodeCollection? commentNodes = html.DocumentNode.SelectNodes("//comment()[contains(., ' with the comment node itself. - foreach (var c in commentNodes) - { - var div = c.ParentNode; - var divContainer = div.ParentNode; - divContainer.ReplaceChild(c, div); - } - - var parsed = html.DocumentNode.OuterHtml; - - //now replace all the with nothing - return MacroRteContent.Replace(parsed, match => - { - if (match.Groups.Count >= 3) - { - //get the 3rd group which is the macro syntax - return match.Groups[2].Value; - } - //replace with nothing if we couldn't find the syntax for whatever reason - return string.Empty; - }); + // There are no macros found, just return the normal content + return rteContent; } - /// - /// This will accept a text block and search/parse it for macro markup. - /// When either a text block or a a macro is found, it will call the callback method. - /// - /// - /// - /// - /// - /// - /// This method simply parses the macro contents, it does not create a string or result, - /// this is up to the developer calling this method to implement this with the callbacks. - /// - public static void ParseMacros( - string text, - Action textFoundCallback, - Action> macroFoundCallback ) + // replace each containing parent
with the comment node itself. + foreach (HtmlNode? c in commentNodes) { - if (textFoundCallback == null) throw new ArgumentNullException("textFoundCallback"); - if (macroFoundCallback == null) throw new ArgumentNullException("macroFoundCallback"); + HtmlNode? div = c.ParentNode; + HtmlNode? divContainer = div.ParentNode; + divContainer.ReplaceChild(c, div); + } - string elementText = text; + var parsed = html.DocumentNode.OuterHtml; - var fieldResult = new StringBuilder(elementText); - - //NOTE: This is legacy code, this is definitely not the correct way to do a while loop! :) - var stop = false; - while (!stop) + // now replace all the with nothing + return _macroRteContent.Replace(parsed, match => + { + if (match.Groups.Count >= 3) { - var tagIndex = fieldResult.ToString().ToLower().IndexOf(" -1) + // get the 3rd group which is the macro syntax + return match.Groups[2].Value; + } + + // replace with nothing if we couldn't find the syntax for whatever reason + return string.Empty; + }); + } + + /// + /// This will accept a text block and search/parse it for macro markup. + /// When either a text block or a a macro is found, it will call the callback method. + /// + /// + /// + /// + /// + /// + /// This method simply parses the macro contents, it does not create a string or result, + /// this is up to the developer calling this method to implement this with the callbacks. + /// + public static void ParseMacros( + string text, + Action textFoundCallback, + Action> macroFoundCallback) + { + if (textFoundCallback == null) + { + throw new ArgumentNullException("textFoundCallback"); + } + + if (macroFoundCallback == null) + { + throw new ArgumentNullException("macroFoundCallback"); + } + + var elementText = text; + + var fieldResult = new StringBuilder(elementText); + + // NOTE: This is legacy code, this is definitely not the correct way to do a while loop! :) + var stop = false; + while (!stop) + { + var tagIndex = fieldResult.ToString().ToLower().IndexOf(" -1) + { + var tempElementContent = string.Empty; + + // text block found, call the call back method + textFoundCallback(fieldResult.ToString().Substring(0, tagIndex)); + + fieldResult.Remove(0, tagIndex); + + var tag = fieldResult.ToString().Substring(0, fieldResult.ToString().IndexOf(">", StringComparison.InvariantCulture) + 1); + Dictionary attributes = XmlHelper.GetAttributesFromElement(tag); + + // Check whether it's a single tag () or a tag with children (...) + if (tag.Substring(tag.Length - 2, 1) != "/" && tag.IndexOf(" ", StringComparison.InvariantCulture) > -1) { - var tempElementContent = ""; + var closingTag = ""; - //text block found, call the call back method - textFoundCallback(fieldResult.ToString().Substring(0, tagIndex)); - - fieldResult.Remove(0, tagIndex); - - var tag = fieldResult.ToString().Substring(0, fieldResult.ToString().IndexOf(">") + 1); - var attributes = XmlHelper.GetAttributesFromElement(tag); - - // Check whether it's a single tag () or a tag with children (...) - if (tag.Substring(tag.Length - 2, 1) != "/" && tag.IndexOf(" ") > -1) + // Tag with children are only used when a macro is inserted by the umbraco-editor, in the + // following format: "", so we + // need to delete extra information inserted which is the image-tag and the closing + // umbraco_macro tag + if (fieldResult.ToString().IndexOf(closingTag, StringComparison.InvariantCulture) > -1) { - string closingTag = ""; - // Tag with children are only used when a macro is inserted by the umbraco-editor, in the - // following format: "", so we - // need to delete extra information inserted which is the image-tag and the closing - // umbraco_macro tag - if (fieldResult.ToString().IndexOf(closingTag) > -1) - { - fieldResult.Remove(0, fieldResult.ToString().IndexOf(closingTag)); - } + fieldResult.Remove(0, fieldResult.ToString().IndexOf(closingTag, StringComparison.InvariantCulture)); } - - var macroAlias = attributes.ContainsKey("macroalias") ? attributes["macroalias"] : attributes["alias"]; - - //call the callback now that we have the macro parsed - macroFoundCallback(macroAlias, attributes); - - fieldResult.Remove(0, fieldResult.ToString().IndexOf(">") + 1); - fieldResult.Insert(0, tempElementContent); } - else - { - //text block found, call the call back method - textFoundCallback(fieldResult.ToString()); - stop = true; //break; - } + var macroAlias = attributes.ContainsKey("macroalias") ? attributes["macroalias"] : attributes["alias"]; + + // call the callback now that we have the macro parsed + macroFoundCallback(macroAlias, attributes); + + fieldResult.Remove(0, fieldResult.ToString().IndexOf(">", StringComparison.InvariantCulture) + 1); + fieldResult.Insert(0, tempElementContent); + } + else + { + // text block found, call the call back method + textFoundCallback(fieldResult.ToString()); + + stop = true; // break; } } } diff --git a/src/Umbraco.Infrastructure/Mail/EmailSender.cs b/src/Umbraco.Infrastructure/Mail/EmailSender.cs index 6f94942aed..742075656d 100644 --- a/src/Umbraco.Infrastructure/Mail/EmailSender.cs +++ b/src/Umbraco.Infrastructure/Mail/EmailSender.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.IO; using System.Net.Mail; -using System.Threading.Tasks; using MailKit.Net.Smtp; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -16,147 +13,162 @@ using Umbraco.Cms.Core.Mail; using Umbraco.Cms.Core.Models.Email; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Infrastructure.Extensions; +using SecureSocketOptions = MailKit.Security.SecureSocketOptions; using SmtpClient = MailKit.Net.Smtp.SmtpClient; -namespace Umbraco.Cms.Infrastructure.Mail +namespace Umbraco.Cms.Infrastructure.Mail; + +/// +/// A utility class for sending emails +/// +public class EmailSender : IEmailSender { - /// - /// A utility class for sending emails - /// - public class EmailSender : IEmailSender + // TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly bool _notificationHandlerRegistered; + private GlobalSettings _globalSettings; + + public EmailSender( + ILogger logger, + IOptionsMonitor globalSettings, + IEventAggregator eventAggregator) + : this(logger, globalSettings, eventAggregator, null, null) { - // TODO: This should encapsulate a BackgroundTaskRunner with a queue to send these emails! - private readonly IEventAggregator _eventAggregator; - private GlobalSettings _globalSettings; - private readonly bool _notificationHandlerRegistered; - private readonly ILogger _logger; + } - public EmailSender( - ILogger logger, - IOptionsMonitor globalSettings, - IEventAggregator eventAggregator) - : this(logger, globalSettings, eventAggregator, null, null) { } + public EmailSender( + ILogger logger, + IOptionsMonitor globalSettings, + IEventAggregator eventAggregator, + INotificationHandler? handler1, + INotificationAsyncHandler? handler2) + { + _logger = logger; + _eventAggregator = eventAggregator; + _globalSettings = globalSettings.CurrentValue; + _notificationHandlerRegistered = handler1 is not null || handler2 is not null; + globalSettings.OnChange(x => _globalSettings = x); + } - public EmailSender( - ILogger logger, - IOptionsMonitor globalSettings, - IEventAggregator eventAggregator, - INotificationHandler? handler1, - INotificationAsyncHandler? handler2) + /// + /// Sends the message async + /// + /// + public async Task SendAsync(EmailMessage message, string emailType) => + await SendAsyncInternal(message, emailType, false); + + public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => + await SendAsyncInternal(message, emailType, enableNotification); + + /// + /// Returns true if the application should be able to send a required application email + /// + /// + /// We assume this is possible if either an event handler is registered or an smtp server is configured + /// or a pickup directory location is configured + /// + public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured + || _globalSettings.IsPickupDirectoryLocationConfigured + || _notificationHandlerRegistered; + + private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification) + { + if (enableNotification) { - _logger = logger; - _eventAggregator = eventAggregator; - _globalSettings = globalSettings.CurrentValue; - _notificationHandlerRegistered = handler1 is not null || handler2 is not null; - globalSettings.OnChange(x => _globalSettings = x); - } + var notification = + new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From), emailType); + await _eventAggregator.PublishAsync(notification); - /// - /// Sends the message async - /// - /// - /// - public async Task SendAsync(EmailMessage message, string emailType) => await SendAsyncInternal(message, emailType, false); - - public async Task SendAsync(EmailMessage message, string emailType, bool enableNotification) => - await SendAsyncInternal(message, emailType, enableNotification); - - private async Task SendAsyncInternal(EmailMessage message, string emailType, bool enableNotification) - { - if (enableNotification) + // if a handler handled sending the email then don't continue. + if (notification.IsHandled) { - var notification = new SendEmailNotification(message.ToNotificationEmail(_globalSettings.Smtp?.From), emailType); - await _eventAggregator.PublishAsync(notification); - - // if a handler handled sending the email then don't continue. - if (notification.IsHandled) - { - _logger.LogDebug("The email sending for {Subject} was handled by a notification handler", notification.Message.Subject); - return; - } - } - - if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) - { - _logger.LogDebug("Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", message.Subject); + _logger.LogDebug( + "The email sending for {Subject} was handled by a notification handler", + notification.Message.Subject); return; } - - if (_globalSettings.IsPickupDirectoryLocationConfigured && !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) - { - // The following code snippet is the recommended way to handle PickupDirectoryLocation. - // See more https://github.com/jstedfast/MailKit/blob/master/FAQ.md#q-how-can-i-send-email-to-a-specifiedpickupdirectory - do { - var path = Path.Combine(_globalSettings.Smtp.PickupDirectoryLocation!, Guid.NewGuid () + ".eml"); - Stream stream; - - try - { - stream = File.Open(path, FileMode.CreateNew); - } - catch (IOException) - { - if (File.Exists(path)) - { - continue; - } - throw; - } - - try { - using (stream) - { - using var filtered = new FilteredStream(stream); - filtered.Add(new SmtpDataFilter()); - - FormatOptions options = FormatOptions.Default.Clone(); - options.NewLineFormat = NewLineFormat.Dos; - - await message.ToMimeMessage(_globalSettings.Smtp.From).WriteToAsync(options, filtered); - filtered.Flush(); - return; - - } - } catch { - File.Delete(path); - throw; - } - } while (true); - } - - using var client = new SmtpClient(); - - await client.ConnectAsync(_globalSettings.Smtp!.Host, - _globalSettings.Smtp.Port, - (MailKit.Security.SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); - - if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) - { - await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); - } - - var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); - if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) - { - await client.SendAsync(mailMessage); - } - else - { - client.Send(mailMessage); - } - - await client.DisconnectAsync(true); } - /// - /// Returns true if the application should be able to send a required application email - /// - /// - /// We assume this is possible if either an event handler is registered or an smtp server is configured - /// or a pickup directory location is configured - /// - public bool CanSendRequiredEmail() => _globalSettings.IsSmtpServerConfigured - || _globalSettings.IsPickupDirectoryLocationConfigured - || _notificationHandlerRegistered; + if (!_globalSettings.IsSmtpServerConfigured && !_globalSettings.IsPickupDirectoryLocationConfigured) + { + _logger.LogDebug( + "Could not send email for {Subject}. It was not handled by a notification handler and there is no SMTP configured.", + message.Subject); + return; + } + + if (_globalSettings.IsPickupDirectoryLocationConfigured && + !string.IsNullOrWhiteSpace(_globalSettings.Smtp?.From)) + { + // The following code snippet is the recommended way to handle PickupDirectoryLocation. + // See more https://github.com/jstedfast/MailKit/blob/master/FAQ.md#q-how-can-i-send-email-to-a-specifiedpickupdirectory + do + { + var path = Path.Combine(_globalSettings.Smtp.PickupDirectoryLocation!, Guid.NewGuid() + ".eml"); + Stream stream; + + try + { + stream = File.Open(path, FileMode.CreateNew); + } + catch (IOException) + { + if (File.Exists(path)) + { + continue; + } + + throw; + } + + try + { + using (stream) + { + using var filtered = new FilteredStream(stream); + filtered.Add(new SmtpDataFilter()); + + FormatOptions options = FormatOptions.Default.Clone(); + options.NewLineFormat = NewLineFormat.Dos; + + await message.ToMimeMessage(_globalSettings.Smtp.From).WriteToAsync(options, filtered); + filtered.Flush(); + return; + } + } + catch + { + File.Delete(path); + throw; + } + } + while (true); + } + + using var client = new SmtpClient(); + + await client.ConnectAsync( + _globalSettings.Smtp!.Host, + _globalSettings.Smtp.Port, + (SecureSocketOptions)(int)_globalSettings.Smtp.SecureSocketOptions); + + if (!string.IsNullOrWhiteSpace(_globalSettings.Smtp.Username) && + !string.IsNullOrWhiteSpace(_globalSettings.Smtp.Password)) + { + await client.AuthenticateAsync(_globalSettings.Smtp.Username, _globalSettings.Smtp.Password); + } + + var mailMessage = message.ToMimeMessage(_globalSettings.Smtp.From); + if (_globalSettings.Smtp.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendAsync(mailMessage); + } + else + { + client.Send(mailMessage); + } + + await client.DisconnectAsync(true); } } diff --git a/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs b/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs index 1a34fe373c..7ef945bda8 100644 --- a/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/DashboardAccessRuleConverter.cs @@ -1,46 +1,57 @@ -using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Implements a json read converter for . +/// +internal class DashboardAccessRuleConverter : JsonReadConverter { - /// - /// Implements a json read converter for . - /// - internal class DashboardAccessRuleConverter : JsonReadConverter + /// + protected override IAccessRule Create(Type objectType, string path, JObject jObject) => new AccessRule(); + + /// + protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) { - /// - protected override IAccessRule Create(Type objectType, string path, JObject jObject) + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + if (!(target is AccessRule accessRule)) { - return new AccessRule(); + throw new PanicException("panic."); } - /// - protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer) + GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); + GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); + GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); + + if (accessRule.Type == AccessRuleType.Unknown) { - // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) - - if (!(target is AccessRule accessRule)) - throw new PanicException("panic."); - - GetRule(accessRule, jobject, "grant", AccessRuleType.Grant); - GetRule(accessRule, jobject, "deny", AccessRuleType.Deny); - GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection); - - if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined."); - } - - private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) - { - var token = jobject[name]; - if (token == null) return; - if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule."); - if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string."); - rule.Type = type; - rule.Value = token.Value(); + throw new InvalidOperationException("Rule is not defined."); } } + + private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type) + { + JToken? token = jobject[name]; + if (token == null) + { + return; + } + + if (rule.Type != AccessRuleType.Unknown) + { + throw new InvalidOperationException("Multiple definition of a rule."); + } + + if (token.Type != JTokenType.String) + { + throw new InvalidOperationException("Rule value is not a string."); + } + + rule.Type = type; + rule.Value = token.Value(); + } } diff --git a/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs b/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs index aa10cd6943..70e05c3ff5 100644 --- a/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/DataEditorConverter.cs @@ -1,8 +1,5 @@ -using System; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -11,198 +8,213 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Serialization; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Provides a json read converter for in manifests. +/// +internal class DataEditorConverter : JsonReadConverter { + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private readonly IIOHelper _ioHelper; + private readonly IJsonSerializer _jsonSerializer; + private readonly IShortStringHelper _shortStringHelper; + private readonly ILocalizedTextService _textService; + /// - /// Provides a json read converter for in manifests. + /// Initializes a new instance of the class. /// - internal class DataEditorConverter : JsonReadConverter + public DataEditorConverter( + IDataValueEditorFactory dataValueEditorFactory, + IIOHelper ioHelper, + ILocalizedTextService textService, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer) { - private readonly IDataValueEditorFactory _dataValueEditorFactory; - private readonly IIOHelper _ioHelper; - private readonly ILocalizedTextService _textService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IJsonSerializer _jsonSerializer; + _dataValueEditorFactory = dataValueEditorFactory; + _ioHelper = ioHelper; + _textService = textService; + _shortStringHelper = shortStringHelper; + _jsonSerializer = jsonSerializer; + } - /// - /// Initializes a new instance of the class. - /// - public DataEditorConverter( - IDataValueEditorFactory dataValueEditorFactory, - IIOHelper ioHelper, - ILocalizedTextService textService, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer) + /// + protected override IDataEditor Create(Type objectType, string path, JObject jobject) + { + // in PackageManifest, property editors are IConfiguredDataEditor[] whereas + // parameter editors are IDataEditor[] - both will end up here because we handle + // IDataEditor and IConfiguredDataEditor implements it, but we can check the + // type to figure out what to create + EditorType type = EditorType.PropertyValue; + + var isPropertyEditor = path.StartsWith("propertyEditors["); + + if (isPropertyEditor) { - _dataValueEditorFactory = dataValueEditorFactory; - _ioHelper = ioHelper; - _textService = textService; - _shortStringHelper = shortStringHelper; - _jsonSerializer = jsonSerializer; - } - - /// - protected override IDataEditor Create(Type objectType, string path, JObject jobject) - { - // in PackageManifest, property editors are IConfiguredDataEditor[] whereas - // parameter editors are IDataEditor[] - both will end up here because we handle - // IDataEditor and IConfiguredDataEditor implements it, but we can check the - // type to figure out what to create - - var type = EditorType.PropertyValue; - - var isPropertyEditor = path.StartsWith("propertyEditors["); - - if (isPropertyEditor) + // property editor + jobject["isPropertyEditor"] = JToken.FromObject(true); + if (jobject["isParameterEditor"] is JToken jToken && jToken.Value()) { - // property editor - jobject["isPropertyEditor"] = JToken.FromObject(true); - if (jobject["isParameterEditor"] is JToken jToken && jToken.Value()) - type |= EditorType.MacroParameter; + type |= EditorType.MacroParameter; } - else - { - // parameter editor - type = EditorType.MacroParameter; - } - - return new DataEditor(_dataValueEditorFactory, type); + } + else + { + // parameter editor + type = EditorType.MacroParameter; } - /// - protected override void Deserialize(JObject jobject, IDataEditor target, JsonSerializer serializer) + return new DataEditor(_dataValueEditorFactory, type); + } + + /// + protected override void Deserialize(JObject jobject, IDataEditor target, JsonSerializer serializer) + { + // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) + if (!(target is DataEditor dataEditor)) { - // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property) - - if (!(target is DataEditor dataEditor)) - throw new Exception("panic."); - - if (jobject["isPropertyEditor"] is JToken jtoken && jtoken.Value()) - PrepareForPropertyEditor(jobject, dataEditor); - else - PrepareForParameterEditor(jobject, dataEditor); - - base.Deserialize(jobject, target, serializer); + throw new Exception("panic."); } - private void PrepareForPropertyEditor(JObject jobject, DataEditor target) + if (jobject["isPropertyEditor"] is JToken jtoken && jtoken.Value()) { - if (jobject["editor"] == null) - throw new InvalidOperationException("Missing 'editor' value."); + PrepareForPropertyEditor(jobject, dataEditor); + } + else + { + PrepareForParameterEditor(jobject, dataEditor); + } - // explicitly assign a value editor of type ValueEditor + base.Deserialize(jobject, target, serializer); + } + + private static JArray RewriteValidators(JObject validation) + { + var jarray = new JArray(); + + foreach (KeyValuePair v in validation) + { + var key = v.Key; + JToken? val = v.Value; + var jo = new JObject { { "type", key }, { "configuration", val } }; + jarray.Add(jo); + } + + return jarray; + } + + private void PrepareForPropertyEditor(JObject jobject, DataEditor target) + { + if (jobject["editor"] == null) + { + throw new InvalidOperationException("Missing 'editor' value."); + } + + // explicitly assign a value editor of type ValueEditor + // (else the deserializer will try to read it before setting it) + // (and besides it's an interface) + target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + + // in the manifest, validators are a simple dictionary eg + // { + // required: true, + // regex: '\\d*' + // } + // and we need to turn this into a list of IPropertyValidator + // so, rewrite the json structure accordingly + if (jobject["editor"]?["validation"] is JObject validation) + { + jobject["editor"]!["validation"] = RewriteValidators(validation); + } + + if (jobject["editor"]?["view"] is JValue view) + { + jobject["editor"]!["view"] = RewriteVirtualUrl(view); + } + + var prevalues = jobject["prevalues"] as JObject; + var defaultConfig = jobject["defaultConfig"] as JObject; + if (prevalues != null || defaultConfig != null) + { + // explicitly assign a configuration editor of type ConfigurationEditor // (else the deserializer will try to read it before setting it) // (and besides it's an interface) - target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + target.ExplicitConfigurationEditor = new ConfigurationEditor(); - // in the manifest, validators are a simple dictionary eg - // { - // required: true, - // regex: '\\d*' - // } - // and we need to turn this into a list of IPropertyValidator - // so, rewrite the json structure accordingly - if (jobject["editor"]?["validation"] is JObject validation) - jobject["editor"]!["validation"] = RewriteValidators(validation); - - if(jobject["editor"]?["view"] is JValue view) - jobject["editor"]!["view"] = RewriteVirtualUrl(view); - - var prevalues = jobject["prevalues"] as JObject; - var defaultConfig = jobject["defaultConfig"] as JObject; - if (prevalues != null || defaultConfig != null) + var config = new JObject(); + if (prevalues != null) { - // explicitly assign a configuration editor of type ConfigurationEditor - // (else the deserializer will try to read it before setting it) - // (and besides it's an interface) - target.ExplicitConfigurationEditor = new ConfigurationEditor(); + config = prevalues; - var config = new JObject(); - if (prevalues != null) + // see note about validators, above - same applies to field validators + if (config["fields"] is JArray jarray) { - config = prevalues; - // see note about validators, above - same applies to field validators - if (config["fields"] is JArray jarray) + foreach (JToken field in jarray) { - foreach (var field in jarray) + if (field["validation"] is JObject fvalidation) { - if (field["validation"] is JObject fvalidation) - field["validation"] = RewriteValidators(fvalidation); + field["validation"] = RewriteValidators(fvalidation); + } - if(field["view"] is JValue fview) - field["view"] = RewriteVirtualUrl(fview); + if (field["view"] is JValue fview) + { + field["view"] = RewriteVirtualUrl(fview); } } } - - // in the manifest, default configuration is at editor level - // move it down to configuration editor level so it can be deserialized properly - if (defaultConfig != null) - { - config["defaultConfig"] = defaultConfig; - jobject.Remove("defaultConfig"); - } - - // in the manifest, configuration is named 'prevalues', rename - // it is important to do this LAST - jobject["config"] = config; - jobject.Remove("prevalues"); } + + // in the manifest, default configuration is at editor level + // move it down to configuration editor level so it can be deserialized properly + if (defaultConfig != null) + { + config["defaultConfig"] = defaultConfig; + jobject.Remove("defaultConfig"); + } + + // in the manifest, configuration is named 'prevalues', rename + // it is important to do this LAST + jobject["config"] = config; + jobject.Remove("prevalues"); + } + } + + private string? RewriteVirtualUrl(JValue view) => _ioHelper.ResolveRelativeOrVirtualUrl(view.Value as string); + + private void PrepareForParameterEditor(JObject jobject, DataEditor target) + { + // in a manifest, a parameter editor looks like: + // + // { + // "alias": "...", + // "name": "...", + // "view": "...", + // "config": { "key1": "value1", "key2": "value2" ... } + // } + // + // the view is at top level, but should be down one level to be properly + // deserialized as a ParameterValueEditor property -> need to move it + if (jobject.Property("view") != null) + { + // explicitly assign a value editor of type ParameterValueEditor + target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); + + // move the 'view' property + jobject["editor"] = new JObject { ["view"] = jobject["view"] }; + jobject.Property("view")?.Remove(); } - private string? RewriteVirtualUrl(JValue view) + // in the manifest, default configuration is named 'config', rename + if (jobject["config"] is JObject config) { - return _ioHelper.ResolveRelativeOrVirtualUrl(view.Value as string); + jobject["defaultConfig"] = config; + jobject.Remove("config"); } - private void PrepareForParameterEditor(JObject jobject, DataEditor target) + // We need to null check, if view do not exists, then editor do not exists + if (jobject["editor"]?["view"] is JValue view) { - // in a manifest, a parameter editor looks like: - // - // { - // "alias": "...", - // "name": "...", - // "view": "...", - // "config": { "key1": "value1", "key2": "value2" ... } - // } - // - // the view is at top level, but should be down one level to be properly - // deserialized as a ParameterValueEditor property -> need to move it - - if (jobject.Property("view") != null) - { - // explicitly assign a value editor of type ParameterValueEditor - target.ExplicitValueEditor = new DataValueEditor(_textService, _shortStringHelper, _jsonSerializer); - - // move the 'view' property - jobject["editor"] = new JObject { ["view"] = jobject["view"] }; - jobject.Property("view")?.Remove(); - } - - // in the manifest, default configuration is named 'config', rename - if (jobject["config"] is JObject config) - { - jobject["defaultConfig"] = config; - jobject.Remove("config"); - } - - if(jobject["editor"]?["view"] is JValue view) // We need to null check, if view do not exists, then editor do not exists - jobject["editor"]!["view"] = RewriteVirtualUrl(view); - } - - private static JArray RewriteValidators(JObject validation) - { - var jarray = new JArray(); - - foreach (var v in validation) - { - var key = v.Key; - var val = v.Value; - var jo = new JObject { { "type", key }, { "configuration", val } }; - jarray.Add(jo); - } - - return jarray; + jobject["editor"]!["view"] = RewriteVirtualUrl(view); } } } diff --git a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs index bdf5e5c620..4dbd6abd40 100644 --- a/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs +++ b/src/Umbraco.Infrastructure/Manifest/ManifestParser.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -14,248 +10,246 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Parses the Main.js file and replaces all tokens accordingly. +/// +public class ManifestParser : IManifestParser { + private static readonly string _utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); + + private readonly IAppPolicyCache _cache; + private readonly IDataValueEditorFactory _dataValueEditorFactory; + private readonly ManifestFilterCollection _filters; + private readonly IHostingEnvironment _hostingEnvironment; + + private readonly IIOHelper _ioHelper; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILocalizedTextService _localizedTextService; + private readonly ILogger _logger; + private readonly IShortStringHelper _shortStringHelper; + private readonly ManifestValueValidatorCollection _validators; + + private string _path = null!; + /// - /// Parses the Main.js file and replaces all tokens accordingly. + /// Initializes a new instance of the class. /// - public class ManifestParser : IManifestParser + public ManifestParser( + AppCaches appCaches, + ManifestValueValidatorCollection validators, + ManifestFilterCollection filters, + ILogger logger, + IIOHelper ioHelper, + IHostingEnvironment hostingEnvironment, + IJsonSerializer jsonSerializer, + ILocalizedTextService localizedTextService, + IShortStringHelper shortStringHelper, + IDataValueEditorFactory dataValueEditorFactory) { - - private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IJsonSerializer _jsonSerializer; - private readonly ILocalizedTextService _localizedTextService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IDataValueEditorFactory _dataValueEditorFactory; - private static readonly string s_utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble()); - - private readonly IAppPolicyCache _cache; - private readonly ILogger _logger; - private readonly ManifestValueValidatorCollection _validators; - private readonly ManifestFilterCollection _filters; - - private string _path = null!; - - /// - /// Initializes a new instance of the class. - /// - public ManifestParser( - AppCaches appCaches, - ManifestValueValidatorCollection validators, - ManifestFilterCollection filters, - ILogger logger, - IIOHelper ioHelper, - IHostingEnvironment hostingEnvironment, - IJsonSerializer jsonSerializer, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - IDataValueEditorFactory dataValueEditorFactory) + if (appCaches == null) { - if (appCaches == null) throw new ArgumentNullException(nameof(appCaches)); - _cache = appCaches.RuntimeCache; - _validators = validators ?? throw new ArgumentNullException(nameof(validators)); - _filters = filters ?? throw new ArgumentNullException(nameof(filters)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - AppPluginsPath = "~/App_Plugins"; - _jsonSerializer = jsonSerializer; - _localizedTextService = localizedTextService; - _shortStringHelper = shortStringHelper; - _dataValueEditorFactory = dataValueEditorFactory; + throw new ArgumentNullException(nameof(appCaches)); } - public string AppPluginsPath + _cache = appCaches.RuntimeCache; + _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + _filters = filters ?? throw new ArgumentNullException(nameof(filters)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _ioHelper = ioHelper; + _hostingEnvironment = hostingEnvironment; + AppPluginsPath = "~/App_Plugins"; + _jsonSerializer = jsonSerializer; + _localizedTextService = localizedTextService; + _shortStringHelper = shortStringHelper; + _dataValueEditorFactory = dataValueEditorFactory; + } + + public string AppPluginsPath + { + get => _path; + set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; + } + + /// + /// Gets all manifests, merged into a single manifest object. + /// + /// + public CompositePackageManifest CombinedManifest + => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => { - get => _path; - set => _path = value.StartsWith("~/") ? _hostingEnvironment.MapPathContentRoot(value) : value; + IEnumerable manifests = GetManifests(); + return MergeManifests(manifests); + }, new TimeSpan(0, 4, 0))!; + + /// + /// Gets all manifests. + /// + public IEnumerable GetManifests() + { + var manifests = new List(); + + foreach (var path in GetManifestFiles()) + { + try + { + var text = File.ReadAllText(path); + text = TrimPreamble(text); + if (string.IsNullOrWhiteSpace(text)) + { + continue; + } + + PackageManifest manifest = ParseManifest(text); + manifest.Source = path; + manifests.Add(manifest); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to parse manifest at '{Path}', ignoring.", path); + } } - /// - /// Gets all manifests, merged into a single manifest object. - /// - /// - public CompositePackageManifest CombinedManifest - => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () => - { - IEnumerable manifests = GetManifests(); - return MergeManifests(manifests); + _filters.Filter(manifests); - }, new TimeSpan(0, 4, 0))!; + return manifests; + } - /// - /// Gets all manifests. - /// - public IEnumerable GetManifests() + /// + /// Parses a manifest. + /// + public PackageManifest ParseManifest(string text) + { + if (text == null) { - var manifests = new List(); + throw new ArgumentNullException(nameof(text)); + } - foreach (var path in GetManifestFiles()) + if (string.IsNullOrWhiteSpace(text)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); + } + + PackageManifest? manifest = JsonConvert.DeserializeObject( + text, + new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer), + new ValueValidatorConverter(_validators), + new DashboardAccessRuleConverter()); + + // scripts and stylesheets are raw string, must process here + for (var i = 0; i < manifest!.Scripts.Length; i++) + { + manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; + } + + for (var i = 0; i < manifest.Stylesheets.Length; i++) + { + manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; + } + + foreach (ManifestContentAppDefinition contentApp in manifest.ContentApps) + { + contentApp.View = _ioHelper.ResolveRelativeOrVirtualUrl(contentApp.View); + } + + foreach (ManifestDashboard dashboard in manifest.Dashboards) + { + dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; + } + + foreach (GridEditor gridEditor in manifest.GridEditors) + { + gridEditor.View = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.View); + gridEditor.Render = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.Render); + } + + // add property editors that are also parameter editors, to the parameter editors list + // (the manifest format is kinda legacy) + var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); + if (ppEditors.Count > 0) + { + manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); + } + + return manifest; + } + + /// + /// Merges all manifests into one. + /// + private static CompositePackageManifest MergeManifests(IEnumerable manifests) + { + var scripts = new Dictionary>(); + var stylesheets = new Dictionary>(); + var propertyEditors = new List(); + var parameterEditors = new List(); + var gridEditors = new List(); + var contentApps = new List(); + var dashboards = new List(); + var sections = new List(); + + foreach (PackageManifest manifest in manifests) + { + if (!scripts.TryGetValue(manifest.BundleOptions, out List? scriptsPerBundleOption)) { - try - { - var text = File.ReadAllText(path); - text = TrimPreamble(text); - if (string.IsNullOrWhiteSpace(text)) - { - continue; - } - - PackageManifest manifest = ParseManifest(text); - manifest.Source = path; - manifests.Add(manifest); - } - catch (Exception e) - { - _logger.LogError(e, "Failed to parse manifest at '{Path}', ignoring.", path); - } + scriptsPerBundleOption = new List(); + scripts[manifest.BundleOptions] = scriptsPerBundleOption; } - _filters.Filter(manifests); + scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); - return manifests; + if (!stylesheets.TryGetValue(manifest.BundleOptions, out List? stylesPerBundleOption)) + { + stylesPerBundleOption = new List(); + stylesheets[manifest.BundleOptions] = stylesPerBundleOption; + } + + stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); + + propertyEditors.AddRange(manifest.PropertyEditors); + + parameterEditors.AddRange(manifest.ParameterEditors); + + gridEditors.AddRange(manifest.GridEditors); + + contentApps.AddRange(manifest.ContentApps); + + dashboards.AddRange(manifest.Dashboards); + + sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias, StringComparer.OrdinalIgnoreCase)); } - /// - /// Merges all manifests into one. - /// - private static CompositePackageManifest MergeManifests(IEnumerable manifests) + return new CompositePackageManifest( + propertyEditors, + parameterEditors, + gridEditors, + contentApps, + dashboards, + sections, + scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), + stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); + } + + private static string TrimPreamble(string text) + { + // strangely StartsWith(preamble) would always return true + if (text.Substring(0, 1) == _utf8Preamble) { - var scripts = new Dictionary>(); - var stylesheets = new Dictionary>(); - var propertyEditors = new List(); - var parameterEditors = new List(); - var gridEditors = new List(); - var contentApps = new List(); - var dashboards = new List(); - var sections = new List(); - - foreach (PackageManifest manifest in manifests) - { - if (manifest.Scripts != null) - { - if (!scripts.TryGetValue(manifest.BundleOptions, out List? scriptsPerBundleOption)) - { - scriptsPerBundleOption = new List(); - scripts[manifest.BundleOptions] = scriptsPerBundleOption; - } - - scriptsPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Scripts)); - } - - if (manifest.Stylesheets != null) - { - if (!stylesheets.TryGetValue(manifest.BundleOptions, out List? stylesPerBundleOption)) - { - stylesPerBundleOption = new List(); - stylesheets[manifest.BundleOptions] = stylesPerBundleOption; - } - - stylesPerBundleOption.Add(new ManifestAssets(manifest.PackageName, manifest.Stylesheets)); - } - - if (manifest.PropertyEditors != null) - { - propertyEditors.AddRange(manifest.PropertyEditors); - } - - if (manifest.ParameterEditors != null) - { - parameterEditors.AddRange(manifest.ParameterEditors); - } - - if (manifest.GridEditors != null) - { - gridEditors.AddRange(manifest.GridEditors); - } - - if (manifest.ContentApps != null) - { - contentApps.AddRange(manifest.ContentApps); - } - - if (manifest.Dashboards != null) - { - dashboards.AddRange(manifest.Dashboards); - } - - if (manifest.Sections != null) - { - sections.AddRange(manifest.Sections.DistinctBy(x => x.Alias, StringComparer.OrdinalIgnoreCase)); - } - } - - return new CompositePackageManifest( - propertyEditors, - parameterEditors, - gridEditors, - contentApps, - dashboards, - sections, - scripts.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value), - stylesheets.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value)); + text = text.Remove(0, _utf8Preamble.Length); } - // gets all manifest files (recursively) - private IEnumerable GetManifestFiles() + return text; + } + + // gets all manifest files (recursively) + private IEnumerable GetManifestFiles() + { + if (Directory.Exists(_path) == false) { - if (Directory.Exists(_path) == false) - { - return Array.Empty(); - } - - return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); + return Array.Empty(); } - private static string TrimPreamble(string text) - { - // strangely StartsWith(preamble) would always return true - if (text.Substring(0, 1) == s_utf8Preamble) - text = text.Remove(0, s_utf8Preamble.Length); - - return text; - } - - /// - /// Parses a manifest. - /// - public PackageManifest ParseManifest(string text) - { - if (text == null) throw new ArgumentNullException(nameof(text)); - if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text)); - - var manifest = JsonConvert.DeserializeObject(text, - new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer), - new ValueValidatorConverter(_validators), - new DashboardAccessRuleConverter()); - - // scripts and stylesheets are raw string, must process here - for (var i = 0; i < manifest!.Scripts.Length; i++) - manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!; - for (var i = 0; i < manifest.Stylesheets.Length; i++) - manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!; - foreach (var contentApp in manifest.ContentApps) - { - contentApp.View = _ioHelper.ResolveRelativeOrVirtualUrl(contentApp.View); - } - foreach (var dashboard in manifest.Dashboards) - { - dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!; - } - foreach (var gridEditor in manifest.GridEditors) - { - gridEditor.View = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.View); - gridEditor.Render = _ioHelper.ResolveRelativeOrVirtualUrl(gridEditor.Render); - } - - // add property editors that are also parameter editors, to the parameter editors list - // (the manifest format is kinda legacy) - var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList(); - if (ppEditors.Count > 0) - manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray(); - - return manifest; - } + return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories); } } diff --git a/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs b/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs index 6d6483a8bb..64f81e7697 100644 --- a/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs +++ b/src/Umbraco.Infrastructure/Manifest/ValueValidatorConverter.cs @@ -1,34 +1,31 @@ -using System; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Manifest +namespace Umbraco.Cms.Core.Manifest; + +/// +/// Implements a json read converter for . +/// +internal class ValueValidatorConverter : JsonReadConverter { + private readonly ManifestValueValidatorCollection _validators; + /// - /// Implements a json read converter for . + /// Initializes a new instance of the class. /// - internal class ValueValidatorConverter : JsonReadConverter + public ValueValidatorConverter(ManifestValueValidatorCollection validators) => _validators = validators; + + protected override IValueValidator Create(Type objectType, string path, JObject jObject) { - private readonly ManifestValueValidatorCollection _validators; - - /// - /// Initializes a new instance of the class. - /// - public ValueValidatorConverter(ManifestValueValidatorCollection validators) + var type = jObject["type"]?.Value(); + if (string.IsNullOrWhiteSpace(type)) { - _validators = validators; + throw new InvalidOperationException("Could not get the type of the validator."); } - protected override IValueValidator Create(Type objectType, string path, JObject jObject) - { - var type = jObject["type"]?.Value(); - if (string.IsNullOrWhiteSpace(type)) - throw new InvalidOperationException("Could not get the type of the validator."); + return _validators.GetByName(type); - return _validators.GetByName(type); - - // jObject["configuration"] is going to be deserialized in a Configuration property, if any - } + // jObject["configuration"] is going to be deserialized in a Configuration property, if any } } diff --git a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs index 7a73e9303f..09cfcf5aaf 100644 --- a/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Infrastructure/Mapping/UmbracoMapper.cs @@ -1,492 +1,560 @@ -using System; using System.Collections; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Scoping; -using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Mapping +namespace Umbraco.Cms.Core.Mapping; + +// notes: +// AutoMapper maps null to empty arrays, lists, etc + +// TODO: +// when mapping from TSource, and no map is found, consider the actual source.GetType()? +// when mapping to TTarget, and no map is found, consider the actual target.GetType()? +// not sure we want to add magic to this simple mapper class, though + +/// +/// Umbraco Mapper. +/// +/// +/// +/// When a map is defined from TSource to TTarget, the mapper automatically knows how to map +/// from IEnumerable{TSource} to IEnumerable{TTarget} (using a List{TTarget}) and to TTarget[]. +/// +/// +/// When a map is defined from TSource to TTarget, the mapper automatically uses that map +/// for any source type that inherits from, or implements, TSource. +/// +/// +/// When a map is defined from TSource to TTarget, the mapper can map to TTarget exclusively +/// and cannot re-use that map for types that would inherit from, or implement, TTarget. +/// +/// +/// When using the Map{TSource, TTarget}(TSource source, ...) overloads, TSource is explicit. When +/// using the Map{TTarget}(object source, ...) TSource is defined as source.GetType(). +/// +/// In both cases, TTarget is explicit and not typeof(target). +/// +public class UmbracoMapper : IUmbracoMapper { - // notes: - // AutoMapper maps null to empty arrays, lists, etc + // note + // + // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary + // the inner dictionaries are never modified and therefore can be simple Dictionary + private readonly ConcurrentDictionary>> _ctors = + new(); - // TODO: - // when mapping from TSource, and no map is found, consider the actual source.GetType()? - // when mapping to TTarget, and no map is found, consider the actual target.GetType()? - // not sure we want to add magic to this simple mapper class, though + private readonly ConcurrentDictionary>> _maps = + new(); + + private readonly ICoreScopeProvider _scopeProvider; /// - /// Umbraco Mapper. + /// Initializes a new instance of the class. /// - /// - /// When a map is defined from TSource to TTarget, the mapper automatically knows how to map - /// from IEnumerable{TSource} to IEnumerable{TTarget} (using a List{TTarget}) and to TTarget[]. - /// When a map is defined from TSource to TTarget, the mapper automatically uses that map - /// for any source type that inherits from, or implements, TSource. - /// When a map is defined from TSource to TTarget, the mapper can map to TTarget exclusively - /// and cannot re-use that map for types that would inherit from, or implement, TTarget. - /// When using the Map{TSource, TTarget}(TSource source, ...) overloads, TSource is explicit. When - /// using the Map{TTarget}(object source, ...) TSource is defined as source.GetType(). - /// In both cases, TTarget is explicit and not typeof(target). - /// - public class UmbracoMapper : IUmbracoMapper + /// + /// + public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) { - // note - // - // the outer dictionary *can* be modified, see GetCtor and GetMap, hence have to be ConcurrentDictionary - // the inner dictionaries are never modified and therefore can be simple Dictionary + _scopeProvider = scopeProvider; - private readonly ConcurrentDictionary>> _ctors - = new ConcurrentDictionary>>(); - - private readonly ConcurrentDictionary>> _maps - = new ConcurrentDictionary>>(); - - private readonly ICoreScopeProvider _scopeProvider; - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public UmbracoMapper(MapDefinitionCollection profiles, ICoreScopeProvider scopeProvider) + foreach (IMapDefinition profile in profiles) { - _scopeProvider = scopeProvider; + profile.DefineMaps(this); + } + } - foreach (var profile in profiles) - profile.DefineMaps(this); + #region Define + + private static TTarget ThrowCtor(TSource source, MapperContext context) + => throw new InvalidOperationException($"Don't know how to create {typeof(TTarget).FullName} instances."); + + private static void Identity(TSource source, TTarget target, MapperContext context) + { + } + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + public void Define() + => Define(ThrowCtor, Identity); + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A mapping method. + public void Define(Action map) + => Define(ThrowCtor, map); + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + public void Define(Func ctor) + => Define(ctor, Identity); + + /// + /// Defines a mapping. + /// + /// The source type. + /// The target type. + /// A constructor method. + /// A mapping method. + public void Define( + Func ctor, + Action map) + { + Type sourceType = typeof(TSource); + Type targetType = typeof(TTarget); + + Dictionary> sourceCtors = DefineCtors(sourceType); + if (ctor != null) + { + sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; } - #region Define + Dictionary> sourceMaps = DefineMaps(sourceType); + sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); + } - private static TTarget ThrowCtor(TSource source, MapperContext context) - => throw new InvalidOperationException($"Don't know how to create {typeof(TTarget).FullName} instances."); + private Dictionary> DefineCtors(Type sourceType) => + _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); - private static void Identity(TSource source, TTarget target, MapperContext context) - { } + private Dictionary> DefineMaps(Type sourceType) => + _maps.GetOrAdd(sourceType, _ => new Dictionary>()); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - public void Define() - => Define(ThrowCtor, Identity); + #endregion - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A mapping method. - public void Define(Action map) - => Define(ThrowCtor, map); + #region Map - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - public void Define(Func ctor) - => Define(ctor, Identity); + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(object? source) + => Map(source, new MapperContext(this)); - /// - /// Defines a mapping. - /// - /// The source type. - /// The target type. - /// A constructor method. - /// A mapping method. - public void Define(Func ctor, Action map) + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget? Map(object? source, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, context); + } + + /// + /// Maps a source object to a new target object. + /// + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + public TTarget? Map(object? source, MapperContext context) + => Map(source, source?.GetType(), context); + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + public TTarget? Map(TSource? source) + => Map(source, new MapperContext(this)); + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context preparation method. + /// The target object. + public TTarget? Map(TSource source, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, context); + } + + /// + /// Maps a source object to a new target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// A mapper context. + /// The target object. + public TTarget? Map(TSource? source, MapperContext context) + => Map(source, typeof(TSource), context); + + private TTarget? Map(object? source, Type? sourceType, MapperContext context) + { + if (source == null) { - var sourceType = typeof(TSource); - var targetType = typeof(TTarget); - - var sourceCtors = DefineCtors(sourceType); - if (ctor != null) - sourceCtors[targetType] = (source, context) => ctor((TSource)source, context)!; - - var sourceMaps = DefineMaps(sourceType); - sourceMaps[targetType] = (source, target, context) => map((TSource)source, (TTarget)target, context); + return default; } - private Dictionary> DefineCtors(Type sourceType) + Type targetType = typeof(TTarget); + + Func? ctor = GetCtor(sourceType, targetType); + Action? map = GetMap(sourceType, targetType); + + // if there is a direct constructor, map + if (ctor != null && map != null) { - return _ctors.GetOrAdd(sourceType, _ => new Dictionary>()); + var target = ctor(source, context); + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + { + map(source, target, context); + } + + return (TTarget)target; } - private Dictionary> DefineMaps(Type sourceType) + // otherwise, see if we can deal with enumerable + Type? ienumerableOfT = typeof(IEnumerable<>); + + bool IsIEnumerableOfT(Type? type) { - return _maps.GetOrAdd(sourceType, _ => new Dictionary>()); + return type is not null && + type.IsGenericType && + type.GenericTypeArguments.Length == 1 && + type.GetGenericTypeDefinition() == ienumerableOfT; } - #endregion + // try to get source as an IEnumerable + Type? sourceIEnumerable = IsIEnumerableOfT(sourceType) + ? sourceType + : sourceType?.GetInterfaces().FirstOrDefault(IsIEnumerableOfT); - #region Map - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(object? source) - => Map(source, new MapperContext(this)); - - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget? Map(object? source, Action f) + // if source is an IEnumerable and target is T[] or IEnumerable, we can create a map + if (sourceIEnumerable != null && IsEnumerableOrArrayOfType(targetType)) { - var context = new MapperContext(this); - f(context); - return Map(source, context); - } + Type sourceGenericArg = sourceIEnumerable.GenericTypeArguments[0]; + Type? targetGenericArg = GetEnumerableOrArrayTypeArgument(targetType); - /// - /// Maps a source object to a new target object. - /// - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - public TTarget? Map(object? source, MapperContext context) - => Map(source, source?.GetType(), context); + ctor = GetCtor(sourceGenericArg, targetGenericArg); + map = GetMap(sourceGenericArg, targetGenericArg); - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - public TTarget? Map(TSource? source) - => Map(source, new MapperContext(this)); - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context preparation method. - /// The target object. - public TTarget? Map(TSource source, Action f) - { - var context = new MapperContext(this); - f(context); - return Map(source, context); - } - - /// - /// Maps a source object to a new target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// A mapper context. - /// The target object. - public TTarget? Map(TSource? source, MapperContext context) - => Map(source, typeof(TSource), context); - - private TTarget? Map(object? source, Type? sourceType, MapperContext context) - { - if (source == null) - return default; - - var targetType = typeof(TTarget); - - var ctor = GetCtor(sourceType, targetType); - var map = GetMap(sourceType, targetType); - - // if there is a direct constructor, map + // if there is a constructor for the underlying type, create & invoke the map if (ctor != null && map != null) { - var target = ctor(source, context); - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + // register (for next time) and do it now (for this time) + object NCtor(object s, MapperContext c) { - map(source, target, context); - } - return (TTarget)target; - } - - // otherwise, see if we can deal with enumerable - - var ienumerableOfT = typeof(IEnumerable<>); - - bool IsIEnumerableOfT(Type? type) => - type is not null && - type.IsGenericType && - type.GenericTypeArguments.Length == 1 && - type.GetGenericTypeDefinition() == ienumerableOfT; - - // try to get source as an IEnumerable - var sourceIEnumerable = IsIEnumerableOfT(sourceType) ? sourceType : sourceType?.GetInterfaces().FirstOrDefault(IsIEnumerableOfT); - - // if source is an IEnumerable and target is T[] or IEnumerable, we can create a map - if (sourceIEnumerable != null && IsEnumerableOrArrayOfType(targetType)) - { - var sourceGenericArg = sourceIEnumerable.GenericTypeArguments[0]; - var targetGenericArg = GetEnumerableOrArrayTypeArgument(targetType); - - ctor = GetCtor(sourceGenericArg, targetGenericArg); - map = GetMap(sourceGenericArg, targetGenericArg); - - // if there is a constructor for the underlying type, create & invoke the map - if (ctor != null && map != null) - { - // register (for next time) and do it now (for this time) - object NCtor(object s, MapperContext c) => MapEnumerableInternal((IEnumerable)s, targetGenericArg!, ctor, map, c)!; - DefineCtors(sourceType!)[targetType] = NCtor; - DefineMaps(sourceType!)[targetType] = Identity; - return (TTarget)NCtor(source, context); + return MapEnumerableInternal((IEnumerable)s, targetGenericArg!, ctor, map, c)!; } - throw new InvalidOperationException($"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg?.FullName}, so don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + DefineCtors(sourceType!)[targetType] = NCtor; + DefineMaps(sourceType!)[targetType] = Identity; + return (TTarget)NCtor(source, context); } - throw new InvalidOperationException($"Don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + throw new InvalidOperationException( + $"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg?.FullName}, so don't know how to map {sourceType?.FullName} to {targetType.FullName}."); } - private TTarget? MapEnumerableInternal(IEnumerable source, Type targetGenericArg, Func ctor, Action map, MapperContext context) + throw new InvalidOperationException($"Don't know how to map {sourceType?.FullName} to {targetType.FullName}."); + } + + private TTarget? MapEnumerableInternal( + IEnumerable source, + Type targetGenericArg, + Func ctor, + Action map, + MapperContext context) + { + var targetList = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); + + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - var targetList = (IList?)Activator.CreateInstance(typeof(List<>).MakeGenericType(targetGenericArg)); - - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) + foreach (var sourceItem in source) { - foreach (var sourceItem in source) - { - var targetItem = ctor(sourceItem, context); - map(sourceItem, targetItem, context); - targetList?.Add(targetItem); - } + var targetItem = ctor(sourceItem, context); + map(sourceItem, targetItem, context); + targetList?.Add(targetItem); } - - object? target = targetList; - - if (typeof(TTarget).IsArray) - { - var elementType = typeof(TTarget).GetElementType(); - if (elementType == null) throw new PanicException("elementType == null which should never occur"); - var targetArray = Array.CreateInstance(elementType, targetList?.Count ?? 0); - targetList?.CopyTo(targetArray, 0); - target = targetArray; - } - - return (TTarget?)target; } - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// The target object. - public TTarget Map(TSource source, TTarget target) - => Map(source, target, new MapperContext(this)); + object? target = targetList; - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context preparation method. - /// The target object. - public TTarget Map(TSource source, TTarget target, Action f) + if (typeof(TTarget).IsArray) { - var context = new MapperContext(this); - f(context); - return Map(source, target, context); + Type? elementType = typeof(TTarget).GetElementType(); + if (elementType == null) + { + throw new PanicException("elementType == null which should never occur"); + } + + var targetArray = Array.CreateInstance(elementType, targetList?.Count ?? 0); + targetList?.CopyTo(targetArray, 0); + target = targetArray; } - /// - /// Maps a source object to an existing target object. - /// - /// The source type. - /// The target type. - /// The source object. - /// The target object. - /// A mapper context. - /// The target object. - public TTarget Map(TSource source, TTarget target, MapperContext context) + return (TTarget?)target; + } + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// The target object. + public TTarget Map(TSource source, TTarget target) + => Map(source, target, new MapperContext(this)); + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context preparation method. + /// The target object. + public TTarget Map(TSource source, TTarget target, Action f) + { + var context = new MapperContext(this); + f(context); + return Map(source, target, context); + } + + /// + /// Maps a source object to an existing target object. + /// + /// The source type. + /// The target type. + /// The source object. + /// The target object. + /// A mapper context. + /// The target object. + public TTarget Map(TSource source, TTarget target, MapperContext context) + { + Type sourceType = typeof(TSource); + Type targetType = typeof(TTarget); + + Action? map = GetMap(sourceType, targetType); + + // if there is a direct map, map + if (map != null) { - var sourceType = typeof(TSource); - var targetType = typeof(TTarget); - - var map = GetMap(sourceType, targetType); - - // if there is a direct map, map - if (map != null) + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - using (var scope = _scopeProvider.CreateCoreScope(autoComplete: true)) - { - map(source!, target!, context); - } - return target; + map(source!, target!, context); } - // we cannot really map to an existing enumerable - give up - - throw new InvalidOperationException($"Don't know how to map {typeof(TSource).FullName} to {typeof(TTarget).FullName}."); + return target; } - private Func? GetCtor(Type? sourceType, Type? targetType) + // we cannot really map to an existing enumerable - give up + throw new InvalidOperationException( + $"Don't know how to map {typeof(TSource).FullName} to {typeof(TTarget).FullName}."); + } + + private Func? GetCtor(Type? sourceType, Type? targetType) + { + if (sourceType is null || targetType is null) { - if (sourceType is null || targetType is null) - { - return null; - } - if (_ctors.TryGetValue(sourceType, out var sourceCtor) && sourceCtor.TryGetValue(targetType, out var ctor)) - return ctor; - - // we *may* run this more than once but it does not matter - - ctor = null; - foreach (var (stype, sctors) in _ctors) - { - if (!stype.IsAssignableFrom(sourceType)) continue; - if (!sctors.TryGetValue(targetType, out ctor)) continue; - - sourceCtor = sctors; - break; - } - - if (ctor is null || sourceCtor is null) return null; - - _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => - { - // Add missing constructors - foreach (var c in sourceCtor) - { - if (!v.ContainsKey(c.Key)) - { - v.Add(c.Key, c.Value); - } - } - - return v; - }); - + return null; + } + if (_ctors.TryGetValue(sourceType, out Dictionary>? sourceCtor) && + sourceCtor.TryGetValue(targetType, out Func? ctor)) + { return ctor; } - private Action? GetMap(Type? sourceType, Type? targetType) + // we *may* run this more than once but it does not matter + ctor = null; + foreach ((Type stype, Dictionary> sctors) in _ctors) { - if (sourceType is null || targetType is null) + if (!stype.IsAssignableFrom(sourceType)) { - return null; - } - if (_maps.TryGetValue(sourceType, out var sourceMap) && sourceMap.TryGetValue(targetType, out var map)) - return map; - - // we *may* run this more than once but it does not matter - - map = null; - foreach (var (stype, smap) in _maps) - { - if (!stype.IsAssignableFrom(sourceType)) continue; - - // TODO: consider looking for assignable types for target too? - if (!smap.TryGetValue(targetType, out map)) continue; - - sourceMap = smap; - break; + continue; } - if (map is null || sourceMap is null) return null; - - if (_maps.ContainsKey(sourceType)) + if (!sctors.TryGetValue(targetType, out ctor)) { - foreach (var m in sourceMap) + continue; + } + + sourceCtor = sctors; + break; + } + + if (ctor is null || sourceCtor is null) + { + return null; + } + + _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => + { + // Add missing constructors + foreach (KeyValuePair> c in sourceCtor) + { + if (!v.ContainsKey(c.Key)) { - if (!_maps[sourceType].TryGetValue(m.Key, out _)) - _maps[sourceType].Add(m.Key, m.Value); + v.Add(c.Key, c.Value); } } - else - _maps[sourceType] = sourceMap; + return v; + }); + + return ctor; + } + + private Action? GetMap(Type? sourceType, Type? targetType) + { + if (sourceType is null || targetType is null) + { + return null; + } + + if (_maps.TryGetValue(sourceType, out Dictionary>? sourceMap) && + sourceMap.TryGetValue(targetType, out Action? map)) + { return map; } - private static bool IsEnumerableOrArrayOfType(Type type) + // we *may* run this more than once but it does not matter + map = null; + foreach ((Type stype, Dictionary> smap) in _maps) { - if (type.IsArray && type.GetArrayRank() == 1) return true; - if (type.IsGenericType && type.GenericTypeArguments.Length == 1) return true; - return false; + if (!stype.IsAssignableFrom(sourceType)) + { + continue; + } + + // TODO: consider looking for assignable types for target too? + if (!smap.TryGetValue(targetType, out map)) + { + continue; + } + + sourceMap = smap; + break; } - private static Type? GetEnumerableOrArrayTypeArgument(Type type) + if (map is null || sourceMap is null) { - if (type.IsArray) return type.GetElementType(); - if (type.IsGenericType) return type.GenericTypeArguments[0]; - throw new PanicException($"Could not get enumerable or array type from {type}"); + return null; } - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source) + if (_maps.ContainsKey(sourceType)) { - return source - .Select(Map) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); + foreach (KeyValuePair> m in sourceMap) + { + if (!_maps[sourceType].TryGetValue(m.Key, out _)) + { + _maps[sourceType].Add(m.Key, m.Value); + } + } + } + else + { + _maps[sourceType] = sourceMap; } - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context preparation method. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source, Action f) - { - var context = new MapperContext(this); - f(context); - return source - .Select(x => Map(x, context)) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); - } - - /// - /// Maps an enumerable of source objects to a new list of target objects. - /// - /// The type of the source objects. - /// The type of the target objects. - /// The source objects. - /// A mapper context. - /// A list containing the target objects. - public List MapEnumerable(IEnumerable source, MapperContext context) - { - return source - .Select(x => Map(x, context)) - .Where(x => x is not null) - .Select(x => x!) - .ToList(); - } - - #endregion + return map; } + + private static bool IsEnumerableOrArrayOfType(Type type) + { + if (type.IsArray && type.GetArrayRank() == 1) + { + return true; + } + + if (type.IsGenericType && type.GenericTypeArguments.Length == 1) + { + return true; + } + + return false; + } + + private static Type? GetEnumerableOrArrayTypeArgument(Type type) + { + if (type.IsArray) + { + return type.GetElementType(); + } + + if (type.IsGenericType) + { + return type.GenericTypeArguments[0]; + } + + throw new PanicException($"Could not get enumerable or array type from {type}"); + } + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source) => + source + .Select(Map) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context preparation method. + /// A list containing the target objects. + public List MapEnumerable( + IEnumerable source, + Action f) + { + var context = new MapperContext(this); + f(context); + return source + .Select(x => Map(x, context)) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + } + + /// + /// Maps an enumerable of source objects to a new list of target objects. + /// + /// The type of the source objects. + /// The type of the target objects. + /// The source objects. + /// A mapper context. + /// A list containing the target objects. + public List MapEnumerable(IEnumerable source, MapperContext context) => + source + .Select(x => Map(x, context)) + .Where(x => x is not null) + .Select(x => x!) + .ToList(); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs index 7881daa593..fbc2add152 100644 --- a/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs +++ b/src/Umbraco.Infrastructure/Media/ImageSharpDimensionExtractor.cs @@ -1,71 +1,66 @@ -using System; -using System.IO; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using Umbraco.Cms.Core.Media; using Size = System.Drawing.Size; -namespace Umbraco.Cms.Infrastructure.Media +namespace Umbraco.Cms.Infrastructure.Media; + +internal class ImageSharpDimensionExtractor : IImageDimensionExtractor { - internal class ImageSharpDimensionExtractor : IImageDimensionExtractor + private readonly Configuration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public ImageSharpDimensionExtractor(Configuration configuration) + => _configuration = configuration; + + /// + /// Gets the dimensions of an image. + /// + /// A stream containing the image bytes. + /// + /// The dimension of the image. + /// + public Size? GetDimensions(Stream? stream) { - private readonly Configuration _configuration; + Size? size = null; - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - public ImageSharpDimensionExtractor(Configuration configuration) - => _configuration = configuration; - - /// - /// Gets the dimensions of an image. - /// - /// A stream containing the image bytes. - /// - /// The dimension of the image. - /// - public Size? GetDimensions(Stream? stream) + IImageInfo imageInfo = Image.Identify(_configuration, stream); + if (imageInfo != null) { - Size? size = null; - - IImageInfo imageInfo = Image.Identify(_configuration, stream); - if (imageInfo != null) - { - size = IsExifOrientationRotated(imageInfo) - ? new Size(imageInfo.Height, imageInfo.Width) - : new Size(imageInfo.Width, imageInfo.Height); - } - - return size; + size = IsExifOrientationRotated(imageInfo) + ? new Size(imageInfo.Height, imageInfo.Width) + : new Size(imageInfo.Width, imageInfo.Height); } - private static bool IsExifOrientationRotated(IImageInfo imageInfo) - => GetExifOrientation(imageInfo) switch - { - ExifOrientationMode.LeftTop + return size; + } + + private static bool IsExifOrientationRotated(IImageInfo imageInfo) + => GetExifOrientation(imageInfo) switch + { + ExifOrientationMode.LeftTop or ExifOrientationMode.RightTop or ExifOrientationMode.RightBottom or ExifOrientationMode.LeftBottom => true, - _ => false, - }; + _ => false, + }; - private static ushort GetExifOrientation(IImageInfo imageInfo) + private static ushort GetExifOrientation(IImageInfo imageInfo) + { + IExifValue? orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); + if (orientation is not null) { - IExifValue? orientation = imageInfo.Metadata.ExifProfile?.GetValue(ExifTag.Orientation); - if (orientation is not null) + if (orientation.DataType == ExifDataType.Short) { - if (orientation.DataType == ExifDataType.Short) - { - return orientation.Value; - } - else - { - return Convert.ToUInt16(orientation.Value); - } + return orientation.Value; } - return ExifOrientationMode.Unknown; + return Convert.ToUInt16(orientation.Value); } + + return ExifOrientationMode.Unknown; } } diff --git a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs index 9979da1e40..2e99770874 100644 --- a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs @@ -1,18 +1,17 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +public class ExecutedMigrationPlan { - public class ExecutedMigrationPlan + public ExecutedMigrationPlan(MigrationPlan plan, string initialState, string finalState) { - public ExecutedMigrationPlan(MigrationPlan plan, string initialState, string finalState) - { - Plan = plan; - InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState)); - FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); - } - - public MigrationPlan Plan { get; } - public string InitialState { get; } - public string FinalState { get; } + Plan = plan; + InitialState = initialState ?? throw new ArgumentNullException(nameof(initialState)); + FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); } + + public MigrationPlan Plan { get; } + + public string InitialState { get; } + + public string FinalState { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs index fec6b5d0c1..5340eab203 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/AlterBuilder.cs @@ -1,25 +1,21 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter; + +/// +/// Implements . +/// +public class AlterBuilder : IAlterBuilder { - /// - /// Implements . - /// - public class AlterBuilder : IAlterBuilder + private readonly IMigrationContext _context; + + public AlterBuilder(IMigrationContext context) => _context = context; + + /// + public IAlterTableBuilder Table(string tableName) { - private readonly IMigrationContext _context; - - public AlterBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IAlterTableBuilder Table(string tableName) - { - var expression = new AlterTableExpression(_context) { TableName = tableName }; - return new AlterTableBuilder(_context, expression); - } + var expression = new AlterTableExpression(_context) { TableName = tableName }; + return new AlterTableBuilder(_context, expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs index 6d1bfe4561..f24810c62b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterColumnExpression.cs @@ -1,25 +1,22 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterColumnExpression : MigrationExpressionBase { - public class AlterColumnExpression : MigrationExpressionBase - { + public AlterColumnExpression(IMigrationContext context) + : base(context) => + Column = new ColumnDefinition { ModificationType = ModificationType.Alter }; - public AlterColumnExpression(IMigrationContext context) - : base(context) - { - Column = new ColumnDefinition { ModificationType = ModificationType.Alter }; - } + public virtual string? SchemaName { get; set; } - public virtual string? SchemaName { get; set; } - public virtual string? TableName { get; set; } - public virtual ColumnDefinition Column { get; set; } + public virtual string? TableName { get; set; } - protected override string GetSql() - { - return string.Format(SqlSyntax.AlterColumn, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.Format(Column)); - } - } + public virtual ColumnDefinition Column { get; set; } + + protected override string GetSql() => + string.Format( + SqlSyntax.AlterColumn, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.Format(Column)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs index 18298c7378..00bf8bbb3b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterDefaultConstraintExpression.cs @@ -1,26 +1,25 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterDefaultConstraintExpression : MigrationExpressionBase { - public class AlterDefaultConstraintExpression : MigrationExpressionBase + public AlterDefaultConstraintExpression(IMigrationContext context) + : base(context) { - public AlterDefaultConstraintExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - - public virtual string? ColumnName { get; set; } - - public virtual string? ConstraintName { get; set; } - - public virtual object? DefaultValue { get; set; } - - protected override string GetSql() - { - //NOTE Should probably investigate if Deleting a Default Constraint is different from deleting a 'regular' constraint - - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.GetQuotedName(ConstraintName)); - } } + + public virtual string? TableName { get; set; } + + public virtual string? ColumnName { get; set; } + + public virtual string? ConstraintName { get; set; } + + public virtual object? DefaultValue { get; set; } + + protected override string GetSql() => + + // NOTE Should probably investigate if Deleting a Default Constraint is different from deleting a 'regular' constraint + string.Format( + SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedName(ConstraintName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs index 9be5354590..2787553ab4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Expressions/AlterTableExpression.cs @@ -1,16 +1,13 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; + +public class AlterTableExpression : MigrationExpressionBase { - public class AlterTableExpression : MigrationExpressionBase + public AlterTableExpression(IMigrationContext context) + : base(context) { - public AlterTableExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - - protected override string GetSql() - { - return string.Empty; - } } + + public virtual string? TableName { get; set; } + + protected override string GetSql() => string.Empty; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs index 7f3bf080d4..b64c82dec8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/IAlterBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter; + +/// +/// Builds an Alter expression. +/// +public interface IAlterBuilder : IFluentBuilder { /// - /// Builds an Alter expression. + /// Specifies the table to alter. /// - public interface IAlterBuilder : IFluentBuilder - { - /// - /// Specifies the table to alter. - /// - IAlterTableBuilder Table(string tableName); - } + IAlterTableBuilder Table(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs index 199db34102..3dc5483266 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs @@ -1,279 +1,251 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public class AlterTableBuilder : ExpressionBuilderBase, + IAlterTableColumnTypeBuilder, + IAlterTableColumnOptionForeignKeyCascadeBuilder { - public class AlterTableBuilder : ExpressionBuilderBase, - IAlterTableColumnTypeBuilder, - IAlterTableColumnOptionForeignKeyCascadeBuilder + private readonly IMigrationContext _context; + + public AlterTableBuilder(IMigrationContext context, AlterTableExpression expression) + : base(expression) => + _context = context; + + public ColumnDefinition CurrentColumn { get; set; } = null!; + + public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + + public void Do() => Expression.Execute(); + + public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) { - private readonly IMigrationContext _context; + CurrentColumn.DefaultValue = method; + return this; + } - public AlterTableBuilder(IMigrationContext context, AlterTableExpression expression) - : base(expression) + public IAlterTableColumnOptionBuilder WithDefaultValue(object value) + { + if (CurrentColumn.ModificationType == ModificationType.Alter) { - _context = context; - } - - public void Do() => Expression.Execute(); - - public ColumnDefinition CurrentColumn { get; set; } = null!; - - public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; - - public override ColumnDefinition GetColumnForType() - { - return CurrentColumn; - } - - public IAlterTableColumnOptionBuilder WithDefault(SystemMethods method) - { - CurrentColumn.DefaultValue = method; - return this; - } - - public IAlterTableColumnOptionBuilder WithDefaultValue(object value) - { - if (CurrentColumn.ModificationType == ModificationType.Alter) + var dc = new AlterDefaultConstraintExpression(_context) { - var dc = new AlterDefaultConstraintExpression(_context) - { - TableName = Expression.TableName, - ColumnName = CurrentColumn.Name, - DefaultValue = value - }; - - Expression.Expressions.Add(dc); - } - - CurrentColumn.DefaultValue = value; - return this; - } - - public IAlterTableColumnOptionBuilder Identity() - { - CurrentColumn.IsIdentity = true; - return this; - } - - public IAlterTableColumnOptionBuilder Indexed() - { - return Indexed(null); - } - - public IAlterTableColumnOptionBuilder Indexed(string? indexName) - { - CurrentColumn.IsIndexed = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - TableName = Expression.TableName - }); - - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); - - Expression.Expressions.Add(index); - - return this; - } - - public IAlterTableColumnOptionBuilder PrimaryKey() - { - CurrentColumn.IsPrimaryKey = true; - - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - TableName = Expression.TableName, - Columns = new[] { CurrentColumn.Name } - } + TableName = Expression.TableName, + ColumnName = CurrentColumn.Name, + DefaultValue = value, }; - Expression.Expressions.Add(expression); - return this; + Expression.Expressions.Add(dc); } - public IAlterTableColumnOptionBuilder PrimaryKey(string primaryKeyName) - { - CurrentColumn.IsPrimaryKey = true; - CurrentColumn.PrimaryKeyName = primaryKeyName; + CurrentColumn.DefaultValue = value; + return this; + } - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + public IAlterTableColumnOptionBuilder Identity() + { + CurrentColumn.IsIdentity = true; + return this; + } + + public IAlterTableColumnOptionBuilder Indexed() => Indexed(null); + + public IAlterTableColumnOptionBuilder Indexed(string? indexName) + { + CurrentColumn.IsIndexed = true; + + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName }); + + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); + + Expression.Expressions.Add(index); + + return this; + } + + public IAlterTableColumnOptionBuilder PrimaryKey() + { + CurrentColumn.IsPrimaryKey = true; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { TableName = Expression.TableName, Columns = new[] { CurrentColumn.Name } }, + }; + Expression.Expressions.Add(expression); + + return this; + } + + public IAlterTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + CurrentColumn.IsPrimaryKey = true; + CurrentColumn.PrimaryKeyName = primaryKeyName; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { - Constraint = - { - ConstraintName = primaryKeyName, - TableName = Expression.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); + ConstraintName = primaryKeyName, + TableName = Expression.TableName, + Columns = new[] { CurrentColumn.Name } + }, + }; + Expression.Expressions.Add(expression); - return this; - } + return this; + } - public IAlterTableColumnOptionBuilder Nullable() - { - CurrentColumn.IsNullable = true; - return this; - } + public IAlterTableColumnOptionBuilder Nullable() + { + CurrentColumn.IsNullable = true; + return this; + } - public IAlterTableColumnOptionBuilder NotNullable() - { - CurrentColumn.IsNullable = false; - return this; - } + public IAlterTableColumnOptionBuilder NotNullable() + { + CurrentColumn.IsNullable = false; + return this; + } - public IAlterTableColumnOptionBuilder Unique() - { - return Unique(null); - } + public IAlterTableColumnOptionBuilder Unique() => Unique(null); - public IAlterTableColumnOptionBuilder Unique(string? indexName) - { - CurrentColumn.IsUnique = true; + public IAlterTableColumnOptionBuilder Unique(string? indexName) + { + CurrentColumn.IsUnique = true; - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered + IndexType = IndexTypes.UniqueNonClustered, }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder + ForeignKey(string primaryTableName, string primaryColumnName) => + ForeignKey(null, null, primaryTableName, primaryColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - CurrentColumn.IsForeignKey = true; + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey( + string? foreignKeyName, + string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + CurrentColumn.IsForeignKey = true; - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, PrimaryTableSchema = primaryTableSchema, - ForeignTable = Expression.TableName + ForeignTable = Expression.TableName, }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - CurrentColumn.IsForeignKey = true; - return this; - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + CurrentColumn.IsForeignKey = true; + return this; + } - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string foreignTableName, + string foreignColumnName) => ReferencedBy(null, null, foreignTableName, foreignColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + public IAlterTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string? foreignKeyName, + string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, ForeignTable = foreignTableName, - ForeignTableSchema = foreignTableSchema + ForeignTableSchema = foreignTableSchema, }); - fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } - - public IAlterTableColumnTypeBuilder AddColumn(string name) - { - var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Create }; - var createColumn = new CreateColumnExpression(_context) - { - Column = column, - TableName = Expression.TableName - }; - - CurrentColumn = column; - - Expression.Expressions.Add(createColumn); - return this; - } - - public IAlterTableColumnTypeBuilder AlterColumn(string name) - { - var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Alter }; - var alterColumn = new AlterColumnExpression(_context) - { - Column = column, - TableName = Expression.TableName - }; - - CurrentColumn = column; - - Expression.Expressions.Add(alterColumn); - return this; - } - - public IAlterTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - CurrentForeignKey.OnDelete = rule; - return this; - } - - public IAlterTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - CurrentForeignKey.OnUpdate = rule; - return this; - } - - public IAlterTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; } + + public IAlterTableColumnTypeBuilder AddColumn(string name) + { + var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Create }; + var createColumn = new CreateColumnExpression(_context) { Column = column, TableName = Expression.TableName }; + + CurrentColumn = column; + + Expression.Expressions.Add(createColumn); + return this; + } + + public IAlterTableColumnTypeBuilder AlterColumn(string name) + { + var column = new ColumnDefinition { Name = name, ModificationType = ModificationType.Alter }; + var alterColumn = new AlterColumnExpression(_context) { Column = column, TableName = Expression.TableName }; + + CurrentColumn = column; + + Expression.Expressions.Add(alterColumn); + return this; + } + + public IAlterTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + CurrentForeignKey.OnDelete = rule; + return this; + } + + public IAlterTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + CurrentForeignKey.OnUpdate = rule; + return this; + } + + public IAlterTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; + } + + public override ColumnDefinition GetColumnForType() => CurrentColumn; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs index 642e71757a..9d30d28a38 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableBuilder.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +/// +/// Builds an Alter Table expression. +/// +public interface IAlterTableBuilder : IFluentBuilder { /// - /// Builds an Alter Table expression. + /// Specifies a column to add. /// - public interface IAlterTableBuilder : IFluentBuilder - { - /// - /// Specifies a column to add. - /// - IAlterTableColumnTypeBuilder AddColumn(string name); + IAlterTableColumnTypeBuilder AddColumn(string name); - /// - /// Specifies a column to alter. - /// - IAlterTableColumnTypeBuilder AlterColumn(string name); - } + /// + /// Specifies a column to alter. + /// + IAlterTableColumnTypeBuilder AlterColumn(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs index 3ace421b7b..8d05199507 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionBuilder.cs @@ -1,8 +1,10 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnOptionBuilder : + IColumnOptionBuilder, + IAlterTableBuilder, + IExecutableBuilder { - public interface IAlterTableColumnOptionBuilder : IColumnOptionBuilder, - IAlterTableBuilder, IExecutableBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs index e42fcb266d..e76b4d726e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnOptionForeignKeyCascadeBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnOptionForeignKeyCascadeBuilder : + IAlterTableColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface IAlterTableColumnOptionForeignKeyCascadeBuilder : - IAlterTableColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs index 4768b52e7f..90c446468a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Alter/Table/IAlterTableColumnTypeBuilder.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Alter.Table; + +public interface IAlterTableColumnTypeBuilder : IColumnTypeBuilder { - public interface IAlterTableColumnTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs index 5ec8c200e0..a388a971b4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/ExecutableBuilder.cs @@ -1,15 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public class ExecutableBuilder : IExecutableBuilder { - public class ExecutableBuilder : IExecutableBuilder - { - private readonly IMigrationExpression _expression; + private readonly IMigrationExpression _expression; - public ExecutableBuilder(IMigrationExpression expression) - { - _expression = expression; - } + public ExecutableBuilder(IMigrationExpression expression) => _expression = expression; - /// - public void Do() => _expression.Execute(); - } + /// + public void Do() => _expression.Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs index 8e701f845e..f539f9ec7d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateColumnExpression.cs @@ -1,26 +1,27 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateColumnExpression : MigrationExpressionBase { - public class CreateColumnExpression : MigrationExpressionBase + public CreateColumnExpression(IMigrationContext context) + : base(context) => + Column = new ColumnDefinition { ModificationType = ModificationType.Create }; + + public string? TableName { get; set; } + + public ColumnDefinition Column { get; set; } + + protected override string GetSql() { - public CreateColumnExpression(IMigrationContext context) - : base(context) + if (string.IsNullOrEmpty(Column.TableName)) { - Column = new ColumnDefinition { ModificationType = ModificationType.Create }; + Column.TableName = TableName; } - public string? TableName { get; set; } - public ColumnDefinition Column { get; set; } - - protected override string GetSql() - { - if (string.IsNullOrEmpty(Column.TableName)) - Column.TableName = TableName; - - return string.Format(SqlSyntax.AddColumn, - SqlSyntax.GetQuotedTableName(Column.TableName), - SqlSyntax.Format(Column)); - } + return string.Format( + SqlSyntax.AddColumn, + SqlSyntax.GetQuotedTableName(Column.TableName), + SqlSyntax.Format(Column)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs index 511f4ac634..c012455e53 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateForeignKeyExpression.cs @@ -1,26 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateForeignKeyExpression : MigrationExpressionBase { - public class CreateForeignKeyExpression : MigrationExpressionBase - { - public CreateForeignKeyExpression(IMigrationContext context, ForeignKeyDefinition fkDef) - : base(context) - { - ForeignKey = fkDef; - } + public CreateForeignKeyExpression(IMigrationContext context, ForeignKeyDefinition fkDef) + : base(context) => + ForeignKey = fkDef; - public CreateForeignKeyExpression(IMigrationContext context) - : base(context) - { - ForeignKey = new ForeignKeyDefinition(); - } + public CreateForeignKeyExpression(IMigrationContext context) + : base(context) => + ForeignKey = new ForeignKeyDefinition(); - public ForeignKeyDefinition ForeignKey { get; set; } + public ForeignKeyDefinition ForeignKey { get; set; } - protected override string GetSql() - { - return SqlSyntax.Format(ForeignKey); - } - } + protected override string GetSql() => SqlSyntax.Format(ForeignKey); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs index cef5a8387a..837e805d49 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/Expressions/CreateIndexExpression.cs @@ -1,27 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; + +public class CreateIndexExpression : MigrationExpressionBase { - public class CreateIndexExpression : MigrationExpressionBase - { + public CreateIndexExpression(IMigrationContext context, IndexDefinition index) + : base(context) => + Index = index; - public CreateIndexExpression(IMigrationContext context, IndexDefinition index) - : base(context) - { - Index = index; - } + public CreateIndexExpression(IMigrationContext context) + : base(context) => + Index = new IndexDefinition(); - public CreateIndexExpression(IMigrationContext context) - : base(context) - { - Index = new IndexDefinition(); - } + public IndexDefinition Index { get; set; } - public IndexDefinition Index { get; set; } - - protected override string GetSql() - { - return SqlSyntax.Format(Index); - } - } + protected override string GetSql() => SqlSyntax.Format(Index); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs index 10057c0f6f..a1374f735f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnOptionBuilder.cs @@ -1,31 +1,46 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IColumnOptionBuilder : IFluentBuilder + where TNext : IFluentBuilder + where TNextFk : IFluentBuilder { - public interface IColumnOptionBuilder : IFluentBuilder - where TNext : IFluentBuilder - where TNextFk : IFluentBuilder - { - TNext WithDefault(SystemMethods method); - TNext WithDefaultValue(object value); - TNext Identity(); - TNext Indexed(); - TNext Indexed(string indexName); + TNext WithDefault(SystemMethods method); - TNext PrimaryKey(); - TNext PrimaryKey(string primaryKeyName); - TNext Nullable(); - TNext NotNullable(); - TNext Unique(); - TNext Unique(string indexName); + TNext WithDefaultValue(object value); - TNextFk ForeignKey(string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(string foreignKeyName, string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(string foreignKeyName, string primaryTableSchema, string primaryTableName, string primaryColumnName); - TNextFk ForeignKey(); + TNext Identity(); - TNextFk ReferencedBy(string foreignTableName, string foreignColumnName); - TNextFk ReferencedBy(string foreignKeyName, string foreignTableName, string foreignColumnName); - TNextFk ReferencedBy(string foreignKeyName, string foreignTableSchema, string foreignTableName, string foreignColumnName); - } + TNext Indexed(); + + TNext Indexed(string indexName); + + TNext PrimaryKey(); + + TNext PrimaryKey(string primaryKeyName); + + TNext Nullable(); + + TNext NotNullable(); + + TNext Unique(); + + TNext Unique(string indexName); + + TNextFk ForeignKey(string primaryTableName, string primaryColumnName); + + TNextFk ForeignKey(string foreignKeyName, string primaryTableName, string primaryColumnName); + + TNextFk ForeignKey(string foreignKeyName, string primaryTableSchema, string primaryTableName, + string primaryColumnName); + + TNextFk ForeignKey(); + + TNextFk ReferencedBy(string foreignTableName, string foreignColumnName); + + TNextFk ReferencedBy(string foreignKeyName, string foreignTableName, string foreignColumnName); + + TNextFk ReferencedBy(string foreignKeyName, string foreignTableSchema, string foreignTableName, + string foreignColumnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs index 75d5512cee..6f910fcc7d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IColumnTypeBuilder.cs @@ -1,35 +1,58 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +/// +/// Builds a column type expression. +/// +public interface IColumnTypeBuilder : IFluentBuilder + where TNext : IFluentBuilder { - /// - /// Builds a column type expression. - /// - public interface IColumnTypeBuilder : IFluentBuilder - where TNext : IFluentBuilder - { - TNext AsAnsiString(); - TNext AsAnsiString(int size); - TNext AsBinary(); - TNext AsBinary(int size); - TNext AsBoolean(); - TNext AsByte(); - TNext AsCurrency(); - TNext AsDate(); - TNext AsDateTime(); - TNext AsDecimal(); - TNext AsDecimal(int size, int precision); - TNext AsDouble(); - TNext AsGuid(); - TNext AsFixedLengthString(int size); - TNext AsFixedLengthAnsiString(int size); - TNext AsFloat(); - TNext AsInt16(); - TNext AsInt32(); - TNext AsInt64(); - TNext AsString(); - TNext AsString(int size); - TNext AsTime(); - TNext AsXml(); - TNext AsXml(int size); - TNext AsCustom(string customType); - } + TNext AsAnsiString(); + + TNext AsAnsiString(int size); + + TNext AsBinary(); + + TNext AsBinary(int size); + + TNext AsBoolean(); + + TNext AsByte(); + + TNext AsCurrency(); + + TNext AsDate(); + + TNext AsDateTime(); + + TNext AsDecimal(); + + TNext AsDecimal(int size, int precision); + + TNext AsDouble(); + + TNext AsGuid(); + + TNext AsFixedLengthString(int size); + + TNext AsFixedLengthAnsiString(int size); + + TNext AsFloat(); + + TNext AsInt16(); + + TNext AsInt32(); + + TNext AsInt64(); + + TNext AsString(); + + TNext AsString(int size); + + TNext AsTime(); + + TNext AsXml(); + + TNext AsXml(int size); + + TNext AsCustom(string customType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs index b5a29d801b..bff6789be9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IExecutableBuilder.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IExecutableBuilder { - public interface IExecutableBuilder - { - /// - /// Executes. - /// - void Do(); - } + /// + /// Executes. + /// + void Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs index f566e5c4bb..07388a9156 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Common/IForeignKeyCascadeBuilder.cs @@ -1,24 +1,23 @@ -using System.Data; +using System.Data; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; + +public interface IForeignKeyCascadeBuilder : IFluentBuilder + where TNext : IFluentBuilder + where TNextFk : IFluentBuilder { - public interface IForeignKeyCascadeBuilder : IFluentBuilder - where TNext : IFluentBuilder - where TNextFk : IFluentBuilder - { - /// - /// Specifies a rule on deletes. - /// - TNextFk OnDelete(Rule rule); + /// + /// Specifies a rule on deletes. + /// + TNextFk OnDelete(Rule rule); - /// - /// Specifies a rule on updates. - /// - TNextFk OnUpdate(Rule rule); + /// + /// Specifies a rule on updates. + /// + TNextFk OnUpdate(Rule rule); - /// - /// Specifies a rule on deletes and updates. - /// - TNext OnDeleteOrUpdate(Rule rule); - } + /// + /// Specifies a rule on deletes and updates. + /// + TNext OnDeleteOrUpdate(Rule rule); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs index 07c11c57cd..90ae100b3a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs @@ -1,224 +1,200 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public class CreateColumnBuilder : ExpressionBuilderBase, + ICreateColumnOnTableBuilder, + ICreateColumnTypeBuilder, + ICreateColumnOptionForeignKeyCascadeBuilder { - public class CreateColumnBuilder : ExpressionBuilderBase, - ICreateColumnOnTableBuilder, - ICreateColumnTypeBuilder, - ICreateColumnOptionForeignKeyCascadeBuilder + private readonly IMigrationContext _context; + + public CreateColumnBuilder(IMigrationContext context, CreateColumnExpression expression) + : base(expression) => + _context = context; + + public ForeignKeyDefinition? CurrentForeignKey { get; set; } + + public ICreateColumnTypeBuilder OnTable(string name) { - private readonly IMigrationContext _context; + Expression.TableName = name; + return this; + } - public CreateColumnBuilder(IMigrationContext context, CreateColumnExpression expression) - : base(expression) - { - _context = context; - } + public void Do() => Expression.Execute(); - public void Do() => Expression.Execute(); + public ICreateColumnOptionBuilder WithDefault(SystemMethods method) + { + Expression.Column.DefaultValue = method; + return this; + } - public ForeignKeyDefinition? CurrentForeignKey { get; set; } + public ICreateColumnOptionBuilder WithDefaultValue(object value) + { + Expression.Column.DefaultValue = value; + return this; + } - public override ColumnDefinition GetColumnForType() - { - return Expression.Column; - } + public ICreateColumnOptionBuilder Identity() => Indexed(null); - public ICreateColumnTypeBuilder OnTable(string name) - { - Expression.TableName = name; - return this; - } + public ICreateColumnOptionBuilder Indexed() => Indexed(null); - public ICreateColumnOptionBuilder WithDefault(SystemMethods method) - { - Expression.Column.DefaultValue = method; - return this; - } + public ICreateColumnOptionBuilder Indexed(string? indexName) + { + Expression.Column.IsIndexed = true; - public ICreateColumnOptionBuilder WithDefaultValue(object value) - { - Expression.Column.DefaultValue = value; - return this; - } + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName }); - public ICreateColumnOptionBuilder Identity() - { - return Indexed(null); - } + index.Index.Columns.Add(new IndexColumnDefinition { Name = Expression.Column.Name }); - public ICreateColumnOptionBuilder Indexed() - { - return Indexed(null); - } + Expression.Expressions.Add(index); - public ICreateColumnOptionBuilder Indexed(string? indexName) - { - Expression.Column.IsIndexed = true; + return this; + } - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - TableName = Expression.TableName - }); + public ICreateColumnOptionBuilder PrimaryKey() + { + Expression.Column.IsPrimaryKey = true; + return this; + } - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = Expression.Column.Name - }); + public ICreateColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + Expression.Column.IsPrimaryKey = true; + Expression.Column.PrimaryKeyName = primaryKeyName; + return this; + } - Expression.Expressions.Add(index); + public ICreateColumnOptionBuilder Nullable() + { + Expression.Column.IsNullable = true; + return this; + } - return this; - } + public ICreateColumnOptionBuilder NotNullable() + { + Expression.Column.IsNullable = false; + return this; + } - public ICreateColumnOptionBuilder PrimaryKey() - { - Expression.Column.IsPrimaryKey = true; - return this; - } + public ICreateColumnOptionBuilder Unique() => Unique(null); - public ICreateColumnOptionBuilder PrimaryKey(string primaryKeyName) - { - Expression.Column.IsPrimaryKey = true; - Expression.Column.PrimaryKeyName = primaryKeyName; - return this; - } + public ICreateColumnOptionBuilder Unique(string? indexName) + { + Expression.Column.IsUnique = true; - public ICreateColumnOptionBuilder Nullable() - { - Expression.Column.IsNullable = true; - return this; - } - - public ICreateColumnOptionBuilder NotNullable() - { - Expression.Column.IsNullable = false; - return this; - } - - public ICreateColumnOptionBuilder Unique() - { - return Unique(null); - } - - public ICreateColumnOptionBuilder Unique(string? indexName) - { - Expression.Column.IsUnique = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered + IndexType = IndexTypes.UniqueNonClustered, }); - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = Expression.Column.Name - }); + index.Index.Columns.Add(new IndexColumnDefinition { Name = Expression.Column.Name }); - Expression.Expressions.Add(index); + Expression.Expressions.Add(index); - return this; - } + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) => + ForeignKey(null, null, primaryTableName, primaryColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - Expression.Column.IsForeignKey = true; + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + Expression.Column.IsForeignKey = true; - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, PrimaryTableSchema = primaryTableSchema, - ForeignTable = Expression.TableName + ForeignTable = Expression.TableName, }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(Expression.Column.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(Expression.Column.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - Expression.Column.IsForeignKey = true; - return this; - } + public ICreateColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + Expression.Column.IsForeignKey = true; + return this; + } - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder + ReferencedBy(string foreignTableName, string foreignColumnName) => + ReferencedBy(null, null, foreignTableName, foreignColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + public ICreateColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, ForeignTable = foreignTableName, - ForeignTableSchema = foreignTableSchema + ForeignTableSchema = foreignTableSchema, }); - fk.ForeignKey.PrimaryColumns.Add(Expression.Column.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(Expression.Column.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } - - public ICreateColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - if (CurrentForeignKey is not null) - { - CurrentForeignKey.OnDelete = rule; - } - - return this; - } - - public ICreateColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - if (CurrentForeignKey is not null) - { - CurrentForeignKey.OnUpdate = rule; - } - - return this; - } - - public ICreateColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; } + + public ICreateColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + if (CurrentForeignKey is not null) + { + CurrentForeignKey.OnDelete = rule; + } + + return this; + } + + public ICreateColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + if (CurrentForeignKey is not null) + { + CurrentForeignKey.OnUpdate = rule; + } + + return this; + } + + public ICreateColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; + } + + public override ColumnDefinition GetColumnForType() => Expression.Column; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs index 982b495ac8..c6b515554b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOnTableBuilder.cs @@ -1,12 +1,11 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOnTableBuilder : IColumnTypeBuilder { - public interface ICreateColumnOnTableBuilder : IColumnTypeBuilder - { - /// - /// Specifies the name of the table. - /// - ICreateColumnTypeBuilder OnTable(string name); - } + /// + /// Specifies the name of the table. + /// + ICreateColumnTypeBuilder OnTable(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs index 9a4c2c647e..573fb46c95 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionBuilder.cs @@ -1,8 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOptionBuilder : + IColumnOptionBuilder, + IExecutableBuilder { - public interface ICreateColumnOptionBuilder : IColumnOptionBuilder - , IExecutableBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs index 25e0d792c4..3bc5c1a66f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnOptionForeignKeyCascadeBuilder.cs @@ -1,8 +1,8 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnOptionForeignKeyCascadeBuilder : ICreateColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface ICreateColumnOptionForeignKeyCascadeBuilder : ICreateColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs index f1177efad3..42379df6fc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Column/ICreateColumnTypeBuilder.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; + +public interface ICreateColumnTypeBuilder : IColumnTypeBuilder { - public interface ICreateColumnTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs index f61d99f237..a7d16bfe11 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/CreateConstraintBuilder.cs @@ -1,36 +1,39 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public class CreateConstraintBuilder : ExpressionBuilderBase, + ICreateConstraintOnTableBuilder, + ICreateConstraintColumnsBuilder { - public class CreateConstraintBuilder : ExpressionBuilderBase, - ICreateConstraintOnTableBuilder, - ICreateConstraintColumnsBuilder + public CreateConstraintBuilder(CreateConstraintExpression expression) + : base(expression) { - public CreateConstraintBuilder(CreateConstraintExpression expression) - : base(expression) - { } + } - /// - public ICreateConstraintColumnsBuilder OnTable(string tableName) - { - Expression.Constraint.TableName = tableName; - return this; - } + /// + public IExecutableBuilder Column(string columnName) + { + Expression.Constraint.Columns.Add(columnName); + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder Column(string columnName) + /// + public IExecutableBuilder Columns(string[] columnNames) + { + foreach (var columnName in columnNames) { Expression.Constraint.Columns.Add(columnName); - return new ExecutableBuilder(Expression); } - /// - public IExecutableBuilder Columns(string[] columnNames) - { - foreach (var columnName in columnNames) - Expression.Constraint.Columns.Add(columnName); - return new ExecutableBuilder(Expression); - } + return new ExecutableBuilder(Expression); + } + + /// + public ICreateConstraintColumnsBuilder OnTable(string tableName) + { + Expression.Constraint.TableName = tableName; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs index cfc7568686..979d58f98a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintColumnsBuilder.cs @@ -1,17 +1,16 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public interface ICreateConstraintColumnsBuilder : IFluentBuilder { - public interface ICreateConstraintColumnsBuilder : IFluentBuilder - { - /// - /// Specifies the constraint column. - /// - IExecutableBuilder Column(string columnName); + /// + /// Specifies the constraint column. + /// + IExecutableBuilder Column(string columnName); - /// - /// Specifies the constraint columns. - /// - IExecutableBuilder Columns(string[] columnNames); - } + /// + /// Specifies the constraint columns. + /// + IExecutableBuilder Columns(string[] columnNames); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs index 01d2da0cd1..dc14f5789c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Constraint/ICreateConstraintOnTableBuilder.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; + +public interface ICreateConstraintOnTableBuilder : IFluentBuilder { - public interface ICreateConstraintOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table name. - /// - ICreateConstraintColumnsBuilder OnTable(string tableName); - } + /// + /// Specifies the table name. + /// + ICreateConstraintColumnsBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs index b672b1e5d4..2ba0c435f1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/CreateBuilder.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; @@ -10,121 +9,112 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create; + +public class CreateBuilder : ICreateBuilder { - public class CreateBuilder : ICreateBuilder + private readonly IMigrationContext _context; + + public CreateBuilder(IMigrationContext context) => + _context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + public IExecutableBuilder Table(bool withoutKeysAndIndexes = false) => + new CreateTableOfDtoBuilder(_context) { TypeOfDto = typeof(TDto), WithoutKeysAndIndexes = withoutKeysAndIndexes }; + + /// + public IExecutableBuilder KeysAndIndexes() => + new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeof(TDto) }; + + /// + public IExecutableBuilder KeysAndIndexes(Type typeOfDto) => + new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeOfDto }; + + /// + public ICreateTableWithColumnBuilder Table(string tableName) { - private readonly IMigrationContext _context; + var expression = new CreateTableExpression(_context) { TableName = tableName }; + return new CreateTableBuilder(_context, expression); + } - public CreateBuilder(IMigrationContext context) - { - _context = context ?? throw new ArgumentNullException(nameof(context)); - } + /// + public ICreateColumnOnTableBuilder Column(string columnName) + { + var expression = new CreateColumnExpression(_context) { Column = { Name = columnName } }; + return new CreateColumnBuilder(_context, expression); + } - /// - public IExecutableBuilder Table(bool withoutKeysAndIndexes = false) - { - return new CreateTableOfDtoBuilder(_context) { TypeOfDto = typeof(TDto), WithoutKeysAndIndexes = withoutKeysAndIndexes }; - } + /// + public ICreateForeignKeyFromTableBuilder ForeignKey() + { + var expression = new CreateForeignKeyExpression(_context); + return new CreateForeignKeyBuilder(expression); + } - /// - public IExecutableBuilder KeysAndIndexes() - { - return new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeof(TDto) }; - } + /// + public ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName) + { + var expression = new CreateForeignKeyExpression(_context) { ForeignKey = { Name = foreignKeyName } }; + return new CreateForeignKeyBuilder(expression); + } - /// - public IExecutableBuilder KeysAndIndexes(Type typeOfDto) - { - return new CreateKeysAndIndexesBuilder(_context) { TypeOfDto = typeOfDto }; - } + /// + public ICreateIndexForTableBuilder Index() + { + var expression = new CreateIndexExpression(_context); + return new CreateIndexBuilder(expression); + } - /// - public ICreateTableWithColumnBuilder Table(string tableName) - { - var expression = new CreateTableExpression(_context) { TableName = tableName }; - return new CreateTableBuilder(_context, expression); - } + /// + public ICreateIndexForTableBuilder Index(string indexName) + { + var expression = new CreateIndexExpression(_context) { Index = { Name = indexName } }; + return new CreateIndexBuilder(expression); + } - /// - public ICreateColumnOnTableBuilder Column(string columnName) - { - var expression = new CreateColumnExpression(_context) { Column = { Name = columnName } }; - return new CreateColumnBuilder(_context, expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey() => PrimaryKey(true); - /// - public ICreateForeignKeyFromTableBuilder ForeignKey() - { - var expression = new CreateForeignKeyExpression(_context); - return new CreateForeignKeyBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey(bool clustered) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); + expression.Constraint.IsPrimaryKeyClustered = clustered; + return new CreateConstraintBuilder(expression); + } - /// - public ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName) - { - var expression = new CreateForeignKeyExpression(_context) { ForeignKey = { Name = foreignKeyName } }; - return new CreateForeignKeyBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName) => PrimaryKey(primaryKeyName, true); - /// - public ICreateIndexForTableBuilder Index() - { - var expression = new CreateIndexExpression(_context); - return new CreateIndexBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); + expression.Constraint.ConstraintName = primaryKeyName; + expression.Constraint.IsPrimaryKeyClustered = clustered; + return new CreateConstraintBuilder(expression); + } - /// - public ICreateIndexForTableBuilder Index(string indexName) - { - var expression = new CreateIndexExpression(_context) { Index = { Name = indexName } }; - return new CreateIndexBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder UniqueConstraint() + { + var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); + return new CreateConstraintBuilder(expression); + } - /// - public ICreateConstraintOnTableBuilder PrimaryKey() => PrimaryKey(true); + /// + public ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); + expression.Constraint.ConstraintName = constraintName; + return new CreateConstraintBuilder(expression); + } - /// - public ICreateConstraintOnTableBuilder PrimaryKey(bool clustered) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); - expression.Constraint.IsPrimaryKeyClustered = clustered; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName) => PrimaryKey(primaryKeyName, true); - - /// - public ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey); - expression.Constraint.ConstraintName = primaryKeyName; - expression.Constraint.IsPrimaryKeyClustered = clustered; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder UniqueConstraint() - { - var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.Unique); - expression.Constraint.ConstraintName = constraintName; - return new CreateConstraintBuilder(expression); - } - - /// - public ICreateConstraintOnTableBuilder Constraint(string constraintName) - { - var expression = new CreateConstraintExpression(_context, ConstraintType.NonUnique); - expression.Constraint.ConstraintName = constraintName; - return new CreateConstraintBuilder(expression); - } + /// + public ICreateConstraintOnTableBuilder Constraint(string constraintName) + { + var expression = new CreateConstraintExpression(_context, ConstraintType.NonUnique); + expression.Constraint.ConstraintName = constraintName; + return new CreateConstraintBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs index 7440d6c837..f8ea3bde27 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateConstraintExpression.cs @@ -1,40 +1,41 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; + +public class CreateConstraintExpression : MigrationExpressionBase { - public class CreateConstraintExpression : MigrationExpressionBase + public CreateConstraintExpression(IMigrationContext context, ConstraintType constraint) + : base(context) => + Constraint = new ConstraintDefinition(constraint); + + public ConstraintDefinition Constraint { get; } + + protected override string GetSql() { - public CreateConstraintExpression(IMigrationContext context, ConstraintType constraint) - : base(context) + var constraintType = Constraint.IsPrimaryKeyConstraint ? "PRIMARY KEY" : "UNIQUE"; + + if (Constraint.IsPrimaryKeyConstraint && SqlSyntax.SupportsClustered()) { - Constraint = new ConstraintDefinition(constraint); + constraintType += Constraint.IsPrimaryKeyClustered ? " CLUSTERED" : " NONCLUSTERED"; } - public ConstraintDefinition Constraint { get; } - - protected override string GetSql() + if (Constraint.IsNonUniqueConstraint) { - var constraintType = (Constraint.IsPrimaryKeyConstraint) ? "PRIMARY KEY" : "UNIQUE"; - - if (Constraint.IsPrimaryKeyConstraint && SqlSyntax.SupportsClustered()) - constraintType += Constraint.IsPrimaryKeyClustered ? " CLUSTERED" : " NONCLUSTERED"; - - if (Constraint.IsNonUniqueConstraint) - constraintType = string.Empty; - - var columns = new string[Constraint.Columns.Count]; - - for (var i = 0; i < Constraint.Columns.Count; i++) - { - columns[i] = SqlSyntax.GetQuotedColumnName(Constraint.Columns.ElementAt(i)); - } - - return string.Format(SqlSyntax.CreateConstraint, - SqlSyntax.GetQuotedTableName(Constraint.TableName), - SqlSyntax.GetQuotedName(Constraint.ConstraintName), - constraintType, - string.Join(", ", columns)); + constraintType = string.Empty; } + + var columns = new string[Constraint.Columns.Count]; + + for (var i = 0; i < Constraint.Columns.Count; i++) + { + columns[i] = SqlSyntax.GetQuotedColumnName(Constraint.Columns.ElementAt(i)); + } + + return string.Format( + SqlSyntax.CreateConstraint, + SqlSyntax.GetQuotedTableName(Constraint.TableName), + SqlSyntax.GetQuotedName(Constraint.ConstraintName), + constraintType, + string.Join(", ", columns)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs index e7ed2faf53..b7072ed739 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Expressions/CreateTableExpression.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; + +public class CreateTableExpression : MigrationExpressionBase { - public class CreateTableExpression : MigrationExpressionBase + public CreateTableExpression(IMigrationContext context) + : base(context) => + Columns = new List(); + + public virtual string SchemaName { get; set; } = null!; + + public virtual string TableName { get; set; } = null!; + + public virtual IList Columns { get; set; } + + protected override string GetSql() { - public CreateTableExpression(IMigrationContext context) - : base(context) - { - Columns = new List(); - } + var table = new TableDefinition { Name = TableName, SchemaName = SchemaName, Columns = Columns }; - public virtual string SchemaName { get; set; } = null!; - public virtual string TableName { get; set; } = null!; - public virtual IList Columns { get; set; } - - protected override string GetSql() - { - var table = new TableDefinition { Name = TableName, SchemaName = SchemaName, Columns = Columns }; - - return string.Format(SqlSyntax.Format(table)); - } + return string.Format(SqlSyntax.Format(table)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs index 4a30a815a2..0d0f4e82bc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/CreateForeignKeyBuilder.cs @@ -1,87 +1,93 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public class CreateForeignKeyBuilder : ExpressionBuilderBase, + ICreateForeignKeyFromTableBuilder, + ICreateForeignKeyForeignColumnBuilder, + ICreateForeignKeyToTableBuilder, + ICreateForeignKeyPrimaryColumnBuilder, + ICreateForeignKeyCascadeBuilder { - public class CreateForeignKeyBuilder : ExpressionBuilderBase, - ICreateForeignKeyFromTableBuilder, - ICreateForeignKeyForeignColumnBuilder, - ICreateForeignKeyToTableBuilder, - ICreateForeignKeyPrimaryColumnBuilder, - ICreateForeignKeyCascadeBuilder + public CreateForeignKeyBuilder(CreateForeignKeyExpression expression) + : base(expression) { - public CreateForeignKeyBuilder(CreateForeignKeyExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public ICreateForeignKeyForeignColumnBuilder FromTable(string table) - { - Expression.ForeignKey.ForeignTable = table; - return this; - } + /// + public ICreateForeignKeyCascadeBuilder OnDelete(Rule rule) + { + Expression.ForeignKey.OnDelete = rule; + return this; + } - /// - public ICreateForeignKeyToTableBuilder ForeignColumn(string column) + /// + public ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + Expression.ForeignKey.OnUpdate = rule; + return this; + } + + /// + public IExecutableBuilder OnDeleteOrUpdate(Rule rule) + { + Expression.ForeignKey.OnDelete = rule; + Expression.ForeignKey.OnUpdate = rule; + return new ExecutableBuilder(Expression); + } + + /// + public ICreateForeignKeyToTableBuilder ForeignColumn(string column) + { + Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } + + /// + public ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.ForeignColumns.Add(column); - return this; } - /// - public ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.ForeignColumns.Add(column); - return this; - } + return this; + } - /// - public ICreateForeignKeyPrimaryColumnBuilder ToTable(string table) - { - Expression.ForeignKey.PrimaryTable = table; - return this; - } + /// + public ICreateForeignKeyForeignColumnBuilder FromTable(string table) + { + Expression.ForeignKey.ForeignTable = table; + return this; + } - /// - public ICreateForeignKeyCascadeBuilder PrimaryColumn(string column) + /// + public ICreateForeignKeyCascadeBuilder PrimaryColumn(string column) + { + Expression.ForeignKey.PrimaryColumns.Add(column); + return this; + } + + /// + public ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.PrimaryColumns.Add(column); - return this; } - /// - public ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.PrimaryColumns.Add(column); - return this; - } + return this; + } - /// - public ICreateForeignKeyCascadeBuilder OnDelete(Rule rule) - { - Expression.ForeignKey.OnDelete = rule; - return this; - } - - /// - public ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - Expression.ForeignKey.OnUpdate = rule; - return this; - } - - /// - public IExecutableBuilder OnDeleteOrUpdate(Rule rule) - { - Expression.ForeignKey.OnDelete = rule; - Expression.ForeignKey.OnUpdate = rule; - return new ExecutableBuilder(Expression); - } + /// + public ICreateForeignKeyPrimaryColumnBuilder ToTable(string table) + { + Expression.ForeignKey.PrimaryTable = table; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs index 3b45404b85..2761cf0b1b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyCascadeBuilder.cs @@ -1,12 +1,13 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyCascadeBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateForeignKeyCascadeBuilder : IFluentBuilder, IExecutableBuilder - { - ICreateForeignKeyCascadeBuilder OnDelete(Rule rule); - ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule); - IExecutableBuilder OnDeleteOrUpdate(Rule rule); - } + ICreateForeignKeyCascadeBuilder OnDelete(Rule rule); + + ICreateForeignKeyCascadeBuilder OnUpdate(Rule rule); + + IExecutableBuilder OnDeleteOrUpdate(Rule rule); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs index 8f37b40487..321b605693 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyForeignColumnBuilder.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyForeignColumnBuilder : IFluentBuilder { - public interface ICreateForeignKeyForeignColumnBuilder : IFluentBuilder - { - ICreateForeignKeyToTableBuilder ForeignColumn(string column); - ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns); - } + ICreateForeignKeyToTableBuilder ForeignColumn(string column); + + ICreateForeignKeyToTableBuilder ForeignColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs index 941647e27d..a5b2cefe4a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyFromTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyFromTableBuilder : IFluentBuilder { - public interface ICreateForeignKeyFromTableBuilder : IFluentBuilder - { - ICreateForeignKeyForeignColumnBuilder FromTable(string table); - } + ICreateForeignKeyForeignColumnBuilder FromTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs index 95d1346d0f..81a258cfa3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyPrimaryColumnBuilder.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyPrimaryColumnBuilder : IFluentBuilder { - public interface ICreateForeignKeyPrimaryColumnBuilder : IFluentBuilder - { - ICreateForeignKeyCascadeBuilder PrimaryColumn(string column); - ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns); - } + ICreateForeignKeyCascadeBuilder PrimaryColumn(string column); + + ICreateForeignKeyCascadeBuilder PrimaryColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs index 1ea49b1369..1d78c732f4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ForeignKey/ICreateForeignKeyToTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; + +public interface ICreateForeignKeyToTableBuilder : IFluentBuilder { - public interface ICreateForeignKeyToTableBuilder : IFluentBuilder - { - ICreateForeignKeyPrimaryColumnBuilder ToTable(string table); - } + ICreateForeignKeyPrimaryColumnBuilder ToTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs index d01326eb0d..80637cb870 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/ICreateBuilder.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Constraint; @@ -6,91 +5,90 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create; + +/// +/// Builds a Create expression. +/// +public interface ICreateBuilder : IFluentBuilder { /// - /// Builds a Create expression. + /// Builds a Create Table expression, and executes. /// - public interface ICreateBuilder : IFluentBuilder - { - /// - /// Builds a Create Table expression, and executes. - /// - IExecutableBuilder Table(bool withoutKeysAndIndexes = false); + IExecutableBuilder Table(bool withoutKeysAndIndexes = false); - /// - /// Builds a Create Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(); + /// + /// Builds a Create Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(); - /// - /// Builds a Create Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(Type typeOfDto); + /// + /// Builds a Create Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(Type typeOfDto); - /// - /// Builds a Create Table expression. - /// - ICreateTableWithColumnBuilder Table(string tableName); + /// + /// Builds a Create Table expression. + /// + ICreateTableWithColumnBuilder Table(string tableName); - /// - /// Builds a Create Column expression. - /// - ICreateColumnOnTableBuilder Column(string columnName); + /// + /// Builds a Create Column expression. + /// + ICreateColumnOnTableBuilder Column(string columnName); - /// - /// Builds a Create Foreign Key expression. - /// - ICreateForeignKeyFromTableBuilder ForeignKey(); + /// + /// Builds a Create Foreign Key expression. + /// + ICreateForeignKeyFromTableBuilder ForeignKey(); - /// - /// Builds a Create Foreign Key expression. - /// - ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName); + /// + /// Builds a Create Foreign Key expression. + /// + ICreateForeignKeyFromTableBuilder ForeignKey(string foreignKeyName); - /// - /// Builds a Create Index expression. - /// - ICreateIndexForTableBuilder Index(); + /// + /// Builds a Create Index expression. + /// + ICreateIndexForTableBuilder Index(); - /// - /// Builds a Create Index expression. - /// - ICreateIndexForTableBuilder Index(string indexName); + /// + /// Builds a Create Index expression. + /// + ICreateIndexForTableBuilder Index(string indexName); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(bool clustered); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(bool clustered); - /// - /// Builds a Create Primary Key expression. - /// - ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered); + /// + /// Builds a Create Primary Key expression. + /// + ICreateConstraintOnTableBuilder PrimaryKey(string primaryKeyName, bool clustered); - /// - /// Builds a Create Unique Constraint expression. - /// - ICreateConstraintOnTableBuilder UniqueConstraint(); + /// + /// Builds a Create Unique Constraint expression. + /// + ICreateConstraintOnTableBuilder UniqueConstraint(); - /// - /// Builds a Create Unique Constraint expression. - /// - ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName); + /// + /// Builds a Create Unique Constraint expression. + /// + ICreateConstraintOnTableBuilder UniqueConstraint(string constraintName); - /// - /// Builds a Create Constraint expression. - /// - ICreateConstraintOnTableBuilder Constraint(string constraintName); - } + /// + /// Builds a Create Constraint expression. + /// + ICreateConstraintOnTableBuilder Constraint(string constraintName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs index a6024c19db..ea056afaff 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs @@ -1,94 +1,91 @@ -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public class CreateIndexBuilder : ExpressionBuilderBase, + ICreateIndexForTableBuilder, + ICreateIndexOnColumnBuilder, + ICreateIndexColumnOptionsBuilder, + ICreateIndexOptionsBuilder { - public class CreateIndexBuilder : ExpressionBuilderBase, - ICreateIndexForTableBuilder, - ICreateIndexOnColumnBuilder, - ICreateIndexColumnOptionsBuilder, - ICreateIndexOptionsBuilder + public CreateIndexBuilder(CreateIndexExpression expression) + : base(expression) { - public CreateIndexBuilder(CreateIndexExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + public IndexColumnDefinition? CurrentColumn { get; set; } - public IndexColumnDefinition? CurrentColumn { get; set; } - - /// - public ICreateIndexOnColumnBuilder OnTable(string tableName) + /// + public ICreateIndexOnColumnBuilder Ascending() + { + if (CurrentColumn is not null) { - Expression.Index.TableName = tableName; - return this; + CurrentColumn.Direction = Direction.Ascending; } - /// - public ICreateIndexColumnOptionsBuilder OnColumn(string columnName) + return this; + } + + /// + public ICreateIndexOnColumnBuilder Descending() + { + if (CurrentColumn is not null) { - CurrentColumn = new IndexColumnDefinition { Name = columnName }; - Expression.Index.Columns.Add(CurrentColumn); - return this; + CurrentColumn.Direction = Direction.Descending; } - /// - public ICreateIndexOptionsBuilder WithOptions() - { - return this; - } + return this; + } - /// - public ICreateIndexOnColumnBuilder Ascending() - { - if (CurrentColumn is not null) - { - CurrentColumn.Direction = Direction.Ascending; - } + /// + ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; + return this; + } - return this; - } + /// + public ICreateIndexOnColumnBuilder OnTable(string tableName) + { + Expression.Index.TableName = tableName; + return this; + } - /// - public ICreateIndexOnColumnBuilder Descending() - { - if (CurrentColumn is not null) - { - CurrentColumn.Direction = Direction.Descending; - } + /// + public void Do() => Expression.Execute(); - return this; - } + /// + public ICreateIndexColumnOptionsBuilder OnColumn(string columnName) + { + CurrentColumn = new IndexColumnDefinition { Name = columnName }; + Expression.Index.Columns.Add(CurrentColumn); + return this; + } - /// - ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique() - { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - return this; - } + /// + public ICreateIndexOptionsBuilder WithOptions() => this; - /// - public ICreateIndexOnColumnBuilder NonClustered() - { - Expression.Index.IndexType = IndexTypes.NonClustered; - return this; - } + /// + public ICreateIndexOnColumnBuilder NonClustered() + { + Expression.Index.IndexType = IndexTypes.NonClustered; + return this; + } - /// - public ICreateIndexOnColumnBuilder Clustered() - { - Expression.Index.IndexType = IndexTypes.Clustered; - return this; - } + /// + public ICreateIndexOnColumnBuilder Clustered() + { + Expression.Index.IndexType = IndexTypes.Clustered; + return this; + } - /// - ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() - { - Expression.Index.IndexType = IndexTypes.UniqueNonClustered; - return this; - } + /// + ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique() + { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs index 037e9e71f5..3ea0b4cb7b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexColumnOptionsBuilder.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexColumnOptionsBuilder : IFluentBuilder { - public interface ICreateIndexColumnOptionsBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder Ascending(); - ICreateIndexOnColumnBuilder Descending(); - ICreateIndexOnColumnBuilder Unique(); - } + ICreateIndexOnColumnBuilder Ascending(); + + ICreateIndexOnColumnBuilder Descending(); + + ICreateIndexOnColumnBuilder Unique(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs index c74c5b546e..a26d4b41b0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexForTableBuilder.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexForTableBuilder : IFluentBuilder { - public interface ICreateIndexForTableBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder OnTable(string tableName); - } + ICreateIndexOnColumnBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs index 4981186fa3..62f0c7c3ea 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOnColumnBuilder.cs @@ -1,17 +1,16 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexOnColumnBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateIndexOnColumnBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Specifies the index column. - /// - ICreateIndexColumnOptionsBuilder OnColumn(string columnName); + /// + /// Specifies the index column. + /// + ICreateIndexColumnOptionsBuilder OnColumn(string columnName); - /// - /// Specifies options. - /// - ICreateIndexOptionsBuilder WithOptions(); - } + /// + /// Specifies options. + /// + ICreateIndexOptionsBuilder WithOptions(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs index fc2e4f2a53..687f3b0cac 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Index/ICreateIndexOptionsBuilder.cs @@ -1,9 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Index; + +public interface ICreateIndexOptionsBuilder : IFluentBuilder { - public interface ICreateIndexOptionsBuilder : IFluentBuilder - { - ICreateIndexOnColumnBuilder Unique(); - ICreateIndexOnColumnBuilder NonClustered(); - ICreateIndexOnColumnBuilder Clustered(); - } + ICreateIndexOnColumnBuilder Unique(); + + ICreateIndexOnColumnBuilder NonClustered(); + + ICreateIndexOnColumnBuilder Clustered(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs index 86c3dc537a..51c70b5dba 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/KeysAndIndexes/CreateKeysAndIndexesBuilder.cs @@ -1,59 +1,61 @@ -using System; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.KeysAndIndexes; + +public class CreateKeysAndIndexesBuilder : IExecutableBuilder { - public class CreateKeysAndIndexesBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + private readonly DatabaseType[] _supportedDatabaseTypes; + + public CreateKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - private readonly DatabaseType[] _supportedDatabaseTypes; - - public CreateKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) - { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } - - public Type? TypeOfDto { get; set; } - - /// - public void Do() - { - var syntax = _context.SqlContext.SqlSyntax; - if (TypeOfDto is null) - { - return; - } - var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - - // note: of course we are creating the keys and indexes as per the DTO, so - // changing the DTO may break old migrations - or, better, these migrations - // should capture a copy of the DTO class that will not change - - ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); - foreach (var sql in syntax.Format(tableDefinition.Indexes)) - ExecuteSql(sql); - foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) - ExecuteSql(sql); - - // note: we do *not* create the DF_ default constraints - /* - foreach (var column in tableDefinition.Columns) - { - var sql = syntax.FormatDefaultConstraint(column); - if (!sql.IsNullOrWhiteSpace()) - ExecuteSql(sql); - } - */ - } - - private void ExecuteSql(string sql) - { - new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } - .Execute(); - } + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; } + + public Type? TypeOfDto { get; set; } + + /// + public void Do() + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + if (TypeOfDto is null) + { + return; + } + + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); + + // note: of course we are creating the keys and indexes as per the DTO, so + // changing the DTO may break old migrations - or, better, these migrations + // should capture a copy of the DTO class that will not change + ExecuteSql(syntax.FormatPrimaryKey(tableDefinition)); + foreach (var sql in syntax.Format(tableDefinition.Indexes)) + { + ExecuteSql(sql); + } + + foreach (var sql in syntax.Format(tableDefinition.ForeignKeys)) + { + ExecuteSql(sql); + } + + // note: we do *not* create the DF_ default constraints + /* + foreach (var column in tableDefinition.Columns) + { + var sql = syntax.FormatDefaultConstraint(column); + if (!sql.IsNullOrWhiteSpace()) + ExecuteSql(sql); + } + */ + } + + private void ExecuteSql(string sql) => + new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } + .Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs index 81e3a702ce..0d2970753b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableBuilder.cs @@ -1,270 +1,259 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public class CreateTableBuilder : ExpressionBuilderBase, + ICreateTableColumnAsTypeBuilder, + ICreateTableColumnOptionForeignKeyCascadeBuilder { - public class CreateTableBuilder : ExpressionBuilderBase, - ICreateTableColumnAsTypeBuilder, - ICreateTableColumnOptionForeignKeyCascadeBuilder + private readonly IMigrationContext _context; + + public CreateTableBuilder(IMigrationContext context, CreateTableExpression expression) + : base(expression) => + _context = context; + + public ColumnDefinition CurrentColumn { get; set; } = null!; + + public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + + /// + public void Do() => Expression.Execute(); + + /// + public ICreateTableColumnAsTypeBuilder WithColumn(string name) { - private readonly IMigrationContext _context; - - public CreateTableBuilder(IMigrationContext context, CreateTableExpression expression) - : base(expression) + var column = new ColumnDefinition { - _context = context; - } + Name = name, + TableName = Expression.TableName, + ModificationType = ModificationType.Create, + }; + Expression.Columns.Add(column); + CurrentColumn = column; + return this; + } - /// - public void Do() => Expression.Execute(); + /// + public ICreateTableColumnOptionBuilder WithDefault(SystemMethods method) + { + CurrentColumn.DefaultValue = method; + return this; + } - public ColumnDefinition CurrentColumn { get; set; } = null!; + public ICreateTableColumnOptionBuilder WithDefaultValue(object value) + { + CurrentColumn.DefaultValue = value; + return this; + } - public ForeignKeyDefinition CurrentForeignKey { get; set; } = null!; + /// + public ICreateTableColumnOptionBuilder Identity() + { + CurrentColumn.IsIdentity = true; + return this; + } - public override ColumnDefinition GetColumnForType() - { - return CurrentColumn; - } + /// + public ICreateTableColumnOptionBuilder Indexed() => Indexed(null); - /// - public ICreateTableColumnAsTypeBuilder WithColumn(string name) - { - var column = new ColumnDefinition { Name = name, TableName = Expression.TableName, ModificationType = ModificationType.Create }; - Expression.Columns.Add(column); - CurrentColumn = column; - return this; - } + /// + public ICreateTableColumnOptionBuilder Indexed(string? indexName) + { + CurrentColumn.IsIndexed = true; - /// - public ICreateTableColumnOptionBuilder WithDefault(SystemMethods method) - { - CurrentColumn.DefaultValue = method; - return this; - } - - public ICreateTableColumnOptionBuilder WithDefaultValue(object value) - { - CurrentColumn.DefaultValue = value; - return this; - } - - /// - public ICreateTableColumnOptionBuilder Identity() - { - CurrentColumn.IsIdentity = true; - return this; - } - - /// - public ICreateTableColumnOptionBuilder Indexed() - { - return Indexed(null); - } - - /// - public ICreateTableColumnOptionBuilder Indexed(string? indexName) - { - CurrentColumn.IsIndexed = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition - { - Name = indexName, - SchemaName = Expression.SchemaName, - TableName = Expression.TableName - }); - - index.Index.Columns.Add(new IndexColumnDefinition - { - Name = CurrentColumn.Name - }); - - Expression.Expressions.Add(index); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder PrimaryKey() - { - CurrentColumn.IsPrimaryKey = true; - - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - TableName = CurrentColumn.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder PrimaryKey(string primaryKeyName) - { - CurrentColumn.IsPrimaryKey = true; - CurrentColumn.PrimaryKeyName = primaryKeyName; - - var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = - { - ConstraintName = primaryKeyName, - TableName = CurrentColumn.TableName, - Columns = new[] { CurrentColumn.Name } - } - }; - Expression.Expressions.Add(expression); - - return this; - } - - /// - public ICreateTableColumnOptionBuilder Nullable() - { - CurrentColumn.IsNullable = true; - return this; - } - - /// - public ICreateTableColumnOptionBuilder NotNullable() - { - CurrentColumn.IsNullable = false; - return this; - } - - /// - public ICreateTableColumnOptionBuilder Unique() - { - return Unique(null); - } - - /// - public ICreateTableColumnOptionBuilder Unique(string? indexName) - { - CurrentColumn.IsUnique = true; - - var index = new CreateIndexExpression(_context, new IndexDefinition + var index = new CreateIndexExpression( + _context, + new IndexDefinition { Name = indexName, SchemaName = Expression.SchemaName, TableName = Expression.TableName, - IndexType = IndexTypes.UniqueNonClustered }); - index.Index.Columns.Add(new IndexColumnDefinition + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); + + Expression.Expressions.Add(index); + + return this; + } + + /// + public ICreateTableColumnOptionBuilder PrimaryKey() + { + CurrentColumn.IsPrimaryKey = true; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { TableName = CurrentColumn.TableName, Columns = new[] { CurrentColumn.Name } }, + }; + Expression.Expressions.Add(expression); + + return this; + } + + /// + public ICreateTableColumnOptionBuilder PrimaryKey(string primaryKeyName) + { + CurrentColumn.IsPrimaryKey = true; + CurrentColumn.PrimaryKeyName = primaryKeyName; + + var expression = new CreateConstraintExpression(_context, ConstraintType.PrimaryKey) + { + Constraint = { - Name = CurrentColumn.Name + ConstraintName = primaryKeyName, + TableName = CurrentColumn.TableName, + Columns = new[] { CurrentColumn.Name } + }, + }; + Expression.Expressions.Add(expression); + + return this; + } + + /// + public ICreateTableColumnOptionBuilder Nullable() + { + CurrentColumn.IsNullable = true; + return this; + } + + /// + public ICreateTableColumnOptionBuilder NotNullable() + { + CurrentColumn.IsNullable = false; + return this; + } + + /// + public ICreateTableColumnOptionBuilder Unique() => Unique(null); + + /// + public ICreateTableColumnOptionBuilder Unique(string? indexName) + { + CurrentColumn.IsUnique = true; + + var index = new CreateIndexExpression( + _context, + new IndexDefinition + { + Name = indexName, + SchemaName = Expression.SchemaName, + TableName = Expression.TableName, + IndexType = IndexTypes.UniqueNonClustered, }); - Expression.Expressions.Add(index); + index.Index.Columns.Add(new IndexColumnDefinition { Name = CurrentColumn.Name }); - return this; - } + Expression.Expressions.Add(index); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string primaryTableName, string primaryColumnName) - { - return ForeignKey(null, null, primaryTableName, primaryColumnName); - } + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, - string primaryColumnName) - { - return ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey( + string primaryTableName, + string primaryColumnName) => ForeignKey(null, null, primaryTableName, primaryColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string? foreignKeyName, string? primaryTableSchema, - string primaryTableName, string primaryColumnName) - { - CurrentColumn.IsForeignKey = true; + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey(string foreignKeyName, string primaryTableName, + string primaryColumnName) => + ForeignKey(foreignKeyName, null, primaryTableName, primaryColumnName); - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey( + string? foreignKeyName, + string? primaryTableSchema, + string primaryTableName, string primaryColumnName) + { + CurrentColumn.IsForeignKey = true; + + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = primaryTableName, PrimaryTableSchema = primaryTableSchema, ForeignTable = Expression.TableName, - ForeignTableSchema = Expression.SchemaName + ForeignTableSchema = Expression.SchemaName, }); - fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); - fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); + fk.ForeignKey.PrimaryColumns.Add(primaryColumnName); + fk.ForeignKey.ForeignColumns.Add(CurrentColumn.Name); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey() - { - CurrentColumn.IsForeignKey = true; - return this; - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ForeignKey() + { + CurrentColumn.IsForeignKey = true; + return this; + } - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignTableName, string foreignColumnName) - { - return ReferencedBy(null, null, foreignTableName, foreignColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string foreignTableName, + string foreignColumnName) => ReferencedBy(null, null, foreignTableName, foreignColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, - string foreignColumnName) - { - return ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - } + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string foreignKeyName, string foreignTableName, + string foreignColumnName) => + ReferencedBy(foreignKeyName, null, foreignTableName, foreignColumnName); - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy(string? foreignKeyName, string? foreignTableSchema, - string foreignTableName, string foreignColumnName) - { - var fk = new CreateForeignKeyExpression(_context, new ForeignKeyDefinition + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder ReferencedBy( + string? foreignKeyName, + string? foreignTableSchema, + string foreignTableName, string foreignColumnName) + { + var fk = new CreateForeignKeyExpression( + _context, + new ForeignKeyDefinition { Name = foreignKeyName, PrimaryTable = Expression.TableName, PrimaryTableSchema = Expression.SchemaName, ForeignTable = foreignTableName, - ForeignTableSchema = foreignTableSchema + ForeignTableSchema = foreignTableSchema, }); - fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); - fk.ForeignKey.ForeignColumns.Add(foreignColumnName); + fk.ForeignKey.PrimaryColumns.Add(CurrentColumn.Name); + fk.ForeignKey.ForeignColumns.Add(foreignColumnName); - Expression.Expressions.Add(fk); - CurrentForeignKey = fk.ForeignKey; - return this; - } - - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) - { - CurrentForeignKey.OnDelete = rule; - return this; - } - - /// - public ICreateTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) - { - CurrentForeignKey.OnUpdate = rule; - return this; - } - - /// - public ICreateTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) - { - OnDelete(rule); - OnUpdate(rule); - return this; - } + Expression.Expressions.Add(fk); + CurrentForeignKey = fk.ForeignKey; + return this; } + + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder OnDelete(Rule rule) + { + CurrentForeignKey.OnDelete = rule; + return this; + } + + /// + public ICreateTableColumnOptionForeignKeyCascadeBuilder OnUpdate(Rule rule) + { + CurrentForeignKey.OnUpdate = rule; + return this; + } + + /// + public ICreateTableColumnOptionBuilder OnDeleteOrUpdate(Rule rule) + { + OnDelete(rule); + OnUpdate(rule); + return this; + } + + public override ColumnDefinition GetColumnForType() => CurrentColumn; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs index 5aac9e90f7..c75d3fb07a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/CreateTableOfDtoBuilder.cs @@ -1,46 +1,44 @@ -using System; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public class CreateTableOfDtoBuilder : IExecutableBuilder { - public class CreateTableOfDtoBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + + // TODO: This doesn't do anything. + private readonly DatabaseType[] _supportedDatabaseTypes; + + public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - - // TODO: This doesn't do anything. - private readonly DatabaseType[] _supportedDatabaseTypes; - - public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) - { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } - - public Type? TypeOfDto { get; set; } - - public bool WithoutKeysAndIndexes { get; set; } - - /// - public void Do() - { - var syntax = _context.SqlContext.SqlSyntax; - if (TypeOfDto is null) - { - return; - } - var tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); - - syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); - _context.BuildingExpression = false; - } - - private void ExecuteSql(string sql) - { - new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } - .Execute(); - } + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; } + + public Type? TypeOfDto { get; set; } + + public bool WithoutKeysAndIndexes { get; set; } + + /// + public void Do() + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + if (TypeOfDto is null) + { + return; + } + + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(TypeOfDto, syntax); + + syntax.HandleCreateTable(_context.Database, tableDefinition, WithoutKeysAndIndexes); + _context.BuildingExpression = false; + } + + private void ExecuteSql(string sql) => + new ExecuteSqlStatementExpression(_context) { SqlStatement = sql } + .Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs index dfbeacde35..fb5d069e3a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnAsTypeBuilder.cs @@ -1,7 +1,7 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnAsTypeBuilder : IColumnTypeBuilder { - public interface ICreateTableColumnAsTypeBuilder : IColumnTypeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs index 9c3d877277..43c130cb09 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnOptionBuilder : + IColumnOptionBuilder, + ICreateTableWithColumnBuilder { - public interface ICreateTableColumnOptionBuilder : - IColumnOptionBuilder, - ICreateTableWithColumnBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs index 14d9369cfc..bc00242afb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableColumnOptionForeignKeyCascadeBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableColumnOptionForeignKeyCascadeBuilder : + ICreateTableColumnOptionBuilder, + IForeignKeyCascadeBuilder { - public interface ICreateTableColumnOptionForeignKeyCascadeBuilder : - ICreateTableColumnOptionBuilder, - IForeignKeyCascadeBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs index d913406387..1f16ce80c3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Create/Table/ICreateTableWithColumnBuilder.cs @@ -1,9 +1,8 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table; + +public interface ICreateTableWithColumnBuilder : IFluentBuilder, IExecutableBuilder { - public interface ICreateTableWithColumnBuilder : IFluentBuilder, IExecutableBuilder - { - ICreateTableColumnAsTypeBuilder WithColumn(string name); - } + ICreateTableColumnAsTypeBuilder WithColumn(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs index 50101f46a1..bd61efaa48 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/DeleteColumnBuilder.cs @@ -1,27 +1,27 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; + +public class DeleteColumnBuilder : ExpressionBuilderBase, + IDeleteColumnBuilder { - public class DeleteColumnBuilder : ExpressionBuilderBase, - IDeleteColumnBuilder + public DeleteColumnBuilder(DeleteColumnExpression expression) + : base(expression) { - public DeleteColumnBuilder(DeleteColumnExpression expression) - : base(expression) - { } + } - /// - public IExecutableBuilder FromTable(string tableName) - { - Expression.TableName = tableName; - return new ExecutableBuilder(Expression); - } + /// + public IExecutableBuilder FromTable(string tableName) + { + Expression.TableName = tableName; + return new ExecutableBuilder(Expression); + } - /// - public IDeleteColumnBuilder Column(string columnName) - { - Expression.ColumnNames.Add(columnName); - return this; - } + /// + public IDeleteColumnBuilder Column(string columnName) + { + Expression.ColumnNames.Add(columnName); + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs index 80755635ee..eaed24cfdf 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Column/IDeleteColumnBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; + +/// +/// Builds a Delete Column expression. +/// +public interface IDeleteColumnBuilder : IFluentBuilder { /// - /// Builds a Delete Column expression. + /// Specifies the table of the column to delete. /// - public interface IDeleteColumnBuilder : IFluentBuilder - { - /// - /// Specifies the table of the column to delete. - /// - IExecutableBuilder FromTable(string tableName); + IExecutableBuilder FromTable(string tableName); - /// - /// Specifies the column to delete. - /// - IDeleteColumnBuilder Column(string columnName); - } + /// + /// Specifies the column to delete. + /// + IDeleteColumnBuilder Column(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs index 84e5393549..84287c265d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/DeleteConstraintBuilder.cs @@ -1,20 +1,20 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint -{ - public class DeleteConstraintBuilder : ExpressionBuilderBase, - IDeleteConstraintBuilder - { - public DeleteConstraintBuilder(DeleteConstraintExpression expression) - : base(expression) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; - /// - public IExecutableBuilder FromTable(string tableName) - { - Expression.Constraint.TableName = tableName; - return new ExecutableBuilder(Expression); - } +public class DeleteConstraintBuilder : ExpressionBuilderBase, + IDeleteConstraintBuilder +{ + public DeleteConstraintBuilder(DeleteConstraintExpression expression) + : base(expression) + { + } + + /// + public IExecutableBuilder FromTable(string tableName) + { + Expression.Constraint.TableName = tableName; + return new ExecutableBuilder(Expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs index a3304f552d..7030848ceb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Constraint/IDeleteConstraintBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; + +/// +/// Builds a Delete Constraint expression. +/// +public interface IDeleteConstraintBuilder : IFluentBuilder { /// - /// Builds a Delete Constraint expression. + /// Specifies the table of the constraint to delete. /// - public interface IDeleteConstraintBuilder : IFluentBuilder - { - /// - /// Specifies the table of the constraint to delete. - /// - IExecutableBuilder FromTable(string tableName); - } + IExecutableBuilder FromTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs index 77d00b29f3..076239f9f9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/DeleteDataBuilder.cs @@ -1,53 +1,52 @@ -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; + +public class DeleteDataBuilder : ExpressionBuilderBase, + IDeleteDataBuilder { - public class DeleteDataBuilder : ExpressionBuilderBase, - IDeleteDataBuilder + public DeleteDataBuilder(DeleteDataExpression expression) + : base(expression) { - public DeleteDataBuilder(DeleteDataExpression expression) - : base(expression) - { } + } - /// - public IExecutableBuilder IsNull(string columnName) + /// + public IExecutableBuilder IsNull(string columnName) + { + Expression.Rows.Add(new DeletionDataDefinition { new(columnName, null) }); + return this; + } + + /// + public IDeleteDataBuilder Row(object dataAsAnonymousType) + { + Expression.Rows.Add(GetData(dataAsAnonymousType)); + return this; + } + + /// + public IExecutableBuilder AllRows() + { + Expression.IsAllRows = true; + return this; + } + + /// + public void Do() => Expression.Execute(); + + private static DeletionDataDefinition GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + + var data = new DeletionDataDefinition(); + foreach (PropertyDescriptor property in properties) { - Expression.Rows.Add(new DeletionDataDefinition { new KeyValuePair(columnName, null) }); - return this; + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } - /// - public IDeleteDataBuilder Row(object dataAsAnonymousType) - { - Expression.Rows.Add(GetData(dataAsAnonymousType)); - return this; - } - - /// - public IExecutableBuilder AllRows() - { - Expression.IsAllRows = true; - return this; - } - - /// - public void Do() - { - Expression.Execute(); - } - - private static DeletionDataDefinition GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - - var data = new DeletionDataDefinition(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; - } + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs index 701d526d7d..1450de2c3c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Data/IDeleteDataBuilder.cs @@ -1,25 +1,24 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDataBuilder : IFluentBuilder, IExecutableBuilder { /// - /// Builds a Delete expression. + /// Specifies a row to be deleted. /// - public interface IDeleteDataBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Specifies a row to be deleted. - /// - IDeleteDataBuilder Row(object dataAsAnonymousType); + IDeleteDataBuilder Row(object dataAsAnonymousType); - /// - /// Specifies that all rows must be deleted. - /// - IExecutableBuilder AllRows(); + /// + /// Specifies that all rows must be deleted. + /// + IExecutableBuilder AllRows(); - /// - /// Specifies that rows with a specified column being null must be deleted. - /// - IExecutableBuilder IsNull(string columnName); - } + /// + /// Specifies that rows with a specified column being null must be deleted. + /// + IExecutableBuilder IsNull(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs index 7093256c5f..5003d50dbd 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/DeleteDefaultConstraintBuilder.cs @@ -1,38 +1,38 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Implements , +/// . +/// +public class DeleteDefaultConstraintBuilder : ExpressionBuilderBase, + IDeleteDefaultConstraintOnTableBuilder, + IDeleteDefaultConstraintOnColumnBuilder { - /// - /// Implements , . - /// - public class DeleteDefaultConstraintBuilder : ExpressionBuilderBase, - IDeleteDefaultConstraintOnTableBuilder, - IDeleteDefaultConstraintOnColumnBuilder + private readonly IMigrationContext _context; + + public DeleteDefaultConstraintBuilder(IMigrationContext context, DeleteDefaultConstraintExpression expression) + : base(expression) => + _context = context; + + /// + public IExecutableBuilder OnColumn(string columnName) { - private readonly IMigrationContext _context; + Expression.ColumnName = columnName; + Expression.HasDefaultConstraint = _context.SqlContext.SqlSyntax.TryGetDefaultConstraint( + _context.Database, + Expression.TableName, columnName, out var constraintName); + Expression.ConstraintName = constraintName ?? string.Empty; - public DeleteDefaultConstraintBuilder(IMigrationContext context, DeleteDefaultConstraintExpression expression) - : base(expression) - { - _context = context; - } + return new ExecutableBuilder(Expression); + } - /// - public IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName) - { - Expression.TableName = tableName; - return this; - } - - /// - public IExecutableBuilder OnColumn(string columnName) - { - Expression.ColumnName = columnName; - Expression.HasDefaultConstraint = _context.SqlContext.SqlSyntax.TryGetDefaultConstraint(_context.Database, Expression.TableName, columnName, out var constraintName); - Expression.ConstraintName = constraintName ?? string.Empty; - - return new ExecutableBuilder(Expression); - } + /// + public IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName) + { + Expression.TableName = tableName; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs index dcc613e0fb..7fda9519d5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnColumnBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDefaultConstraintOnColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the column of the constraint to delete. /// - public interface IDeleteDefaultConstraintOnColumnBuilder : IFluentBuilder - { - /// - /// Specifies the column of the constraint to delete. - /// - IExecutableBuilder OnColumn(string columnName); - } + IExecutableBuilder OnColumn(string columnName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs index 4b8be9f3ee..67feb8601e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DefaultConstraint/IDeleteDefaultConstraintOnTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteDefaultConstraintOnTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the constraint to delete. /// - public interface IDeleteDefaultConstraintOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the constraint to delete. - /// - IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName); - } + IDeleteDefaultConstraintOnColumnBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs index c8a3ed5d28..a139af96fa 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/DeleteBuilder.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; @@ -9,109 +8,120 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete; + +public class DeleteBuilder : IDeleteBuilder { - public class DeleteBuilder : IDeleteBuilder + private readonly IMigrationContext _context; + + public DeleteBuilder(IMigrationContext context) => _context = context; + + /// + public IExecutableBuilder Table(string tableName) { - private readonly IMigrationContext _context; + var expression = new DeleteTableExpression(_context) { TableName = tableName }; + return new ExecutableBuilder(expression); + } - public DeleteBuilder(IMigrationContext context) + /// + public IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true) + { + ISqlSyntaxProvider syntax = _context.SqlContext.SqlSyntax; + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(typeof(TDto), syntax); + return KeysAndIndexes(tableDefinition.Name, local, foreign); + } + + /// + public IExecutableBuilder KeysAndIndexes(string? tableName, bool local = true, bool foreign = true) + { + if (tableName == null) { - _context = context; + throw new ArgumentNullException(nameof(tableName)); } - /// - public IExecutableBuilder Table(string tableName) + if (string.IsNullOrWhiteSpace(tableName)) { - var expression = new DeleteTableExpression(_context) { TableName = tableName }; - return new ExecutableBuilder(expression); + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(tableName)); } - /// - public IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true) + return new DeleteKeysAndIndexesBuilder(_context) { - var syntax = _context.SqlContext.SqlSyntax; - var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(TDto), syntax); - return KeysAndIndexes(tableDefinition.Name, local, foreign); - } + TableName = tableName, + DeleteLocal = local, + DeleteForeign = foreign, + }; + } - /// - public IExecutableBuilder KeysAndIndexes(string? tableName, bool local = true, bool foreign = true) + /// + public IDeleteColumnBuilder Column(string columnName) + { + var expression = new DeleteColumnExpression(_context) { ColumnNames = { columnName } }; + return new DeleteColumnBuilder(expression); + } + + /// + public IDeleteForeignKeyFromTableBuilder ForeignKey() + { + var expression = new DeleteForeignKeyExpression(_context); + return new DeleteForeignKeyBuilder(expression); + } + + /// + public IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName) + { + var expression = new DeleteForeignKeyExpression(_context) { ForeignKey = { Name = foreignKeyName } }; + return new DeleteForeignKeyBuilder(expression); + } + + /// + public IDeleteDataBuilder FromTable(string tableName) + { + var expression = new DeleteDataExpression(_context) { TableName = tableName }; + return new DeleteDataBuilder(expression); + } + + /// + public IDeleteIndexForTableBuilder Index() + { + var expression = new DeleteIndexExpression(_context); + return new DeleteIndexBuilder(expression); + } + + /// + public IDeleteIndexForTableBuilder Index(string indexName) + { + var expression = new DeleteIndexExpression(_context) { Index = { Name = indexName } }; + return new DeleteIndexBuilder(expression); + } + + /// + public IDeleteConstraintBuilder PrimaryKey(string primaryKeyName) + { + var expression = new DeleteConstraintExpression(_context, ConstraintType.PrimaryKey) { - if (tableName == null) throw new ArgumentNullException(nameof(tableName)); - if (string.IsNullOrWhiteSpace(tableName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(tableName)); + Constraint = { ConstraintName = primaryKeyName }, + }; + return new DeleteConstraintBuilder(expression); + } - return new DeleteKeysAndIndexesBuilder(_context) { TableName = tableName, DeleteLocal = local, DeleteForeign = foreign }; - } - - /// - public IDeleteColumnBuilder Column(string columnName) + /// + public IDeleteConstraintBuilder UniqueConstraint(string constraintName) + { + var expression = new DeleteConstraintExpression(_context, ConstraintType.Unique) { - var expression = new DeleteColumnExpression(_context) {ColumnNames = {columnName}}; - return new DeleteColumnBuilder(expression); - } + Constraint = { ConstraintName = constraintName }, + }; + return new DeleteConstraintBuilder(expression); + } - /// - public IDeleteForeignKeyFromTableBuilder ForeignKey() - { - var expression = new DeleteForeignKeyExpression(_context); - return new DeleteForeignKeyBuilder(expression); - } - - /// - public IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName) - { - var expression = new DeleteForeignKeyExpression(_context) {ForeignKey = {Name = foreignKeyName}}; - return new DeleteForeignKeyBuilder(expression); - } - - /// - public IDeleteDataBuilder FromTable(string tableName) - { - var expression = new DeleteDataExpression(_context) { TableName = tableName }; - return new DeleteDataBuilder(expression); - } - - /// - public IDeleteIndexForTableBuilder Index() - { - var expression = new DeleteIndexExpression(_context); - return new DeleteIndexBuilder(expression); - } - - /// - public IDeleteIndexForTableBuilder Index(string indexName) - { - var expression = new DeleteIndexExpression(_context) { Index = { Name = indexName } }; - return new DeleteIndexBuilder(expression); - } - - /// - public IDeleteConstraintBuilder PrimaryKey(string primaryKeyName) - { - var expression = new DeleteConstraintExpression(_context, ConstraintType.PrimaryKey) - { - Constraint = { ConstraintName = primaryKeyName } - }; - return new DeleteConstraintBuilder(expression); - } - - /// - public IDeleteConstraintBuilder UniqueConstraint(string constraintName) - { - var expression = new DeleteConstraintExpression(_context, ConstraintType.Unique) - { - Constraint = { ConstraintName = constraintName } - }; - return new DeleteConstraintBuilder(expression); - } - - /// - public IDeleteDefaultConstraintOnTableBuilder DefaultConstraint() - { - var expression = new DeleteDefaultConstraintExpression(_context); - return new DeleteDefaultConstraintBuilder(_context, expression); - } + /// + public IDeleteDefaultConstraintOnTableBuilder DefaultConstraint() + { + var expression = new DeleteDefaultConstraintExpression(_context); + return new DeleteDefaultConstraintBuilder(_context, expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs index 7cd93133e7..4c1373ab13 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteColumnExpression.cs @@ -1,28 +1,27 @@ -using System.Collections.Generic; using System.Text; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteColumnExpression : MigrationExpressionBase { - public class DeleteColumnExpression : MigrationExpressionBase + public DeleteColumnExpression(IMigrationContext context) + : base(context) => + ColumnNames = new List(); + + public virtual string? TableName { get; set; } + + public ICollection ColumnNames { get; set; } + + protected override string GetSql() { - public DeleteColumnExpression(IMigrationContext context) - : base(context) + var stmts = new StringBuilder(); + foreach (var columnName in ColumnNames) { - ColumnNames = new List(); + stmts.AppendFormat(SqlSyntax.DropColumn, SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedColumnName(columnName)); + AppendStatementSeparator(stmts); } - public virtual string? TableName { get; set; } - public ICollection ColumnNames { get; set; } - - protected override string GetSql() - { - var stmts = new StringBuilder(); - foreach (var columnName in ColumnNames) - { - stmts.AppendFormat(SqlSyntax.DropColumn, SqlSyntax.GetQuotedTableName(TableName), SqlSyntax.GetQuotedColumnName(columnName)); - AppendStatementSeparator(stmts); - } - return stmts.ToString(); - } + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs index 73e17ba124..f9726b8c39 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteConstraintExpression.cs @@ -1,22 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteConstraintExpression : MigrationExpressionBase { - public class DeleteConstraintExpression : MigrationExpressionBase - { - public DeleteConstraintExpression(IMigrationContext context, ConstraintType type) - : base(context) - { - Constraint = new ConstraintDefinition(type); - } + public DeleteConstraintExpression(IMigrationContext context, ConstraintType type) + : base(context) => + Constraint = new ConstraintDefinition(type); - public ConstraintDefinition Constraint { get; } + public ConstraintDefinition Constraint { get; } - protected override string GetSql() - { - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(Constraint.TableName), - SqlSyntax.GetQuotedName(Constraint.ConstraintName)); - } - } + protected override string GetSql() => + string.Format( + SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(Constraint.TableName), + SqlSyntax.GetQuotedName(Constraint.ConstraintName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs index 3d57a77dc0..a45d60f889 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDataExpression.cs @@ -1,38 +1,42 @@ -using System.Collections.Generic; -using System.Linq; using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteDataExpression : MigrationExpressionBase { - public class DeleteDataExpression : MigrationExpressionBase + public DeleteDataExpression(IMigrationContext context) + : base(context) { - public DeleteDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } - public virtual bool IsAllRows { get; set; } + public string? TableName { get; set; } - public List Rows { get; } = new List(); + public virtual bool IsAllRows { get; set; } - protected override string GetSql() + public List Rows { get; } = new(); + + protected override string GetSql() + { + if (IsAllRows) { - if (IsAllRows) - return string.Format(SqlSyntax.DeleteData, SqlSyntax.GetQuotedTableName(TableName), "(1=1)"); - - var stmts = new StringBuilder(); - foreach (var row in Rows) - { - var whereClauses = row.Select(kvp => $"{SqlSyntax.GetQuotedColumnName(kvp.Key)} {(kvp.Value == null ? "IS" : "=")} {GetQuotedValue(kvp.Value)}"); - - stmts.Append(string.Format(SqlSyntax.DeleteData, - SqlSyntax.GetQuotedTableName(TableName), - string.Join(" AND ", whereClauses))); - - AppendStatementSeparator(stmts); - } - return stmts.ToString(); + return string.Format(SqlSyntax.DeleteData, SqlSyntax.GetQuotedTableName(TableName), "(1=1)"); } + + var stmts = new StringBuilder(); + foreach (DeletionDataDefinition row in Rows) + { + IEnumerable whereClauses = row.Select(kvp => + $"{SqlSyntax.GetQuotedColumnName(kvp.Key)} {(kvp.Value == null ? "IS" : "=")} {GetQuotedValue(kvp.Value)}"); + + stmts.Append(string.Format( + SqlSyntax.DeleteData, + SqlSyntax.GetQuotedTableName(TableName), + string.Join(" AND ", whereClauses))); + + AppendStatementSeparator(stmts); + } + + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs index e653d0f6bf..aaa73729ef 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteDefaultConstraintExpression.cs @@ -1,24 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteDefaultConstraintExpression : MigrationExpressionBase { - public class DeleteDefaultConstraintExpression : MigrationExpressionBase + public DeleteDefaultConstraintExpression(IMigrationContext context) + : base(context) { - public DeleteDefaultConstraintExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - public virtual string? ColumnName { get; set; } - public virtual string? ConstraintName { get; set; } - public virtual bool HasDefaultConstraint { get; set; } - - protected override string GetSql() - { - return HasDefaultConstraint - ? string.Format(SqlSyntax.DeleteDefaultConstraint, - SqlSyntax.GetQuotedTableName(TableName), - SqlSyntax.GetQuotedColumnName(ColumnName), - SqlSyntax.GetQuotedName(ConstraintName)) - : string.Empty; - } } + + public virtual string? TableName { get; set; } + + public virtual string? ColumnName { get; set; } + + public virtual string? ConstraintName { get; set; } + + public virtual bool HasDefaultConstraint { get; set; } + + protected override string GetSql() => + HasDefaultConstraint + ? string.Format( + SqlSyntax.DeleteDefaultConstraint, + SqlSyntax.GetQuotedTableName(TableName), + SqlSyntax.GetQuotedColumnName(ColumnName), + SqlSyntax.GetQuotedName(ConstraintName)) + : string.Empty; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs index b7f670006e..2427d905a2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteForeignKeyExpression.cs @@ -1,32 +1,32 @@ -using System; -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteForeignKeyExpression : MigrationExpressionBase { - public class DeleteForeignKeyExpression : MigrationExpressionBase + public DeleteForeignKeyExpression(IMigrationContext context) + : base(context) => + ForeignKey = new ForeignKeyDefinition(); + + public ForeignKeyDefinition ForeignKey { get; set; } + + protected override string GetSql() { - public DeleteForeignKeyExpression(IMigrationContext context) - : base(context) + if (ForeignKey.ForeignTable == null) { - ForeignKey = new ForeignKeyDefinition(); + throw new ArgumentNullException( + "Table name not specified, ensure you have appended the OnTable extension. Format should be Delete.ForeignKey(KeyName).OnTable(TableName)"); } - public ForeignKeyDefinition ForeignKey { get; set; } - - protected override string GetSql() + if (string.IsNullOrEmpty(ForeignKey.Name)) { - if (ForeignKey.ForeignTable == null) - throw new ArgumentNullException("Table name not specified, ensure you have appended the OnTable extension. Format should be Delete.ForeignKey(KeyName).OnTable(TableName)"); - - if (string.IsNullOrEmpty(ForeignKey.Name)) - { - ForeignKey.Name = $"FK_{ForeignKey.ForeignTable}_{ForeignKey.PrimaryTable}_{ForeignKey.PrimaryColumns.First()}"; - } - - return string.Format(SqlSyntax.DeleteConstraint, - SqlSyntax.GetQuotedTableName(ForeignKey.ForeignTable), - SqlSyntax.GetQuotedName(ForeignKey.Name)); + ForeignKey.Name = + $"FK_{ForeignKey.ForeignTable}_{ForeignKey.PrimaryTable}_{ForeignKey.PrimaryColumns.First()}"; } + + return string.Format( + SqlSyntax.DeleteConstraint, + SqlSyntax.GetQuotedTableName(ForeignKey.ForeignTable), + SqlSyntax.GetQuotedName(ForeignKey.Name)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs index dd3c41dd2c..c03e74bf67 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteIndexExpression.cs @@ -1,28 +1,22 @@ -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteIndexExpression : MigrationExpressionBase { - public class DeleteIndexExpression : MigrationExpressionBase - { - public DeleteIndexExpression(IMigrationContext context) - : base(context) - { - Index = new IndexDefinition(); - } + public DeleteIndexExpression(IMigrationContext context) + : base(context) => + Index = new IndexDefinition(); - public DeleteIndexExpression(IMigrationContext context, IndexDefinition index) - : base(context) - { - Index = index; - } + public DeleteIndexExpression(IMigrationContext context, IndexDefinition index) + : base(context) => + Index = index; - public IndexDefinition Index { get; } + public IndexDefinition Index { get; } - protected override string GetSql() - { - return string.Format(SqlSyntax.DropIndex, - SqlSyntax.GetQuotedName(Index.Name), - SqlSyntax.GetQuotedTableName(Index.TableName)); - } - } + protected override string GetSql() => + string.Format( + SqlSyntax.DropIndex, + SqlSyntax.GetQuotedName(Index.Name), + SqlSyntax.GetQuotedTableName(Index.TableName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs index 75d453eb88..84487ef5f5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Expressions/DeleteTableExpression.cs @@ -1,17 +1,16 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; + +public class DeleteTableExpression : MigrationExpressionBase { - public class DeleteTableExpression : MigrationExpressionBase + public DeleteTableExpression(IMigrationContext context) + : base(context) { - public DeleteTableExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - - protected override string GetSql() - { - return string.Format(SqlSyntax.DropTable, - SqlSyntax.GetQuotedTableName(TableName)); - } } + + public virtual string? TableName { get; set; } + + protected override string GetSql() => + string.Format( + SqlSyntax.DropTable, + SqlSyntax.GetQuotedTableName(TableName)); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs index 74bee1a440..b98cc3a389 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/DeleteForeignKeyBuilder.cs @@ -1,72 +1,77 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Implements IDeleteForeignKey... +/// +public class DeleteForeignKeyBuilder : ExpressionBuilderBase, + IDeleteForeignKeyFromTableBuilder, + IDeleteForeignKeyForeignColumnBuilder, + IDeleteForeignKeyToTableBuilder, + IDeleteForeignKeyPrimaryColumnBuilder, + IDeleteForeignKeyOnTableBuilder { - /// - /// Implements IDeleteForeignKey... - /// - public class DeleteForeignKeyBuilder : ExpressionBuilderBase, - IDeleteForeignKeyFromTableBuilder, - IDeleteForeignKeyForeignColumnBuilder, - IDeleteForeignKeyToTableBuilder, - IDeleteForeignKeyPrimaryColumnBuilder, - IDeleteForeignKeyOnTableBuilder + public DeleteForeignKeyBuilder(DeleteForeignKeyExpression expression) + : base(expression) { - public DeleteForeignKeyBuilder(DeleteForeignKeyExpression expression) - : base(expression) - { } + } - /// - public IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName) - { - Expression.ForeignKey.ForeignTable = foreignTableName; - return this; - } + /// + public IDeleteForeignKeyToTableBuilder ForeignColumn(string column) + { + Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } - /// - public IDeleteForeignKeyToTableBuilder ForeignColumn(string column) + /// + public IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.ForeignColumns.Add(column); - return this; } - /// - public IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.ForeignColumns.Add(column); + return this; + } - return this; - } + /// + public IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName) + { + Expression.ForeignKey.ForeignTable = foreignTableName; + return this; + } - /// - public IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table) - { - Expression.ForeignKey.PrimaryTable = table; - return this; - } + /// + public IExecutableBuilder OnTable(string foreignTableName) + { + Expression.ForeignKey.ForeignTable = foreignTableName; + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder PrimaryColumn(string column) + /// + public IExecutableBuilder PrimaryColumn(string column) + { + Expression.ForeignKey.PrimaryColumns.Add(column); + return new ExecutableBuilder(Expression); + } + + /// + public IExecutableBuilder PrimaryColumns(params string[] columns) + { + foreach (var column in columns) { Expression.ForeignKey.PrimaryColumns.Add(column); - return new ExecutableBuilder(Expression); } - /// - public IExecutableBuilder PrimaryColumns(params string[] columns) - { - foreach (var column in columns) - Expression.ForeignKey.PrimaryColumns.Add(column); - return new ExecutableBuilder(Expression); - } + return new ExecutableBuilder(Expression); + } - /// - public IExecutableBuilder OnTable(string foreignTableName) - { - Expression.ForeignKey.ForeignTable = foreignTableName; - return new ExecutableBuilder(Expression); - } + /// + public IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table) + { + Expression.ForeignKey.PrimaryTable = table; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs index 3b17700218..1c2c7c1d34 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyForeignColumnBuilder.cs @@ -1,18 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyForeignColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the foreign column. /// - public interface IDeleteForeignKeyForeignColumnBuilder : IFluentBuilder - { - /// - /// Specifies the foreign column. - /// - IDeleteForeignKeyToTableBuilder ForeignColumn(string column); + IDeleteForeignKeyToTableBuilder ForeignColumn(string column); - /// - /// Specifies the foreign columns. - /// - IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns); - } + /// + /// Specifies the foreign columns. + /// + IDeleteForeignKeyToTableBuilder ForeignColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs index 6d422ad535..c10f184835 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyFromTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyFromTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the source table of the foreign key. /// - public interface IDeleteForeignKeyFromTableBuilder : IFluentBuilder - { - /// - /// Specifies the source table of the foreign key. - /// - IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName); - } + IDeleteForeignKeyForeignColumnBuilder FromTable(string foreignTableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs index 19dd14f36e..1a40fb2f02 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyOnTableBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyOnTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the foreign key. /// - public interface IDeleteForeignKeyOnTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the foreign key. - /// - IExecutableBuilder OnTable(string foreignTableName); - } + IExecutableBuilder OnTable(string foreignTableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs index c44696b45d..8dcedb08d9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyPrimaryColumnBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyPrimaryColumnBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the target primary column. /// - public interface IDeleteForeignKeyPrimaryColumnBuilder : IFluentBuilder - { - /// - /// Specifies the target primary column. - /// - IExecutableBuilder PrimaryColumn(string column); + IExecutableBuilder PrimaryColumn(string column); - /// - /// Specifies the target primary columns. - /// - IExecutableBuilder PrimaryColumns(params string[] columns); - } + /// + /// Specifies the target primary columns. + /// + IExecutableBuilder PrimaryColumns(params string[] columns); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs index 6588b7a18a..6a3f3147a3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/ForeignKey/IDeleteForeignKeyToTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteForeignKeyToTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the target table of the foreign key. /// - public interface IDeleteForeignKeyToTableBuilder : IFluentBuilder - { - /// - /// Specifies the target table of the foreign key. - /// - IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table); - } + IDeleteForeignKeyPrimaryColumnBuilder ToTable(string table); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs index 0b8da10097..e2d06fd71f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/IDeleteBuilder.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Constraint; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Data; @@ -6,73 +6,72 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.DefaultConstraint using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.ForeignKey; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table to delete. /// - public interface IDeleteBuilder : IFluentBuilder - { - /// - /// Specifies the table to delete. - /// - IExecutableBuilder Table(string tableName); + IExecutableBuilder Table(string tableName); - /// - /// Builds a Delete Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true); + /// + /// Builds a Delete Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(bool local = true, bool foreign = true); - /// - /// Builds a Delete Keys and Indexes expression, and executes. - /// - IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true); + /// + /// Builds a Delete Keys and Indexes expression, and executes. + /// + IExecutableBuilder KeysAndIndexes(string tableName, bool local = true, bool foreign = true); - /// - /// Specifies the column to delete. - /// - IDeleteColumnBuilder Column(string columnName); + /// + /// Specifies the column to delete. + /// + IDeleteColumnBuilder Column(string columnName); - /// - /// Specifies the foreign key to delete. - /// - IDeleteForeignKeyFromTableBuilder ForeignKey(); + /// + /// Specifies the foreign key to delete. + /// + IDeleteForeignKeyFromTableBuilder ForeignKey(); - /// - /// Specifies the foreign key to delete. - /// - IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName); + /// + /// Specifies the foreign key to delete. + /// + IDeleteForeignKeyOnTableBuilder ForeignKey(string foreignKeyName); - /// - /// Specifies the table to delete data from. - /// - /// - /// - IDeleteDataBuilder FromTable(string tableName); + /// + /// Specifies the table to delete data from. + /// + /// + /// + IDeleteDataBuilder FromTable(string tableName); - /// - /// Specifies the index to delete. - /// - IDeleteIndexForTableBuilder Index(); + /// + /// Specifies the index to delete. + /// + IDeleteIndexForTableBuilder Index(); - /// - /// Specifies the index to delete. - /// - IDeleteIndexForTableBuilder Index(string indexName); + /// + /// Specifies the index to delete. + /// + IDeleteIndexForTableBuilder Index(string indexName); - /// - /// Specifies the primary key to delete. - /// - IDeleteConstraintBuilder PrimaryKey(string primaryKeyName); + /// + /// Specifies the primary key to delete. + /// + IDeleteConstraintBuilder PrimaryKey(string primaryKeyName); - /// - /// Specifies the unique constraint to delete. - /// - IDeleteConstraintBuilder UniqueConstraint(string constraintName); + /// + /// Specifies the unique constraint to delete. + /// + IDeleteConstraintBuilder UniqueConstraint(string constraintName); - /// - /// Specifies the default constraint to delete. - /// - IDeleteDefaultConstraintOnTableBuilder DefaultConstraint(); - } + /// + /// Specifies the default constraint to delete. + /// + IDeleteDefaultConstraintOnTableBuilder DefaultConstraint(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs index e55b1e3d8f..0e6c57c32e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/DeleteIndexBuilder.cs @@ -1,22 +1,22 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; + +public class DeleteIndexBuilder : ExpressionBuilderBase, + IDeleteIndexForTableBuilder, IExecutableBuilder { - public class DeleteIndexBuilder : ExpressionBuilderBase, - IDeleteIndexForTableBuilder, IExecutableBuilder + public DeleteIndexBuilder(DeleteIndexExpression expression) + : base(expression) { - public DeleteIndexBuilder(DeleteIndexExpression expression) - : base(expression) - { } - - /// - public void Do() => Expression.Execute(); - - public IExecutableBuilder OnTable(string tableName) - { - Expression.Index.TableName = tableName; - return this; - } } + + public IExecutableBuilder OnTable(string tableName) + { + Expression.Index.TableName = tableName; + return this; + } + + /// + public void Do() => Expression.Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs index f99e0d1ea0..d649cd502b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/Index/IDeleteIndexForTableBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.Index; + +/// +/// Builds a Delete expression. +/// +public interface IDeleteIndexForTableBuilder : IFluentBuilder { /// - /// Builds a Delete expression. + /// Specifies the table of the index to delete. /// - public interface IDeleteIndexForTableBuilder : IFluentBuilder - { - /// - /// Specifies the table of the index to delete. - /// - IExecutableBuilder OnTable(string tableName); - } + IExecutableBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 90ade43d6d..ffffb58f9e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -1,102 +1,109 @@ -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Delete.KeysAndIndexes; + +/// +/// +/// Assuming we stick with the current migrations setup this will need to be altered to +/// delegate to SQL syntax provider (we can drop indexes but not PK/FK). +/// +/// +/// 1. For SQLite, rename table.
+/// 2. Create new table with expected keys.
+/// 3. Insert into new from renamed
+/// 4. Drop renamed.
+///
+/// +/// Read more SQL Features That SQLite Does Not Implement +/// +///
+public class DeleteKeysAndIndexesBuilder : IExecutableBuilder { - /// - /// - /// Assuming we stick with the current migrations setup this will need to be altered to - /// delegate to SQL syntax provider (we can drop indexes but not PK/FK). - /// - /// - /// 1. For SQLite, rename table.
- /// 2. Create new table with expected keys.
- /// 3. Insert into new from renamed
- /// 4. Drop renamed.
- ///
- /// - /// Read more SQL Features That SQLite Does Not Implement - /// - ///
- public class DeleteKeysAndIndexesBuilder : IExecutableBuilder + private readonly IMigrationContext _context; + private readonly DatabaseType[] _supportedDatabaseTypes; + + public DeleteKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) { - private readonly IMigrationContext _context; - private readonly DatabaseType[] _supportedDatabaseTypes; + _context = context; + _supportedDatabaseTypes = supportedDatabaseTypes; + } - public DeleteKeysAndIndexesBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes) + public string? TableName { get; set; } + + public bool DeleteLocal { get; set; } + + public bool DeleteForeign { get; set; } + + private IDeleteBuilder Delete => new DeleteBuilder(_context); + + /// + public void Do() + { + _context.BuildingExpression = false; + + // get a list of all constraints - this will include all PK, FK and unique constraints + var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database) + .DistinctBy(x => x.Item2).ToList(); + + // get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes + var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database) + .DistinctBy(x => x.IndexName).ToList(); + + IEnumerable uniqueConstraintNames = tableConstraints + .Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")) + .Select(x => x.Item2); + var indexNames = indexes.Select(x => x.IndexName).ToList(); + + // drop keys + if (DeleteLocal || DeleteForeign) { - _context = context; - _supportedDatabaseTypes = supportedDatabaseTypes; - } - - public string? TableName { get; set; } - - public bool DeleteLocal { get; set; } - - public bool DeleteForeign { get; set; } - - /// - public void Do() - { - _context.BuildingExpression = false; - - //get a list of all constraints - this will include all PK, FK and unique constraints - var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); - - //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); - - var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2); - var indexNames = indexes.Select(x => x.IndexName).ToList(); - - // drop keys - if (DeleteLocal || DeleteForeign) + // table, constraint + if (DeleteForeign) { - // table, constraint - - if (DeleteForeign) + // In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: + // * starts with FK OR + // * doesn't start with PK_ and doesn't exist in the list of indexes + foreach (Tuple key in tableConstraints.Where(x => x.Item1 == TableName + && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && + !indexNames.InvariantContains(x.Item2))))) { - //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: - // * starts with FK OR - // * doesn't start with PK_ and doesn't exist in the list of indexes - - foreach (var key in tableConstraints.Where(x => x.Item1 == TableName - && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2))))) - { - Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); - } - - } - if (DeleteLocal) - { - foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) - Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); - - // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways - // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn + Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); } } - // drop indexes if (DeleteLocal) { - foreach (var index in indexes.Where(x => x.TableName == TableName)) + foreach (Tuple key in tableConstraints.Where(x => + x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) { - //if this is a unique constraint we need to drop the constraint, else drop the index - //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints - - if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) - Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); - else - Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); } + // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways + // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn } } - private IDeleteBuilder Delete => new DeleteBuilder(_context); + // drop indexes + if (DeleteLocal) + { + foreach (DbIndexDefinition index in indexes.Where(x => x.TableName == TableName)) + { + // if this is a unique constraint we need to drop the constraint, else drop the index + // to figure this out, the index must be tagged as unique and it must exist in the tableConstraints + if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) + { + Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); + } + else + { + Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + } + } + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs index f483ec6402..e31d4aa497 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/ExecuteBuilder.cs @@ -1,41 +1,44 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute; + +public class ExecuteBuilder : ExpressionBuilderBase, + IExecuteBuilder, IExecutableBuilder { - public class ExecuteBuilder : ExpressionBuilderBase, - IExecuteBuilder, IExecutableBuilder + public ExecuteBuilder(IMigrationContext context) + : base(new ExecuteSqlStatementExpression(context)) { - public ExecuteBuilder(IMigrationContext context) - : base(new ExecuteSqlStatementExpression(context)) - { } + } - /// - public void Do() + /// + public void Do() + { + // slightly awkward, but doing it right would mean a *lot* + // of changes for MigrationExpressionBase + if (Expression.SqlObject == null) { - // slightly awkward, but doing it right would mean a *lot* - // of changes for MigrationExpressionBase - - if (Expression.SqlObject == null) - Expression.Execute(); - else - Expression.ExecuteSqlObject(); + Expression.Execute(); } - - /// - public IExecutableBuilder Sql(string sqlStatement) + else { - Expression.SqlStatement = sqlStatement; - return this; - } - - /// - public IExecutableBuilder Sql(Sql sql) - { - Expression.SqlObject = sql; - return this; + Expression.ExecuteSqlObject(); } } + + /// + public IExecutableBuilder Sql(string sqlStatement) + { + Expression.SqlStatement = sqlStatement; + return this; + } + + /// + public IExecutableBuilder Sql(Sql sql) + { + Expression.SqlObject = sql; + return this; + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs index 4e9186ace9..30b49f9664 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/Expressions/ExecuteSqlStatementExpression.cs @@ -1,26 +1,20 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; + +public class ExecuteSqlStatementExpression : MigrationExpressionBase { - public class ExecuteSqlStatementExpression : MigrationExpressionBase + public ExecuteSqlStatementExpression(IMigrationContext context) + : base(context) { - public ExecuteSqlStatementExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? SqlStatement { get; set; } - - public virtual Sql? SqlObject { get; set; } - - public void ExecuteSqlObject() - { - Execute(SqlObject); - } - - protected override string? GetSql() - { - return SqlStatement; - } } + + public virtual string? SqlStatement { get; set; } + + public virtual Sql? SqlObject { get; set; } + + public void ExecuteSqlObject() => Execute(SqlObject); + + protected override string? GetSql() => SqlStatement; } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs index 54a1f6a768..36b9460429 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Execute/IExecuteBuilder.cs @@ -1,23 +1,22 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute; + +/// +/// Builds and executes an Sql statement. +/// +/// Deals with multi-statements Sql. +public interface IExecuteBuilder : IFluentBuilder { /// - /// Builds and executes an Sql statement. + /// Specifies the Sql statement to execute. /// - /// Deals with multi-statements Sql. - public interface IExecuteBuilder : IFluentBuilder - { - /// - /// Specifies the Sql statement to execute. - /// - IExecutableBuilder Sql(string sqlStatement); + IExecutableBuilder Sql(string sqlStatement); - /// - /// Specifies the Sql statement to execute. - /// - IExecutableBuilder Sql(Sql sql); - } + /// + /// Specifies the Sql statement to execute. + /// + IExecutableBuilder Sql(Sql sql); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs index a3bed8b5d8..f751861ee7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBase.cs @@ -1,22 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +/// +/// Provides a base class for expression builders. +/// +public abstract class ExpressionBuilderBase + where TExpression : IMigrationExpression { /// - /// Provides a base class for expression builders. + /// Initializes a new instance of the class. /// - public abstract class ExpressionBuilderBase - where TExpression : IMigrationExpression - { - /// - /// Initializes a new instance of the class. - /// - protected ExpressionBuilderBase(TExpression expression) - { - Expression = expression; - } + protected ExpressionBuilderBase(TExpression expression) => Expression = expression; - /// - /// Gets the expression. - /// - public TExpression Expression { get; } - } + /// + /// Gets the expression. + /// + public TExpression Expression { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs index 74f1676def..04737ba019 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/ExpressionBuilderBaseOfNext.cs @@ -1,284 +1,283 @@ -using System.Data; +using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +/// +/// Provides a base class for expression builders. +/// +public abstract class ExpressionBuilderBase : ExpressionBuilderBase + where TExpression : IMigrationExpression + where TNext : IFluentBuilder { /// - /// Provides a base class for expression builders. + /// Initializes a new instance of the class. /// - public abstract class ExpressionBuilderBase : ExpressionBuilderBase - where TExpression : IMigrationExpression - where TNext : IFluentBuilder + protected ExpressionBuilderBase(TExpression expression) + : base(expression) { - /// - /// Initializes a new instance of the class. - /// - protected ExpressionBuilderBase(TExpression expression) - : base(expression) + } + + private ColumnDefinition? Column => GetColumnForType(); + + public abstract ColumnDefinition? GetColumnForType(); + + public TNext AsAnsiString() + { + if (Column is not null) { + Column.Type = DbType.AnsiString; } - public abstract ColumnDefinition? GetColumnForType(); + return (TNext)(object)this; + } - private ColumnDefinition? Column => GetColumnForType(); - - public TNext AsAnsiString() + public TNext AsAnsiString(int size) + { + if (Column is not null) { - if (Column is not null) - { - Column.Type = DbType.AnsiString; - } - - return (TNext)(object)this; + Column.Type = DbType.AnsiString; + Column.Size = size; } - public TNext AsAnsiString(int size) - { - if (Column is not null) - { - Column.Type = DbType.AnsiString; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBinary() + { + if (Column is not null) + { + Column.Type = DbType.Binary; } - public TNext AsBinary() - { - if (Column is not null) - { - Column.Type = DbType.Binary; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBinary(int size) + { + if (Column is not null) + { + Column.Type = DbType.Binary; + Column.Size = size; } - public TNext AsBinary(int size) - { - if (Column is not null) - { - Column.Type = DbType.Binary; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsBoolean() + { + if (Column is not null) + { + Column.Type = DbType.Boolean; } - public TNext AsBoolean() - { - if (Column is not null) - { - Column.Type = DbType.Boolean; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsByte() + { + if (Column is not null) + { + Column.Type = DbType.Byte; } - public TNext AsByte() - { - if (Column is not null) - { - Column.Type = DbType.Byte; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsCurrency() + { + if (Column is not null) + { + Column.Type = DbType.Currency; } - public TNext AsCurrency() - { - if (Column is not null) - { - Column.Type = DbType.Currency; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDate() + { + if (Column is not null) + { + Column.Type = DbType.Date; } - public TNext AsDate() - { - if (Column is not null) - { - Column.Type = DbType.Date; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDateTime() + { + if (Column is not null) + { + Column.Type = DbType.DateTime; } - public TNext AsDateTime() - { - if (Column is not null) - { - Column.Type = DbType.DateTime; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDecimal() + { + if (Column is not null) + { + Column.Type = DbType.Decimal; } - public TNext AsDecimal() - { - if (Column is not null) - { - Column.Type = DbType.Decimal; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDecimal(int size, int precision) + { + if (Column is not null) + { + Column.Type = DbType.Decimal; + Column.Size = size; + Column.Precision = precision; } - public TNext AsDecimal(int size, int precision) - { - if (Column is not null) - { - Column.Type = DbType.Decimal; - Column.Size = size; - Column.Precision = precision; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsDouble() + { + if (Column is not null) + { + Column.Type = DbType.Double; } - public TNext AsDouble() - { - if (Column is not null) - { - Column.Type = DbType.Double; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFixedLengthString(int size) + { + if (Column is not null) + { + Column.Type = DbType.StringFixedLength; + Column.Size = size; } - public TNext AsFixedLengthString(int size) - { - if (Column is not null) - { - Column.Type = DbType.StringFixedLength; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFixedLengthAnsiString(int size) + { + if (Column is not null) + { + Column.Type = DbType.AnsiStringFixedLength; + Column.Size = size; } - public TNext AsFixedLengthAnsiString(int size) - { - if (Column is not null) - { - Column.Type = DbType.AnsiStringFixedLength; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsFloat() + { + if (Column is not null) + { + Column.Type = DbType.Single; } - public TNext AsFloat() - { - if (Column is not null) - { - Column.Type = DbType.Single; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsGuid() + { + if (Column is not null) + { + Column.Type = DbType.Guid; } - public TNext AsGuid() - { - if (Column is not null) - { - Column.Type = DbType.Guid; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt16() + { + if (Column is not null) + { + Column.Type = DbType.Int16; } - public TNext AsInt16() - { - if (Column is not null) - { - Column.Type = DbType.Int16; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt32() + { + if (Column is not null) + { + Column.Type = DbType.Int32; } - public TNext AsInt32() - { - if (Column is not null) - { - Column.Type = DbType.Int32; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsInt64() + { + if (Column is not null) + { + Column.Type = DbType.Int64; } - public TNext AsInt64() - { - if (Column is not null) - { - Column.Type = DbType.Int64; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsString() + { + if (Column is not null) + { + Column.Type = DbType.String; } - public TNext AsString() - { - if (Column is not null) - { - Column.Type = DbType.String; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsString(int size) + { + if (Column is not null) + { + Column.Type = DbType.String; + Column.Size = size; } - public TNext AsString(int size) - { - if (Column is not null) - { - Column.Type = DbType.String; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsTime() + { + if (Column is not null) + { + Column.Type = DbType.Time; } - public TNext AsTime() - { - if (Column is not null) - { - Column.Type = DbType.Time; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsXml() + { + if (Column is not null) + { + Column.Type = DbType.Xml; } - public TNext AsXml() - { - if (Column is not null) - { - Column.Type = DbType.Xml; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsXml(int size) + { + if (Column is not null) + { + Column.Type = DbType.Xml; + Column.Size = size; } - public TNext AsXml(int size) - { - if (Column is not null) - { - Column.Type = DbType.Xml; - Column.Size = size; - } + return (TNext)(object)this; + } - return (TNext)(object)this; + public TNext AsCustom(string customType) + { + if (Column is not null) + { + Column.Type = null; + Column.CustomType = customType; } - public TNext AsCustom(string customType) - { - if (Column is not null) - { - Column.Type = null; - Column.CustomType = customType; - } - - return (TNext)(object)this; - } + return (TNext)(object)this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs index 8ad08b5733..b6f3dc07b2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/IFluentBuilder.cs @@ -1,5 +1,5 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions; + +public interface IFluentBuilder { - public interface IFluentBuilder - { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs index 75664b701e..abcd9714eb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs @@ -1,68 +1,69 @@ -using System.Collections.Generic; using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; + +public class InsertDataExpression : MigrationExpressionBase { - public class InsertDataExpression : MigrationExpressionBase + public InsertDataExpression(IMigrationContext context) + : base(context) { - public InsertDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } - public bool EnabledIdentityInsert { get; set; } + public string? TableName { get; set; } - public List Rows { get; } = new List(); + public bool EnabledIdentityInsert { get; set; } - protected override string GetSql() + public List Rows { get; } = new(); + + protected override string GetSql() + { + var stmts = new StringBuilder(); + + if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) { - var stmts = new StringBuilder(); + stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} ON"); + AppendStatementSeparator(stmts); + } - if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) + try + { + foreach (InsertionDataDefinition item in Rows) { - stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} ON"); - AppendStatementSeparator(stmts); - } - - try - { - foreach (var item in Rows) + var cols = new StringBuilder(); + var vals = new StringBuilder(); + var first = true; + foreach (KeyValuePair keyVal in item) { - var cols = new StringBuilder(); - var vals = new StringBuilder(); - var first = true; - foreach (var keyVal in item) + if (first) { - if (first) - { - first = false; - } - else - { - cols.Append(","); - vals.Append(","); - } - cols.Append(SqlSyntax.GetQuotedColumnName(keyVal.Key)); - vals.Append(GetQuotedValue(keyVal.Value)); + first = false; + } + else + { + cols.Append(","); + vals.Append(","); } - var sql = string.Format(SqlSyntax.InsertData, SqlSyntax.GetQuotedTableName(TableName), cols, vals); - - stmts.Append(sql); - AppendStatementSeparator(stmts); + cols.Append(SqlSyntax.GetQuotedColumnName(keyVal.Key)); + vals.Append(GetQuotedValue(keyVal.Value)); } - } - finally - { - if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) - { - stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} OFF"); - AppendStatementSeparator(stmts); - } - } - return stmts.ToString(); + var sql = string.Format(SqlSyntax.InsertData, SqlSyntax.GetQuotedTableName(TableName), cols, vals); + + stmts.Append(sql); + AppendStatementSeparator(stmts); + } } + finally + { + if (EnabledIdentityInsert && SqlSyntax.SupportsIdentityInsert()) + { + stmts.AppendLine($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(TableName)} OFF"); + AppendStatementSeparator(stmts); + } + } + + return stmts.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs index 407a7a02f1..9178988b9c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Builds an Insert expression. +/// +public interface IInsertBuilder : IFluentBuilder { /// - /// Builds an Insert expression. + /// Specifies the table to insert into. /// - public interface IInsertBuilder : IFluentBuilder - { - /// - /// Specifies the table to insert into. - /// - IInsertIntoBuilder IntoTable(string tableName); - } + IInsertIntoBuilder IntoTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs index dfe4ba7909..d320231dc1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/IInsertIntoBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Builds an Insert Into expression. +/// +public interface IInsertIntoBuilder : IFluentBuilder, IExecutableBuilder { /// - /// Builds an Insert Into expression. + /// Enables identity insert. /// - public interface IInsertIntoBuilder : IFluentBuilder, IExecutableBuilder - { - /// - /// Enables identity insert. - /// - IInsertIntoBuilder EnableIdentityInsert(); + IInsertIntoBuilder EnableIdentityInsert(); - /// - /// Specifies a row to be inserted. - /// - IInsertIntoBuilder Row(object dataAsAnonymousType); - } + /// + /// Specifies a row to be inserted. + /// + IInsertIntoBuilder Row(object dataAsAnonymousType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs index ddae2d5325..18f4e580b8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertBuilder.cs @@ -1,24 +1,20 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Implements . +/// +public class InsertBuilder : IInsertBuilder { - /// - /// Implements . - /// - public class InsertBuilder : IInsertBuilder + private readonly IMigrationContext _context; + + public InsertBuilder(IMigrationContext context) => _context = context; + + /// + public IInsertIntoBuilder IntoTable(string tableName) { - private readonly IMigrationContext _context; - - public InsertBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IInsertIntoBuilder IntoTable(string tableName) - { - var expression = new InsertDataExpression(_context) { TableName = tableName }; - return new InsertIntoBuilder(expression); - } + var expression = new InsertDataExpression(_context) { TableName = tableName }; + return new InsertIntoBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs index 8d27877230..e353d3b782 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Insert/InsertIntoBuilder.cs @@ -1,45 +1,47 @@ -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert.Expressions; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Insert; + +/// +/// Implements . +/// +public class InsertIntoBuilder : ExpressionBuilderBase, + IInsertIntoBuilder { - /// - /// Implements . - /// - public class InsertIntoBuilder : ExpressionBuilderBase, - IInsertIntoBuilder + public InsertIntoBuilder(InsertDataExpression expression) + : base(expression) { - public InsertIntoBuilder(InsertDataExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IInsertIntoBuilder EnableIdentityInsert() + /// + public IInsertIntoBuilder EnableIdentityInsert() + { + Expression.EnabledIdentityInsert = true; + return this; + } + + /// + public IInsertIntoBuilder Row(object dataAsAnonymousType) + { + Expression.Rows.Add(GetData(dataAsAnonymousType)); + return this; + } + + private static InsertionDataDefinition GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + + var data = new InsertionDataDefinition(); + foreach (PropertyDescriptor property in properties) { - Expression.EnabledIdentityInsert = true; - return this; + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } - /// - public IInsertIntoBuilder Row(object dataAsAnonymousType) - { - Expression.Rows.Add(GetData(dataAsAnonymousType)); - return this; - } - - private static InsertionDataDefinition GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - - var data = new InsertionDataDefinition(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; - } + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs index 76a3c06946..656ace382d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +/// +/// Builds a Rename Column expression. +/// +public interface IRenameColumnBuilder : IFluentBuilder { /// - /// Builds a Rename Column expression. + /// Specifies the table name. /// - public interface IRenameColumnBuilder : IFluentBuilder - { - /// - /// Specifies the table name. - /// - IRenameColumnToBuilder OnTable(string tableName); - } + IRenameColumnToBuilder OnTable(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs index 5580226c1f..33a02805c8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/IRenameColumnToBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +/// +/// Builds a Rename Column expression. +/// +public interface IRenameColumnToBuilder : IFluentBuilder { /// - /// Builds a Rename Column expression. + /// Specifies the new name of the column. /// - public interface IRenameColumnToBuilder : IFluentBuilder - { - /// - /// Specifies the new name of the column. - /// - IExecutableBuilder To(string name); - } + IExecutableBuilder To(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs index a3a181c5df..182e997d01 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Column/RenameColumnBuilder.cs @@ -1,30 +1,30 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; + +public class RenameColumnBuilder : ExpressionBuilderBase, + IRenameColumnToBuilder, IRenameColumnBuilder, IExecutableBuilder { - public class RenameColumnBuilder : ExpressionBuilderBase, - IRenameColumnToBuilder, IRenameColumnBuilder, IExecutableBuilder + public RenameColumnBuilder(RenameColumnExpression expression) + : base(expression) { - public RenameColumnBuilder(RenameColumnExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IExecutableBuilder To(string name) - { - Expression.NewName = name; - return this; - } + /// + public IRenameColumnToBuilder OnTable(string tableName) + { + Expression.TableName = tableName; + return this; + } - /// - public IRenameColumnToBuilder OnTable(string tableName) - { - Expression.TableName = tableName; - return this; - } + /// + public IExecutableBuilder To(string name) + { + Expression.NewName = name; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs index cafbc45108..bdfb4727da 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameColumnExpression.cs @@ -1,19 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; + +public class RenameColumnExpression : MigrationExpressionBase { - public class RenameColumnExpression : MigrationExpressionBase + public RenameColumnExpression(IMigrationContext context) + : base(context) { - public RenameColumnExpression(IMigrationContext context) - : base(context) - { } - - public virtual string? TableName { get; set; } - public virtual string? OldName { get; set; } - public virtual string? NewName { get; set; } - - /// - protected override string GetSql() - { - return SqlSyntax.FormatColumnRename(TableName, OldName, NewName); - } } + + public virtual string? TableName { get; set; } + + public virtual string? OldName { get; set; } + + public virtual string? NewName { get; set; } + + /// + protected override string GetSql() => SqlSyntax.FormatColumnRename(TableName, OldName, NewName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs index 77f9de03b3..c7b7704eb7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Expressions/RenameTableExpression.cs @@ -1,29 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; + +/// +/// Represents a Rename Table expression. +/// +public class RenameTableExpression : MigrationExpressionBase { - /// - /// Represents a Rename Table expression. - /// - public class RenameTableExpression : MigrationExpressionBase + public RenameTableExpression(IMigrationContext context) + : base(context) { - public RenameTableExpression(IMigrationContext context) - : base(context) - { } - - /// - /// Gets or sets the source name. - /// - public virtual string? OldName { get; set; } - - /// - /// Gets or sets the target name. - /// - public virtual string? NewName { get; set; } - - /// - /// - protected override string GetSql() - { - return SqlSyntax.FormatTableRename(OldName, NewName); - } } + + /// + /// Gets or sets the source name. + /// + public virtual string? OldName { get; set; } + + /// + /// Gets or sets the target name. + /// + public virtual string? NewName { get; set; } + + /// + /// + protected override string GetSql() => SqlSyntax.FormatTableRename(OldName, NewName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs index e93842ae2a..2d4d6661a9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/IRenameBuilder.cs @@ -1,21 +1,20 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename; + +/// +/// Builds a Rename expression. +/// +public interface IRenameBuilder : IFluentBuilder { /// - /// Builds a Rename expression. + /// Specifies the table to rename. /// - public interface IRenameBuilder : IFluentBuilder - { - /// - /// Specifies the table to rename. - /// - IRenameTableBuilder Table(string oldName); + IRenameTableBuilder Table(string oldName); - /// - /// Specifies the column to rename. - /// - IRenameColumnBuilder Column(string oldName); - } + /// + /// Specifies the column to rename. + /// + IRenameColumnBuilder Column(string oldName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs index c0b80f34bb..e21948a379 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/RenameBuilder.cs @@ -1,30 +1,26 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Column; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename; + +public class RenameBuilder : IRenameBuilder { - public class RenameBuilder : IRenameBuilder + private readonly IMigrationContext _context; + + public RenameBuilder(IMigrationContext context) => _context = context; + + /// + public IRenameTableBuilder Table(string oldName) { - private readonly IMigrationContext _context; + var expression = new RenameTableExpression(_context) { OldName = oldName }; + return new RenameTableBuilder(expression); + } - public RenameBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IRenameTableBuilder Table(string oldName) - { - var expression = new RenameTableExpression(_context) { OldName = oldName }; - return new RenameTableBuilder(expression); - } - - /// - public IRenameColumnBuilder Column(string oldName) - { - var expression = new RenameColumnExpression(_context) { OldName = oldName }; - return new RenameColumnBuilder(expression); - } + /// + public IRenameColumnBuilder Column(string oldName) + { + var expression = new RenameColumnExpression(_context) { OldName = oldName }; + return new RenameColumnBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs index 53f25a1b41..f2a202e497 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/IRenameTableBuilder.cs @@ -1,15 +1,14 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; + +/// +/// Builds a Rename Table expression. +/// +public interface IRenameTableBuilder : IFluentBuilder { /// - /// Builds a Rename Table expression. + /// Specifies the new name of the table. /// - public interface IRenameTableBuilder : IFluentBuilder - { - /// - /// Specifies the new name of the table. - /// - IExecutableBuilder To(string name); - } + IExecutableBuilder To(string name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs index af849b25d7..4fa9d4ad6d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Rename/Table/RenameTableBuilder.cs @@ -1,23 +1,23 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Rename.Table; + +public class RenameTableBuilder : ExpressionBuilderBase, + IRenameTableBuilder, IExecutableBuilder { - public class RenameTableBuilder : ExpressionBuilderBase, - IRenameTableBuilder, IExecutableBuilder + public RenameTableBuilder(RenameTableExpression expression) + : base(expression) { - public RenameTableBuilder(RenameTableExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IExecutableBuilder To(string name) - { - Expression.NewName = name; - return this; - } + /// + public IExecutableBuilder To(string name) + { + Expression.NewName = name; + return this; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs index 62b6f0acfb..fbe7e4d0d4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/Expressions/UpdateDataExpression.cs @@ -1,36 +1,37 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions +public class UpdateDataExpression : MigrationExpressionBase { - public class UpdateDataExpression : MigrationExpressionBase + public UpdateDataExpression(IMigrationContext context) + : base(context) { - public UpdateDataExpression(IMigrationContext context) - : base(context) - { } + } - public string? TableName { get; set; } + public string? TableName { get; set; } - public List>? Set { get; set; } - public List>? Where { get; set; } - public bool IsAllRows { get; set; } + public List>? Set { get; set; } - protected override string GetSql() - { - var updateItems = Set?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} = {GetQuotedValue(x.Value)}"); - var whereClauses = IsAllRows - ? null - : Where?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} {(x.Value == null ? "IS" : "=")} {GetQuotedValue(x.Value)}"); + public List>? Where { get; set; } - var whereClause = whereClauses == null - ? "(1=1)" - : string.Join(" AND ", whereClauses.ToArray()); + public bool IsAllRows { get; set; } - return string.Format(SqlSyntax.UpdateData, - SqlSyntax.GetQuotedTableName(TableName), - string.Join(", ", updateItems ?? Array.Empty()), - whereClause); - } + protected override string GetSql() + { + IEnumerable? updateItems = + Set?.Select(x => $"{SqlSyntax.GetQuotedColumnName(x.Key)} = {GetQuotedValue(x.Value)}"); + IEnumerable? whereClauses = IsAllRows + ? null + : Where?.Select(x => + $"{SqlSyntax.GetQuotedColumnName(x.Key)} {(x.Value == null ? "IS" : "=")} {GetQuotedValue(x.Value)}"); + + var whereClause = whereClauses == null + ? "(1=1)" + : string.Join(" AND ", whereClauses.ToArray()); + + return string.Format( + SqlSyntax.UpdateData, + SqlSyntax.GetQuotedTableName(TableName), + string.Join(", ", updateItems ?? Array.Empty()), + whereClause); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs index 16b1badf48..71849152e5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateBuilder : IFluentBuilder { /// - /// Builds an Update expression. + /// Specifies the table to update. /// - public interface IUpdateBuilder : IFluentBuilder - { - /// - /// Specifies the table to update. - /// - IUpdateTableBuilder Table(string tableName); - } + IUpdateTableBuilder Table(string tableName); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs index abd5201cc5..7754d0b691 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateTableBuilder.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateTableBuilder { /// - /// Builds an Update expression. + /// Specifies the data. /// - public interface IUpdateTableBuilder - { - /// - /// Specifies the data. - /// - IUpdateWhereBuilder Set(object dataAsAnonymousType); - } + IUpdateWhereBuilder Set(object dataAsAnonymousType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs index 378830cf0f..f81a444302 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/IUpdateWhereBuilder.cs @@ -1,20 +1,19 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +/// +/// Builds an Update expression. +/// +public interface IUpdateWhereBuilder { /// - /// Builds an Update expression. + /// Specifies rows to update. /// - public interface IUpdateWhereBuilder - { - /// - /// Specifies rows to update. - /// - IExecutableBuilder Where(object dataAsAnonymousType); + IExecutableBuilder Where(object dataAsAnonymousType); - /// - /// Specifies that all rows must be updated. - /// - IExecutableBuilder AllRows(); - } + /// + /// Specifies that all rows must be updated. + /// + IExecutableBuilder AllRows(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs index e47e31168a..75c5778cfe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateBuilder.cs @@ -1,21 +1,17 @@ -using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; +using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +public class UpdateBuilder : IUpdateBuilder { - public class UpdateBuilder : IUpdateBuilder + private readonly IMigrationContext _context; + + public UpdateBuilder(IMigrationContext context) => _context = context; + + /// + public IUpdateTableBuilder Table(string tableName) { - private readonly IMigrationContext _context; - - public UpdateBuilder(IMigrationContext context) - { - _context = context; - } - - /// - public IUpdateTableBuilder Table(string tableName) - { - var expression = new UpdateDataExpression(_context) { TableName = tableName }; - return new UpdateDataBuilder(expression); - } + var expression = new UpdateDataExpression(_context) { TableName = tableName }; + return new UpdateDataBuilder(expression); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs index fc7608f148..60fbea9552 100644 --- a/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Expressions/Update/UpdateDataBuilder.cs @@ -1,49 +1,51 @@ -using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update.Expressions; -namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update +namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; + +public class UpdateDataBuilder : ExpressionBuilderBase, + IUpdateTableBuilder, IUpdateWhereBuilder, IExecutableBuilder { - public class UpdateDataBuilder : ExpressionBuilderBase, - IUpdateTableBuilder, IUpdateWhereBuilder, IExecutableBuilder + public UpdateDataBuilder(UpdateDataExpression expression) + : base(expression) { - public UpdateDataBuilder(UpdateDataExpression expression) - : base(expression) - { } + } - /// - public void Do() => Expression.Execute(); + /// + public void Do() => Expression.Execute(); - /// - public IUpdateWhereBuilder Set(object dataAsAnonymousType) + /// + public IUpdateWhereBuilder Set(object dataAsAnonymousType) + { + Expression.Set = GetData(dataAsAnonymousType); + return this; + } + + /// + public IExecutableBuilder Where(object dataAsAnonymousType) + { + Expression.Where = GetData(dataAsAnonymousType); + return this; + } + + /// + public IExecutableBuilder AllRows() + { + Expression.IsAllRows = true; + return this; + } + + private static List> GetData(object dataAsAnonymousType) + { + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(dataAsAnonymousType); + + var data = new List>(); + foreach (PropertyDescriptor property in properties) { - Expression.Set = GetData(dataAsAnonymousType); - return this; + data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); } - /// - public IExecutableBuilder Where(object dataAsAnonymousType) - { - Expression.Where = GetData(dataAsAnonymousType); - return this; - } - - /// - public IExecutableBuilder AllRows() - { - Expression.IsAllRows = true; - return this; - } - - private static List> GetData(object dataAsAnonymousType) - { - var properties = TypeDescriptor.GetProperties(dataAsAnonymousType); - - var data = new List>(); - foreach (PropertyDescriptor property in properties) - data.Add(new KeyValuePair(property.Name, property.GetValue(dataAsAnonymousType))); - return data; - } + return data; } } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs index 087e04d41a..078bfcaf38 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationBuilder.cs @@ -1,9 +1,6 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +public interface IMigrationBuilder { - public interface IMigrationBuilder - { - MigrationBase Build(Type migrationType, IMigrationContext context); - } + MigrationBase Build(Type migrationType, IMigrationContext context); } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs index 9b164d38a3..6af5af23a3 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs @@ -1,47 +1,46 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides context to migrations. +/// +public interface IMigrationContext { /// - /// Provides context to migrations. + /// Gets the current migration plan /// - public interface IMigrationContext - { - /// - /// Gets the current migration plan - /// - MigrationPlan Plan { get; } + MigrationPlan Plan { get; } - /// - /// Gets the logger. - /// - ILogger Logger { get; } + /// + /// Gets the logger. + /// + ILogger Logger { get; } - /// - /// Gets the database instance. - /// - IUmbracoDatabase Database { get; } + /// + /// Gets the database instance. + /// + IUmbracoDatabase Database { get; } - /// - /// Gets the Sql context. - /// - ISqlContext SqlContext { get; } + /// + /// Gets the Sql context. + /// + ISqlContext SqlContext { get; } - /// - /// Gets or sets the expression index. - /// - int Index { get; set; } + /// + /// Gets or sets the expression index. + /// + int Index { get; set; } - /// - /// Gets or sets a value indicating whether an expression is being built. - /// - bool BuildingExpression { get; set; } + /// + /// Gets or sets a value indicating whether an expression is being built. + /// + bool BuildingExpression { get; set; } - /// - /// Adds a post-migration. - /// - void AddPostMigration() - where TMigration : MigrationBase; - } + /// + /// Adds a post-migration. + /// + void AddPostMigration() + where TMigration : MigrationBase; } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs index 3a5a4649fe..00756a3da2 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationExpression.cs @@ -1,10 +1,9 @@ -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Marker interface for migration expressions +/// +public interface IMigrationExpression { - /// - /// Marker interface for migration expressions - /// - public interface IMigrationExpression - { - void Execute(); - } + void Execute(); } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs index 41a831360a..552ca21b5e 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationPlanExecutor.cs @@ -1,10 +1,8 @@ -using System.Threading.Tasks; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Migrations +namespace Umbraco.Cms.Core.Migrations; + +public interface IMigrationPlanExecutor { - public interface IMigrationPlanExecutor - { - string Execute(MigrationPlan plan, string fromState); - } + string Execute(MigrationPlan plan, string fromState); } diff --git a/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs b/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs index 67d559c66d..963948d9f6 100644 --- a/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs +++ b/src/Umbraco.Infrastructure/Migrations/IncompleteMigrationExpressionException.cs @@ -1,49 +1,60 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// The exception that is thrown when a migration expression is not executed. +/// +/// +/// Migration expressions such as Alter.Table(...).Do() must end with Do(), else they are not executed. +/// When a non-executed expression is detected, an IncompleteMigrationExpressionException is thrown. +/// +/// +[Serializable] +public class IncompleteMigrationExpressionException : Exception { /// - /// The exception that is thrown when a migration expression is not executed. + /// Initializes a new instance of the class. /// - /// - /// Migration expressions such as Alter.Table(...).Do() must end with Do(), else they are not executed. - /// When a non-executed expression is detected, an IncompleteMigrationExpressionException is thrown. - /// - /// - [Serializable] - public class IncompleteMigrationExpressionException : Exception + public IncompleteMigrationExpressionException() { - /// - /// Initializes a new instance of the class. - /// - public IncompleteMigrationExpressionException() - { } + } - /// - /// Initializes a new instance of the class with a message. - /// - /// The message that describes the error. - public IncompleteMigrationExpressionException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a message. + /// + /// The message that describes the error. + public IncompleteMigrationExpressionException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. - public IncompleteMigrationExpressionException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// + /// The exception that is the cause of the current exception, or a null reference ( + /// in Visual Basic) if no inner exception is specified. + /// + public IncompleteMigrationExpressionException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected IncompleteMigrationExpressionException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + protected IncompleteMigrationExpressionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index c00a745d8e..bd661dfa4a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; @@ -12,1046 +10,2274 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Install -{ - /// - /// Creates the initial database data during install. - /// - internal class DatabaseDataCreator - { - private readonly IDatabase _database; - private readonly ILogger _logger; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _installDefaultDataSettings; +namespace Umbraco.Cms.Infrastructure.Migrations.Install; - private readonly IDictionary> _entitiesToAlwaysCreate = new Dictionary>() +/// +/// Creates the initial database data during install. +/// +internal class DatabaseDataCreator +{ + private readonly IDatabase _database; + + private readonly IDictionary> _entitiesToAlwaysCreate = new Dictionary> + { + { + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + new List { Constants.DataTypes.Guids.LabelString } + }, + }; + + private readonly IOptionsMonitor _installDefaultDataSettings; + private readonly ILogger _logger; + private readonly IUmbracoVersion _umbracoVersion; + + public DatabaseDataCreator(IDatabase database, ILogger logger, IUmbracoVersion umbracoVersion, + IOptionsMonitor installDefaultDataSettings) + { + _database = database; + _logger = logger; + _umbracoVersion = umbracoVersion; + _installDefaultDataSettings = installDefaultDataSettings; + } + + /// + /// Initialize the base data creation by inserting the data foundation for umbraco + /// specific to a table + /// + /// Name of the table to create base data for + public void InitializeBaseData(string tableName) + { + _logger.LogInformation("Creating data in {TableName}", tableName); + + if (tableName.Equals(Constants.DatabaseSchema.Tables.Node)) + { + CreateNodeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.Lock)) + { + CreateLockData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.ContentType)) + { + CreateContentTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.User)) + { + CreateUserData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.UserGroup)) + { + CreateUserGroupData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.User2UserGroup)) + { + CreateUser2UserGroupData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.UserGroup2App)) + { + CreateUserGroup2AppData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.PropertyTypeGroup)) + { + CreatePropertyTypeGroupData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.PropertyType)) + { + CreatePropertyTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.Language)) + { + CreateLanguageData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.ContentChildType)) + { + CreateContentChildTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.DataType)) + { + CreateDataTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.RelationType)) + { + CreateRelationTypeData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.KeyValue)) + { + CreateKeyValueData(); + } + + if (tableName.Equals(Constants.DatabaseSchema.Tables.LogViewerQuery)) + { + CreateLogViewerQueryData(); + } + + _logger.LogInformation("Completed creating data in {TableName}", tableName); + } + + internal static Guid CreateUniqueRelationTypeId(string alias, string name) => (alias + "____" + name).ToGuid(); + + private void CreateNodeData() + { + CreateNodeDataForDataTypes(); + CreateNodeDataForMediaTypes(); + CreateNodeDataForMemberTypes(); + } + + private void CreateNodeDataForDataTypes() + { + void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) + { + var nodeDto = new NodeDto { - { - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - new List - { - Cms.Core.Constants.DataTypes.Guids.LabelString, - } - } + NodeId = id, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1," + id, + SortOrder = sortOrder, + UniqueId = new Guid(uniqueId), + Text = text, + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, }; - public DatabaseDataCreator(IDatabase database, ILogger logger, IUmbracoVersion umbracoVersion, IOptionsMonitor installDefaultDataSettings) - { - _database = database; - _logger = logger; - _umbracoVersion = umbracoVersion; - _installDefaultDataSettings = installDefaultDataSettings; + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + uniqueId, + nodeDto, + Constants.DatabaseSchema.Tables.Node, + "id"); } - /// - /// Initialize the base data creation by inserting the data foundation for umbraco - /// specific to a table - /// - /// Name of the table to create base data for - public void InitializeBaseData(string tableName) + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -1, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1", + SortOrder = 0, + UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), + Text = "SYSTEM DATA: umbraco master root", + NodeObjectType = Constants.ObjectTypes.SystemRoot, + CreateDate = DateTime.Now, + }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -20, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1,-20", + SortOrder = 0, + UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), + Text = "Recycle Bin", + NodeObjectType = Constants.ObjectTypes.ContentRecycleBin, + CreateDate = DateTime.Now, + }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, + new NodeDto + { + NodeId = -21, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 0, + Path = "-1,-21", + SortOrder = 0, + UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), + Text = "Recycle Bin", + NodeObjectType = Constants.ObjectTypes.MediaRecycleBin, + CreateDate = DateTime.Now, + }); + + InsertDataTypeNodeDto(Constants.DataTypes.LabelString, 35, Constants.DataTypes.Guids.LabelString, + "Label (string)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelInt, 36, Constants.DataTypes.Guids.LabelInt, "Label (integer)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelBigint, 36, Constants.DataTypes.Guids.LabelBigInt, + "Label (bigint)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, + "Label (datetime)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, + "Label (decimal)"); + + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Upload, + new NodeDto + { + NodeId = Constants.DataTypes.Upload, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Upload}", + SortOrder = 34, + UniqueId = Constants.DataTypes.Guids.UploadGuid, + Text = "Upload File", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadVideo, + new NodeDto + { + NodeId = Constants.DataTypes.UploadVideo, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadVideo}", + SortOrder = 35, + UniqueId = Constants.DataTypes.Guids.UploadVideoGuid, + Text = "Upload Video", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadAudio, + new NodeDto + { + NodeId = Constants.DataTypes.UploadAudio, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadAudio}", + SortOrder = 36, + UniqueId = Constants.DataTypes.Guids.UploadAudioGuid, + Text = "Upload Audio", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadArticle, + new NodeDto + { + NodeId = Constants.DataTypes.UploadArticle, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadArticle}", + SortOrder = 37, + UniqueId = Constants.DataTypes.Guids.UploadArticleGuid, + Text = "Upload Article", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.UploadVectorGraphics, + new NodeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.UploadVectorGraphics}", + SortOrder = 38, + UniqueId = Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Text = "Upload Vector Graphics", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Textarea, + new NodeDto + { + NodeId = Constants.DataTypes.Textarea, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Textarea}", + SortOrder = 33, + UniqueId = Constants.DataTypes.Guids.TextareaGuid, + Text = "Textarea", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Textstring, + new NodeDto + { + NodeId = Constants.DataTypes.Textbox, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Textbox}", + SortOrder = 32, + UniqueId = Constants.DataTypes.Guids.TextstringGuid, + Text = "Textstring", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.RichtextEditor, + new NodeDto + { + NodeId = Constants.DataTypes.RichtextEditor, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.RichtextEditor}", + SortOrder = 4, + UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, + Text = "Richtext editor", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Numeric, + new NodeDto + { + NodeId = -51, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-51", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.NumericGuid, + Text = "Numeric", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Checkbox, + new NodeDto + { + NodeId = Constants.DataTypes.Boolean, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Boolean}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.CheckboxGuid, + Text = "True/false", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.CheckboxList, + new NodeDto + { + NodeId = -43, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-43", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.CheckboxListGuid, + Text = "Checkbox list", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Dropdown, + new NodeDto + { + NodeId = Constants.DataTypes.DropDownSingle, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DropDownSingle}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DropdownGuid, + Text = "Dropdown", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DatePicker, + new NodeDto + { + NodeId = -41, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-41", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DatePickerGuid, + Text = "Date Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Radiobox, + new NodeDto + { + NodeId = -40, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-40", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.RadioboxGuid, + Text = "Radiobox", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DropdownMultiple, + new NodeDto + { + NodeId = Constants.DataTypes.DropDownMultiple, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DropDownMultiple}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DropdownMultipleGuid, + Text = "Dropdown multiple", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ApprovedColor, + new NodeDto + { + NodeId = -37, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-37", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ApprovedColorGuid, + Text = "Approved Color", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.DatePickerWithTime, + new NodeDto + { + NodeId = Constants.DataTypes.DateTime, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DateTime}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.DatePickerWithTimeGuid, + Text = "Date Picker with time", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewContent, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultContentListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultContentListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewContentGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewMedia, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultMediaListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultMediaListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewMediaGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Media", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ListViewMembers, + new NodeDto + { + NodeId = Constants.DataTypes.DefaultMembersListView, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.DefaultMembersListView}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ListViewMembersGuid, + Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.Tags, + new NodeDto + { + NodeId = Constants.DataTypes.Tags, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.Tags}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.TagsGuid, + Text = "Tags", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ImageCropper, + new NodeDto + { + NodeId = Constants.DataTypes.ImageCropper, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = $"-1,{Constants.DataTypes.ImageCropper}", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ImageCropperGuid, + Text = "Image Cropper", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + // New UDI pickers with newer Ids + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.ContentPicker, + new NodeDto + { + NodeId = 1046, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1046", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.ContentPickerGuid, + Text = "Content Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MemberPicker, + new NodeDto + { + NodeId = 1047, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1047", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MemberPickerGuid, + Text = "Member Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker, + new NodeDto + { + NodeId = 1048, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1048", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPickerGuid, + Text = "Media Picker (legacy)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MultipleMediaPicker, + new NodeDto + { + NodeId = 1049, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1049", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MultipleMediaPickerGuid, + Text = "Multiple Media Picker (legacy)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.RelatedLinks, + new NodeDto + { + NodeId = 1050, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1050", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.RelatedLinksGuid, + Text = "Multi URL Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3, + new NodeDto + { + NodeId = 1051, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1051", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, + Text = "Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3Multiple, + new NodeDto + { + NodeId = 1052, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1052", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, + Text = "Multiple Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3SingleImage, + new NodeDto + { + NodeId = 1053, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1053", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, + Text = "Image Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, + Constants.DataTypes.Guids.MediaPicker3MultipleImages, + new NodeDto + { + NodeId = 1054, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1054", + SortOrder = 2, + UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, + Text = "Multiple Image Media Picker", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateNodeDataForMediaTypes() + { + var folderUniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + folderUniqueId.ToString(), + new NodeDto + { + NodeId = 1031, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1031", + SortOrder = 2, + UniqueId = folderUniqueId, + Text = Constants.Conventions.MediaTypes.Folder, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var imageUniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + imageUniqueId.ToString(), + new NodeDto + { + NodeId = 1032, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1032", + SortOrder = 2, + UniqueId = imageUniqueId, + Text = Constants.Conventions.MediaTypes.Image, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var fileUniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + fileUniqueId.ToString(), + new NodeDto + { + NodeId = 1033, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1033", + SortOrder = 2, + UniqueId = fileUniqueId, + Text = Constants.Conventions.MediaTypes.File, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var videoUniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + videoUniqueId.ToString(), + new NodeDto + { + NodeId = 1034, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1034", + SortOrder = 2, + UniqueId = videoUniqueId, + Text = Constants.Conventions.MediaTypes.Video, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var audioUniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + audioUniqueId.ToString(), + new NodeDto + { + NodeId = 1035, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1035", + SortOrder = 2, + UniqueId = audioUniqueId, + Text = Constants.Conventions.MediaTypes.Audio, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var articleUniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + articleUniqueId.ToString(), + new NodeDto + { + NodeId = 1036, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1036", + SortOrder = 2, + UniqueId = articleUniqueId, + Text = Constants.Conventions.MediaTypes.Article, + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + + var svgUniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, + svgUniqueId.ToString(), + new NodeDto + { + NodeId = 1037, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1037", + SortOrder = 2, + UniqueId = svgUniqueId, + Text = "Vector Graphics (SVG)", + NodeObjectType = Constants.ObjectTypes.MediaType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateNodeDataForMemberTypes() + { + var memberUniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"); + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, + memberUniqueId.ToString(), + new NodeDto + { + NodeId = 1044, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,1044", + SortOrder = 0, + UniqueId = memberUniqueId, + Text = Constants.Conventions.MemberTypes.DefaultAlias, + NodeObjectType = Constants.ObjectTypes.MemberType, + CreateDate = DateTime.Now, + }, + Constants.DatabaseSchema.Tables.Node, + "id"); + } + + private void CreateLockData() + { + // all lock objects + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.Servers, Name = "Servers" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.ContentTypes, Name = "ContentTypes" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.ContentTree, Name = "ContentTree" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MediaTypes, Name = "MediaTypes" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MediaTree, Name = "MediaTree" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MemberTypes, Name = "MemberTypes" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MemberTree, Name = "MemberTree" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.Domains, Name = "Domains" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.Languages, Name = "Languages" }); + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); + + _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); + } + + private void CreateContentTypeData() + { + // Insert content types only if the corresponding Node record exists (which may or may not have been created depending on configuration + // of media or member types to create). + + // Media types. + if (_database.Exists(1031)) { - _logger.LogInformation("Creating data in {TableName}", tableName); - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Node)) - { - CreateNodeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Lock)) - { - CreateLockData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.ContentType)) - { - CreateContentTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.User)) - { - CreateUserData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup)) - { - CreateUserGroupData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.User2UserGroup)) - { - CreateUser2UserGroupData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App)) - { - CreateUserGroup2AppData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup)) - { - CreatePropertyTypeGroupData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)) - { - CreatePropertyTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.Language)) - { - CreateLanguageData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType)) - { - CreateContentChildTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.DataType)) - { - CreateDataTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.RelationType)) - { - CreateRelationTypeData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue)) - { - CreateKeyValueData(); - } - - if (tableName.Equals(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) - { - CreateLogViewerQueryData(); - } - - _logger.LogInformation("Completed creating data in {TableName}", tableName); - } - - private void CreateNodeData() - { - CreateNodeDataForDataTypes(); - CreateNodeDataForMediaTypes(); - CreateNodeDataForMemberTypes(); - } - - private void CreateNodeDataForDataTypes() - { - void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) - { - var nodeDto = new NodeDto + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto { - NodeId = id, - Trashed = false, - ParentId = -1, - UserId = -1, - Level = 1, - Path = "-1," + id, - SortOrder = sortOrder, - UniqueId = new Guid(uniqueId), - Text = text, - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - CreateDate = DateTime.Now, - }; - - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - uniqueId, - nodeDto, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -1, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1", SortOrder = 0, UniqueId = new Guid("916724a5-173d-4619-b97e-b9de133dd6f5"), Text = "SYSTEM DATA: umbraco master root", NodeObjectType = Cms.Core.Constants.ObjectTypes.SystemRoot, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -20, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-20", SortOrder = 0, UniqueId = new Guid("0F582A79-1E41-4CF0-BFA0-76340651891A"), Text = "Recycle Bin", NodeObjectType = Cms.Core.Constants.ObjectTypes.ContentRecycleBin, CreateDate = DateTime.Now }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = -21, Trashed = false, ParentId = -1, UserId = -1, Level = 0, Path = "-1,-21", SortOrder = 0, UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), Text = "Recycle Bin", NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaRecycleBin, CreateDate = DateTime.Now }); - - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelString, 35, Cms.Core.Constants.DataTypes.Guids.LabelString, "Label (string)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelInt, 36, Cms.Core.Constants.DataTypes.Guids.LabelInt, "Label (integer)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelBigint, 36, Cms.Core.Constants.DataTypes.Guids.LabelBigInt, "Label (bigint)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelDateTime, 37, Cms.Core.Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelTime, 38, Cms.Core.Constants.DataTypes.Guids.LabelTime, "Label (time)"); - InsertDataTypeNodeDto(Cms.Core.Constants.DataTypes.LabelDecimal, 39, Cms.Core.Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); - - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Upload, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadGuid, Text = "Upload File", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadVideo, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadVideo, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadVideo}", SortOrder = 35, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadVideoGuid, Text = "Upload Video", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadAudio, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadAudio, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadAudio}", SortOrder = 36, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadAudioGuid, Text = "Upload Audio", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadArticle, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadArticle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadArticle}", SortOrder = 37, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadArticleGuid, Text = "Upload Article", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.UploadVectorGraphics, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.UploadVectorGraphics}", SortOrder = 38, UniqueId = Cms.Core.Constants.DataTypes.Guids.UploadVectorGraphicsGuid, Text = "Upload Vector Graphics", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Textarea, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Cms.Core.Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Textstring, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Cms.Core.Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.RichtextEditor, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Cms.Core.Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Numeric, - new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.NumericGuid, Text = "Numeric", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Checkbox, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Boolean, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Boolean}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.CheckboxGuid, Text = "True/false", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.CheckboxList, - new NodeDto { NodeId = -43, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-43", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.CheckboxListGuid, Text = "Checkbox list", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Dropdown, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DropDownSingle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DropDownSingle}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DropdownGuid, Text = "Dropdown", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DatePicker, - new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DatePickerGuid, Text = "Date Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Radiobox, - new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.RadioboxGuid, Text = "Radiobox", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DropdownMultiple, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DropDownMultiple, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DropDownMultiple}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DropdownMultipleGuid, Text = "Dropdown multiple", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ApprovedColor, - new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ApprovedColorGuid, Text = "Approved Color", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.DatePickerWithTime, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DateTime, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DateTime}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.DatePickerWithTimeGuid, Text = "Date Picker with time", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewContent, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultContentListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultContentListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewContentGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewMedia, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultMediaListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultMediaListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewMediaGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Media", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ListViewMembers, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.DefaultMembersListView, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.DefaultMembersListView}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ListViewMembersGuid, Text = Cms.Core.Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.Tags, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.TagsGuid, Text = "Tags", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ImageCropper, - new NodeDto { NodeId = Cms.Core.Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Cms.Core.Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ImageCropperGuid, Text = "Image Cropper", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - // New UDI pickers with newer Ids - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.ContentPicker, - new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.ContentPickerGuid, Text = "Content Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MemberPicker, - new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MemberPickerGuid, Text = "Member Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker, - new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPickerGuid, Text = "Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MultipleMediaPicker, - new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MultipleMediaPickerGuid, Text = "Multiple Media Picker (legacy)", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.RelatedLinks, - new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.RelatedLinksGuid, Text = "Multi URL Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3, - new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3Multiple, - new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3SingleImage, - new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, - Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleImages, - new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Cms.Core.Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker", NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - private void CreateNodeDataForMediaTypes() - { - var folderUniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - folderUniqueId.ToString(), - new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = folderUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Folder, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var imageUniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - imageUniqueId.ToString(), - new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = imageUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Image, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var fileUniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - fileUniqueId.ToString(), - new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = fileUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.File, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var videoUniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - videoUniqueId.ToString(), - new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = videoUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Video, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var audioUniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - audioUniqueId.ToString(), - new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = audioUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Audio, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var articleUniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - articleUniqueId.ToString(), - new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = articleUniqueId, Text = Cms.Core.Constants.Conventions.MediaTypes.Article, NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - - var svgUniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes, - svgUniqueId.ToString(), - new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = svgUniqueId, Text = "Vector Graphics (SVG)", NodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - private void CreateNodeDataForMemberTypes() - { - var memberUniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"); - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.MemberTypes, - memberUniqueId.ToString(), - new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = memberUniqueId, Text = Cms.Core.Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Cms.Core.Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }, - Cms.Core.Constants.DatabaseSchema.Tables.Node, - "id"); - } - - private void CreateLockData() - { - // all lock objects - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Servers, Name = "Servers" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ContentTypes, Name = "ContentTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ContentTree, Name = "ContentTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MediaTypes, Name = "MediaTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MediaTree, Name = "MediaTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MemberTypes, Name = "MemberTypes" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MemberTree, Name = "MemberTree" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Domains, Name = "Domains" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.KeyValues, Name = "KeyValues" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.Languages, Name = "Languages" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" }); - } - - private void CreateContentTypeData() - { - // Insert content types only if the corresponding Node record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media types. - if (_database.Exists(1031)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Cms.Core.Constants.Conventions.MediaTypes.Folder, Icon = Cms.Core.Constants.Icons.MediaFolder, Thumbnail = Cms.Core.Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1032)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Cms.Core.Constants.Conventions.MediaTypes.Image, Icon = Cms.Core.Constants.Icons.MediaImage, Thumbnail = Cms.Core.Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1033)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Cms.Core.Constants.Conventions.MediaTypes.File, Icon = Cms.Core.Constants.Icons.MediaFile, Thumbnail = Cms.Core.Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1034)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Cms.Core.Constants.Conventions.MediaTypes.VideoAlias, Icon = Cms.Core.Constants.Icons.MediaVideo, Thumbnail = Cms.Core.Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1035)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Cms.Core.Constants.Conventions.MediaTypes.AudioAlias, Icon = Cms.Core.Constants.Icons.MediaAudio, Thumbnail = Cms.Core.Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1036)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Cms.Core.Constants.Conventions.MediaTypes.ArticleAlias, Icon = Cms.Core.Constants.Icons.MediaArticle, Thumbnail = Cms.Core.Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(1037)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Cms.Core.Constants.Conventions.MediaTypes.VectorGraphicsAlias, Icon = Cms.Core.Constants.Icons.MediaVectorGraphics, Thumbnail = Cms.Core.Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte)ContentVariation.Nothing }); - } - - // Member type. - if (_database.Exists(1044)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Cms.Core.Constants.Conventions.MemberTypes.DefaultAlias, Icon = Cms.Core.Constants.Icons.Member, Thumbnail = Cms.Core.Constants.Icons.Member, Variations = (byte)ContentVariation.Nothing }); - } - } - - private void CreateUserData() - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.User, "id", false, new UserDto { Id = Cms.Core.Constants.Security.SuperUserId, Disabled = false, NoConsole = false, UserName = "Administrator", Login = "admin", Password = "default", Email = string.Empty, UserLanguage = "en-US", CreateDate = DateTime.Now, UpdateDate = DateTime.Now }); - } - - private void CreateUserGroupData() - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 1, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.AdminGroupAlias, Name = "Administrators", DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-medal" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.WriterGroupAlias, Name = "Writers", DefaultPermissions = "CAH:FN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.EditorGroupAlias, Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5FïN", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 4, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.TranslatorGroupAlias, Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe" }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 5, StartMediaId = -1, StartContentId = -1, Alias = Cms.Core.Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = string.Empty, CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" }); - } - - private void CreateUser2UserGroupData() - { - _database.Insert(new User2UserGroupDto { UserGroupId = 1, UserId = Cms.Core.Constants.Security.SuperUserId }); // add super to admins - _database.Insert(new User2UserGroupDto { UserGroupId = 5, UserId = Cms.Core.Constants.Security.SuperUserId }); // add super to sensitive data - } - - private void CreateUserGroup2AppData() - { - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Content }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Packages }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Media }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Members }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Settings }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Users }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Forms }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Cms.Core.Constants.Applications.Translation }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Cms.Core.Constants.Applications.Content }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Content }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Media }); - _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Cms.Core.Constants.Applications.Forms }); - - _database.Insert(new UserGroup2AppDto { UserGroupId = 4, AppAlias = Cms.Core.Constants.Applications.Translation }); - } - - private void CreatePropertyTypeGroupData() - { - // Insert property groups only if the corresponding content type node record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media property groups. - if (_database.Exists(1032)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Image), ContentTypeNodeId = 1032, Text = "Image", Alias = "image", SortOrder = 1 }); - } - - if (_database.Exists(1033)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.File), ContentTypeNodeId = 1033, Text = "File", Alias = "file", SortOrder = 1, }); - } - - if (_database.Exists(1034)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Video), ContentTypeNodeId = 1034, Text = "Video", Alias = "video", SortOrder = 1 }); - } - - if (_database.Exists(1035)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Audio), ContentTypeNodeId = 1035, Text = "Audio", Alias = "audio", SortOrder = 1 }); - } - - if (_database.Exists(1036)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Article), ContentTypeNodeId = 1036, Text = "Article", Alias = "article", SortOrder = 1 }); - } - - if (_database.Exists(1037)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.VectorGraphics), ContentTypeNodeId = 1037, Text = "Vector Graphics", Alias = "vectorGraphics", SortOrder = 1 }); - } - - // Membership property group. - if (_database.Exists(1044)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Membership), ContentTypeNodeId = 1044, Text = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName, Alias = Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, SortOrder = 1 }); - } - } - - private void CreatePropertyTypeData() - { - // Insert property types only if the corresponding property group record exists (which may or may not have been created depending on configuration - // of media or member types to create). - - // Media property types. - if (_database.Exists(3)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(4)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "File", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(52)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 40, UniqueId = 40.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadVideo, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Video", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 41, UniqueId = 41.ToGuid(), DataTypeId = -92, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 42, UniqueId = 42.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(53)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 43, UniqueId = 43.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadAudio, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Audio", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 44, UniqueId = 44.ToGuid(), DataTypeId = -92, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 45, UniqueId = 45.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(54)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 46, UniqueId = 46.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadArticle, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Article", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 47, UniqueId = 47.ToGuid(), DataTypeId = -92, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 48, UniqueId = 48.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - if (_database.Exists(55)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 49, UniqueId = 49.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.File, Name = "Vector Graphics", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 50, UniqueId = 50.ToGuid(), DataTypeId = -92, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 51, UniqueId = 51.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelBigint, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Cms.Core.Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte)ContentVariation.Nothing }); - } - - // Membership property types. - if (_database.Exists(11)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, - new PropertyTypeDto - { - Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Textarea, - ContentTypeId = 1044, PropertyTypeGroupId = 11, - Alias = Cms.Core.Constants.Conventions.Member.Comments, - Name = Cms.Core.Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, - ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing - }); - } - } - - private void CreateLanguageData() => - ConditionalInsert( - Cms.Core.Constants.Configuration.NamedOptions.InstallDefaultData.Languages, - "en-us", - new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }, - Cms.Core.Constants.DatabaseSchema.Tables.Language, - "id"); - - private void CreateContentChildTypeData() - { - // Insert data if the corresponding Node records exist (which may or may not have been created depending on configuration - // of media types to create). - if (!_database.Exists(1031)) - { - return; - } - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); - - for (int i = 1032; i <= 1037; i++) - { - if (_database.Exists(i)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = i }); - } - } - } - - private void CreateDataTypeData() - { - void InsertDataTypeDto(int id, string editorAlias, string dbType, string? configuration = null) - { - var dataTypeDto = new DataTypeDto - { - NodeId = id, - EditorAlias = editorAlias, - DbType = dbType - }; - - if (configuration != null) - { - dataTypeDto.Configuration = configuration; - } - - if (_database.Exists(id)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); - } - } - - //layouts for the list view - const string cardLayout = "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; - const string listLayout = "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": 1,\"selected\": true}"; - const string layouts = "[" + cardLayout + "," + listLayout + "]"; - - // Insert data types only if the corresponding Node record exists (which may or may not have been created depending on configuration - // of data types to create). - if (_database.Exists(Cms.Core.Constants.DataTypes.Boolean)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Boolean, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Boolean, DbType = "Integer" }); - } - - if (_database.Exists(-51)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -51, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Integer, DbType = "Integer" }); - } - - if (_database.Exists(-87)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = -87, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TinyMce, - DbType = "Ntext", - Configuration = "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Textbox)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Textbox, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TextBox, DbType = "Nvarchar" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Textarea)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Textarea, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.TextArea, DbType = "Ntext" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Upload)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.Upload, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, DbType = "Nvarchar" }); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelString, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"STRING\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelInt, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelBigint, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDateTime, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDecimal, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelTime, Cms.Core.Constants.PropertyEditors.Aliases.Label, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); - - if (_database.Exists(Cms.Core.Constants.DataTypes.DateTime)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.DateTime, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DateTime, DbType = "Date" }); - } - - if (_database.Exists(-37)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" }); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.DropDownSingle, Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}"); - - if (_database.Exists(-40)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" }); - } - - if (_database.Exists(-41)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.DateTime", DbType = "Date", Configuration = "{\"format\":\"YYYY-MM-DD\"}" }); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.DropDownMultiple, Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}"); - - if (_database.Exists(-43)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.Tags)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.Tags, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Tags, - DbType = "Ntext", - Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.ImageCropper)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = Cms.Core.Constants.DataTypes.ImageCropper, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ImageCropper, DbType = "Ntext" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultContentListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultContentListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultMediaListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultMediaListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + layouts + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.DefaultMembersListView)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.DefaultMembersListView, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ListView, - DbType = "Nvarchar", - Configuration = "{\"pageSize\":10, \"orderBy\":\"username\", \"orderDirection\":\"asc\", \"includeProperties\":[{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}" - }); - } - - // New UDI pickers with newer Ids - if (_database.Exists(1046)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1046, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker, DbType = "Nvarchar" }); - } - - if (_database.Exists(1047)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1047, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MemberPicker, DbType = "Nvarchar" }); - } - - if (_database.Exists(1048)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1048, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext" }); - } - - if (_database.Exists(1049)) - { - _database.Insert( - Cms.Core.Constants.DatabaseSchema.Tables.DataType, - "pk", - false, - new DataTypeDto - { - NodeId = 1049, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker, - DbType = "Ntext", - Configuration = "{\"multiPicker\":1}" - }); - } - - if (_database.Exists(1050)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadVideo)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadVideo, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}" + PrimaryKey = 532, + NodeId = 1031, + Alias = Constants.Conventions.MediaTypes.Folder, + Icon = Constants.Icons.MediaFolder, + Thumbnail = Constants.Icons.MediaFolder, + IsContainer = false, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadAudio)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadAudio, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadArticle)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadArticle, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}" - }); - } - - if (_database.Exists(Cms.Core.Constants.DataTypes.UploadVectorGraphics)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = Cms.Core.Constants.DataTypes.UploadVectorGraphics, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.UploadField, - DbType = "Nvarchar", - Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}" - }); - } - - if (_database.Exists(1051)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1051, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" - }); - } - - if (_database.Exists(1052)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1052, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"multiple\": true}" - }); - } - - if (_database.Exists(1053)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1053, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"filter\":\"" + Cms.Core.Constants.Conventions.MediaTypes.Image + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" - }); - } - - if (_database.Exists(1054)) - { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto - { - NodeId = 1054, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker3, - DbType = "Ntext", - Configuration = "{\"filter\":\"" + Cms.Core.Constants.Conventions.MediaTypes.Image + "\", \"multiple\": true}" - }); - } } - private void CreateRelationTypeData() + if (_database.Exists(1032)) { - CreateRelationTypeData(1, Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, Cms.Core.Constants.ObjectTypes.Document, Cms.Core.Constants.ObjectTypes.Document, true, false); - CreateRelationTypeData(2, Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, Cms.Core.Constants.ObjectTypes.Document, Cms.Core.Constants.ObjectTypes.Document, false, false); - CreateRelationTypeData(3, Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, Cms.Core.Constants.ObjectTypes.Media, Cms.Core.Constants.ObjectTypes.Media, false, false); - CreateRelationTypeData(4, Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName, null, null, false, true); - CreateRelationTypeData(5, Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName, null, null, false, true); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 533, + NodeId = 1032, + Alias = Constants.Conventions.MediaTypes.Image, + Icon = Constants.Icons.MediaImage, + Thumbnail = Constants.Icons.MediaImage, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void CreateRelationTypeData(int id, string alias, string name, Guid? parentObjectType, Guid? childObjectType, bool dual, bool isDependency) + if (_database.Exists(1033)) { - var relationType = new RelationTypeDto { Id = id, Alias = alias, ChildObjectType = childObjectType, ParentObjectType = parentObjectType, Dual = dual, Name = name, IsDependency = isDependency }; - relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 534, + NodeId = 1033, + Alias = Constants.Conventions.MediaTypes.File, + Icon = Constants.Icons.MediaFile, + Thumbnail = Constants.Icons.MediaFile, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - internal static Guid CreateUniqueRelationTypeId(string alias, string name) + if (_database.Exists(1034)) { - return (alias + "____" + name).ToGuid(); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 540, + NodeId = 1034, + Alias = Constants.Conventions.MediaTypes.VideoAlias, + Icon = Constants.Icons.MediaVideo, + Thumbnail = Constants.Icons.MediaVideo, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void CreateKeyValueData() + if (_database.Exists(1035)) { - // On install, initialize the umbraco migration plan with the final state. - var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); - var stateValueKey = upgrader.StateValueKey; - var finalState = upgrader.Plan.FinalState; - - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue, "key", false, new KeyValueDto { Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 541, + NodeId = 1035, + Alias = Constants.Conventions.MediaTypes.AudioAlias, + Icon = Constants.Icons.MediaAudio, + Thumbnail = Constants.Icons.MediaAudio, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void CreateLogViewerQueryData() + if (_database.Exists(1036)) { - LogViewerQueryDto[] defaultData = MigrateLogViewerQueriesFromFileToDb.DefaultLogQueries.ToArray(); - - for (int i = 0; i < defaultData.Length; i++) - { - LogViewerQueryDto dto = defaultData[i]; - dto.Id = i+1; - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery, "id", false, dto); - } + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 542, + NodeId = 1036, + Alias = Constants.Conventions.MediaTypes.ArticleAlias, + Icon = Constants.Icons.MediaArticle, + Thumbnail = Constants.Icons.MediaArticle, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); } - private void ConditionalInsert( - string configKey, - string id, - TDto dto, - string tableName, - string primaryKeyName, - bool autoIncrement = false) + if (_database.Exists(1037)) { - var alwaysInsert = _entitiesToAlwaysCreate.ContainsKey(configKey) && - _entitiesToAlwaysCreate[configKey].InvariantContains(id.ToString()); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 543, + NodeId = 1037, + Alias = Constants.Conventions.MediaTypes.VectorGraphicsAlias, + Icon = Constants.Icons.MediaVectorGraphics, + Thumbnail = Constants.Icons.MediaVectorGraphics, + AllowAtRoot = true, + Variations = (byte)ContentVariation.Nothing, + }); + } - InstallDefaultDataSettings installDefaultDataSettings = _installDefaultDataSettings.Get(configKey); - - // If there's no configuration, we assume to create. - if (installDefaultDataSettings == null) - { - alwaysInsert = true; - } - - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.None) - { - return; - } - - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values && !installDefaultDataSettings.Values.InvariantContains(id)) - { - return; - } - - if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.ExceptValues && installDefaultDataSettings.Values.InvariantContains(id)) - { - return; - } - - _database.Insert(tableName, primaryKeyName, autoIncrement, dto); + // Member type. + if (_database.Exists(1044)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, + new ContentTypeDto + { + PrimaryKey = 531, + NodeId = 1044, + Alias = Constants.Conventions.MemberTypes.DefaultAlias, + Icon = Constants.Icons.Member, + Thumbnail = Constants.Icons.Member, + Variations = (byte)ContentVariation.Nothing, + }); } } + + private void CreateUserData() => _database.Insert(Constants.DatabaseSchema.Tables.User, "id", false, + new UserDto + { + Id = Constants.Security.SuperUserId, + Disabled = false, + NoConsole = false, + UserName = "Administrator", + Login = "admin", + Password = "default", + Email = string.Empty, + UserLanguage = "en-US", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + }); + + private void CreateUserGroupData() + { + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 1, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.AdminGroupAlias, + Name = "Administrators", + DefaultPermissions = "CADMOSKTPIURZ:5F7ïN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-medal", + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 2, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.WriterGroupAlias, + Name = "Writers", + DefaultPermissions = "CAH:FN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-edit", + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 3, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.EditorGroupAlias, + Name = "Editors", + DefaultPermissions = "CADMOSKTPUZ:5FïN", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-tools", + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 4, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.TranslatorGroupAlias, + Name = "Translators", + DefaultPermissions = "AF", + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-globe", + }); + _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, + new UserGroupDto + { + Id = 5, + StartMediaId = -1, + StartContentId = -1, + Alias = Constants.Security.SensitiveDataGroupAlias, + Name = "Sensitive data", + DefaultPermissions = string.Empty, + CreateDate = DateTime.Now, + UpdateDate = DateTime.Now, + Icon = "icon-lock", + }); + } + + private void CreateUser2UserGroupData() + { + _database.Insert(new User2UserGroupDto + { + UserGroupId = 1, + UserId = Constants.Security.SuperUserId, + }); // add super to admins + _database.Insert(new User2UserGroupDto + { + UserGroupId = 5, + UserId = Constants.Security.SuperUserId, + }); // add super to sensitive data + } + + private void CreateUserGroup2AppData() + { + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Content }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Packages }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Media }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Members }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Settings }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Users }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Forms }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Translation }); + + _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Constants.Applications.Content }); + + _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Content }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Media }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 3, AppAlias = Constants.Applications.Forms }); + + _database.Insert(new UserGroup2AppDto { UserGroupId = 4, AppAlias = Constants.Applications.Translation }); + } + + private void CreatePropertyTypeGroupData() + { + // Insert property groups only if the corresponding content type node record exists (which may or may not have been created depending on configuration + // of media or member types to create). + + // Media property groups. + if (_database.Exists(1032)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 3, + UniqueId = new Guid(Constants.PropertyTypeGroups.Image), + ContentTypeNodeId = 1032, + Text = "Image", + Alias = "image", + SortOrder = 1, + }); + } + + if (_database.Exists(1033)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 4, + UniqueId = new Guid(Constants.PropertyTypeGroups.File), + ContentTypeNodeId = 1033, + Text = "File", + Alias = "file", + SortOrder = 1, + }); + } + + if (_database.Exists(1034)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 52, + UniqueId = new Guid(Constants.PropertyTypeGroups.Video), + ContentTypeNodeId = 1034, + Text = "Video", + Alias = "video", + SortOrder = 1, + }); + } + + if (_database.Exists(1035)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 53, + UniqueId = new Guid(Constants.PropertyTypeGroups.Audio), + ContentTypeNodeId = 1035, + Text = "Audio", + Alias = "audio", + SortOrder = 1, + }); + } + + if (_database.Exists(1036)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 54, + UniqueId = new Guid(Constants.PropertyTypeGroups.Article), + ContentTypeNodeId = 1036, + Text = "Article", + Alias = "article", + SortOrder = 1, + }); + } + + if (_database.Exists(1037)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 55, + UniqueId = new Guid(Constants.PropertyTypeGroups.VectorGraphics), + ContentTypeNodeId = 1037, + Text = "Vector Graphics", + Alias = "vectorGraphics", + SortOrder = 1, + }); + } + + // Membership property group. + if (_database.Exists(1044)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, + new PropertyTypeGroupDto + { + Id = 11, + UniqueId = new Guid(Constants.PropertyTypeGroups.Membership), + ContentTypeNodeId = 1044, + Text = Constants.Conventions.Member.StandardPropertiesGroupName, + Alias = Constants.Conventions.Member.StandardPropertiesGroupAlias, + SortOrder = 1, + }); + } + } + + private void CreatePropertyTypeData() + { + // Insert property types only if the corresponding property group record exists (which may or may not have been created depending on configuration + // of media or member types to create). + + // Media property types. + if (_database.Exists(3)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 6, + UniqueId = 6.ToGuid(), + DataTypeId = Constants.DataTypes.ImageCropper, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.File, + Name = "Image", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 7, + UniqueId = 7.ToGuid(), + DataTypeId = Constants.DataTypes.LabelInt, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Width, + Name = "Width", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in pixels", + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 8, + UniqueId = 8.ToGuid(), + DataTypeId = Constants.DataTypes.LabelInt, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Height, + Name = "Height", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in pixels", + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 9, + UniqueId = 9.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 10, + UniqueId = 10.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1032, + PropertyTypeGroupId = 3, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(4)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 24, + UniqueId = 24.ToGuid(), + DataTypeId = Constants.DataTypes.Upload, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.File, + Name = "File", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 25, + UniqueId = 25.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 26, + UniqueId = 26.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1033, + PropertyTypeGroupId = 4, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(52)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 40, + UniqueId = 40.ToGuid(), + DataTypeId = Constants.DataTypes.UploadVideo, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.File, + Name = "Video", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 41, + UniqueId = 41.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 42, + UniqueId = 42.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1034, + PropertyTypeGroupId = 52, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(53)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 43, + UniqueId = 43.ToGuid(), + DataTypeId = Constants.DataTypes.UploadAudio, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.File, + Name = "Audio", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 44, + UniqueId = 44.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 45, + UniqueId = 45.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1035, + PropertyTypeGroupId = 53, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(54)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 46, + UniqueId = 46.ToGuid(), + DataTypeId = Constants.DataTypes.UploadArticle, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.File, + Name = "Article", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 47, + UniqueId = 47.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 48, + UniqueId = 48.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1036, + PropertyTypeGroupId = 54, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + if (_database.Exists(55)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 49, + UniqueId = 49.ToGuid(), + DataTypeId = Constants.DataTypes.UploadVectorGraphics, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.File, + Name = "Vector Graphics", + SortOrder = 0, + Mandatory = true, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 50, + UniqueId = 50.ToGuid(), + DataTypeId = -92, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.Extension, + Name = "Type", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 51, + UniqueId = 51.ToGuid(), + DataTypeId = Constants.DataTypes.LabelBigint, + ContentTypeId = 1037, + PropertyTypeGroupId = 55, + Alias = Constants.Conventions.Media.Bytes, + Name = "Size", + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = "in bytes", + Variations = (byte)ContentVariation.Nothing, + }); + } + + // Membership property types. + if (_database.Exists(11)) + { + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, + new PropertyTypeDto + { + Id = 28, + UniqueId = 28.ToGuid(), + DataTypeId = Constants.DataTypes.Textarea, + ContentTypeId = 1044, + PropertyTypeGroupId = 11, + Alias = Constants.Conventions.Member.Comments, + Name = Constants.Conventions.Member.CommentsLabel, + SortOrder = 0, + Mandatory = false, + ValidationRegExp = null, + Description = null, + Variations = (byte)ContentVariation.Nothing, + }); + } + } + + private void CreateLanguageData() => + ConditionalInsert( + Constants.Configuration.NamedOptions.InstallDefaultData.Languages, + "en-us", + new LanguageDto { Id = 1, IsoCode = "en-US", CultureName = "English (United States)", IsDefault = true }, + Constants.DatabaseSchema.Tables.Language, + "id"); + + private void CreateContentChildTypeData() + { + // Insert data if the corresponding Node records exist (which may or may not have been created depending on configuration + // of media types to create). + if (!_database.Exists(1031)) + { + return; + } + + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, + new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); + + for (var i = 1032; i <= 1037; i++) + { + if (_database.Exists(i)) + { + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, + new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = i }); + } + } + } + + private void CreateDataTypeData() + { + void InsertDataTypeDto(int id, string editorAlias, string dbType, string? configuration = null) + { + var dataTypeDto = new DataTypeDto { NodeId = id, EditorAlias = editorAlias, DbType = dbType }; + + if (configuration != null) + { + dataTypeDto.Configuration = configuration; + } + + if (_database.Exists(id)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + } + + // layouts for the list view + const string cardLayout = + "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; + const string listLayout = + "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": 1,\"selected\": true}"; + const string layouts = "[" + cardLayout + "," + listLayout + "]"; + + // Insert data types only if the corresponding Node record exists (which may or may not have been created depending on configuration + // of data types to create). + if (_database.Exists(Constants.DataTypes.Boolean)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Boolean, + EditorAlias = Constants.PropertyEditors.Aliases.Boolean, + DbType = "Integer", + }); + } + + if (_database.Exists(-51)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -51, + EditorAlias = Constants.PropertyEditors.Aliases.Integer, + DbType = "Integer", + }); + } + + if (_database.Exists(-87)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = -87, + EditorAlias = Constants.PropertyEditors.Aliases.TinyMce, + DbType = "Ntext", + Configuration = + "{\"value\":\",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|\"}", + }); + } + + if (_database.Exists(Constants.DataTypes.Textbox)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Textbox, + EditorAlias = Constants.PropertyEditors.Aliases.TextBox, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(Constants.DataTypes.Textarea)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Textarea, + EditorAlias = Constants.PropertyEditors.Aliases.TextArea, + DbType = "Ntext", + }); + } + + if (_database.Exists(Constants.DataTypes.Upload)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Upload, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + }); + } + + InsertDataTypeDto(Constants.DataTypes.LabelString, Constants.PropertyEditors.Aliases.Label, "Nvarchar", + "{\"umbracoDataValueType\":\"STRING\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelInt, Constants.PropertyEditors.Aliases.Label, "Integer", + "{\"umbracoDataValueType\":\"INT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBigint, Constants.PropertyEditors.Aliases.Label, "Nvarchar", + "{\"umbracoDataValueType\":\"BIGINT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDateTime, Constants.PropertyEditors.Aliases.Label, "Date", + "{\"umbracoDataValueType\":\"DATETIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDecimal, Constants.PropertyEditors.Aliases.Label, "Decimal", + "{\"umbracoDataValueType\":\"DECIMAL\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelTime, Constants.PropertyEditors.Aliases.Label, "Date", + "{\"umbracoDataValueType\":\"TIME\"}"); + + if (_database.Exists(Constants.DataTypes.DateTime)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DateTime, + EditorAlias = Constants.PropertyEditors.Aliases.DateTime, + DbType = "Date", + }); + } + + if (_database.Exists(-37)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -37, + EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, + DbType = "Nvarchar", + }); + } + + InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Nvarchar", "{\"multiple\":false}"); + + if (_database.Exists(-40)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -40, + EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(-41)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -41, + EditorAlias = "Umbraco.DateTime", + DbType = "Date", + Configuration = "{\"format\":\"YYYY-MM-DD\"}", + }); + } + + InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, + "Nvarchar", "{\"multiple\":true}"); + + if (_database.Exists(-43)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = -43, + EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(Constants.DataTypes.Tags)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.Tags, + EditorAlias = Constants.PropertyEditors.Aliases.Tags, + DbType = "Ntext", + Configuration = "{\"group\":\"default\", \"storageType\":\"Json\"}", + }); + } + + if (_database.Exists(Constants.DataTypes.ImageCropper)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.ImageCropper, + EditorAlias = Constants.PropertyEditors.Aliases.ImageCropper, + DbType = "Ntext", + }); + } + + if (_database.Exists(Constants.DataTypes.DefaultContentListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultContentListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + + layouts + + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.DefaultMediaListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultMediaListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":100, \"orderBy\":\"updateDate\", \"orderDirection\":\"desc\", \"layouts\":" + + layouts + + ", \"includeProperties\":[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.DefaultMembersListView)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = Constants.DataTypes.DefaultMembersListView, + EditorAlias = Constants.PropertyEditors.Aliases.ListView, + DbType = "Nvarchar", + Configuration = + "{\"pageSize\":10, \"orderBy\":\"username\", \"orderDirection\":\"asc\", \"includeProperties\":[{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]}", + }); + } + + // New UDI pickers with newer Ids + if (_database.Exists(1046)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1046, + EditorAlias = Constants.PropertyEditors.Aliases.ContentPicker, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(1047)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1047, + EditorAlias = Constants.PropertyEditors.Aliases.MemberPicker, + DbType = "Nvarchar", + }); + } + + if (_database.Exists(1048)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1048, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, + DbType = "Ntext", + }); + } + + if (_database.Exists(1049)) + { + _database.Insert( + Constants.DatabaseSchema.Tables.DataType, + "pk", + false, + new DataTypeDto + { + NodeId = 1049, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, + DbType = "Ntext", + Configuration = "{\"multiPicker\":1}", + }); + } + + if (_database.Exists(1050)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1050, + EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, + DbType = "Ntext", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadVideo)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVideo, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadAudio)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadAudio, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadArticle)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadArticle, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = + "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}", + }); + } + + if (_database.Exists(Constants.DataTypes.UploadVectorGraphics)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}", + }); + } + + if (_database.Exists(1051)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1051, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}", + }); + } + + if (_database.Exists(1052)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1052, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": true}", + }); + } + + if (_database.Exists(1053)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1053, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}", + }); + } + + if (_database.Exists(1054)) + { + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, + new DataTypeDto + { + NodeId = 1054, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + + "\", \"multiple\": true}", + }); + } + } + + private void CreateRelationTypeData() + { + CreateRelationTypeData(1, Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, + Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, true, false); + CreateRelationTypeData(2, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, + Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, Constants.ObjectTypes.Document, + Constants.ObjectTypes.Document, false, false); + CreateRelationTypeData(3, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, + Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, Constants.ObjectTypes.Media, + Constants.ObjectTypes.Media, false, false); + CreateRelationTypeData(4, Constants.Conventions.RelationTypes.RelatedMediaAlias, + Constants.Conventions.RelationTypes.RelatedMediaName, null, null, false, true); + CreateRelationTypeData(5, Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedDocumentName, null, null, false, true); + } + + private void CreateRelationTypeData(int id, string alias, string name, Guid? parentObjectType, + Guid? childObjectType, bool dual, bool isDependency) + { + var relationType = new RelationTypeDto + { + Id = id, + Alias = alias, + ChildObjectType = childObjectType, + ParentObjectType = parentObjectType, + Dual = dual, + Name = name, + IsDependency = isDependency, + }; + relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); + + _database.Insert(Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); + } + + private void CreateKeyValueData() + { + // On install, initialize the umbraco migration plan with the final state. + var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); + var stateValueKey = upgrader.StateValueKey; + var finalState = upgrader.Plan.FinalState; + + _database.Insert(Constants.DatabaseSchema.Tables.KeyValue, "key", false, + new KeyValueDto { Key = stateValueKey, Value = finalState, UpdateDate = DateTime.Now }); + } + + private void CreateLogViewerQueryData() + { + LogViewerQueryDto[] defaultData = MigrateLogViewerQueriesFromFileToDb._defaultLogQueries.ToArray(); + + for (var i = 0; i < defaultData.Length; i++) + { + LogViewerQueryDto dto = defaultData[i]; + dto.Id = i + 1; + _database.Insert(Constants.DatabaseSchema.Tables.LogViewerQuery, "id", false, dto); + } + } + + private void ConditionalInsert( + string configKey, + string id, + TDto dto, + string tableName, + string primaryKeyName, + bool autoIncrement = false) + { + var alwaysInsert = _entitiesToAlwaysCreate.ContainsKey(configKey) && + _entitiesToAlwaysCreate[configKey].InvariantContains(id); + + InstallDefaultDataSettings installDefaultDataSettings = _installDefaultDataSettings.Get(configKey); + + // If there's no configuration, we assume to create. + if (installDefaultDataSettings == null) + { + alwaysInsert = true; + } + + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.None) + { + return; + } + + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.Values && + !installDefaultDataSettings.Values.InvariantContains(id)) + { + return; + } + + if (!alwaysInsert && installDefaultDataSettings?.InstallData == InstallDefaultDataOption.ExceptValues && + installDefaultDataSettings.Values.InvariantContains(id)) + { + return; + } + + _database.Insert(tableName, primaryKeyName, autoIncrement, dto); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index d4527909e9..2c524e3348 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; using System.Data.SqlTypes; -using System.Linq; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,529 +16,540 @@ using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using ColumnInfo = Umbraco.Cms.Infrastructure.Persistence.SqlSyntax.ColumnInfo; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Creates the initial database schema during install. +/// +public class DatabaseSchemaCreator { - /// - /// Creates the initial database schema during install. - /// - public class DatabaseSchemaCreator + // all tables, in order + internal static readonly List _orderedTables = new() { - // all tables, in order - internal static readonly List OrderedTables = new() - { - typeof(UserDto), - typeof(NodeDto), - typeof(ContentTypeDto), - typeof(TemplateDto), - typeof(ContentDto), - typeof(ContentVersionDto), - typeof(MediaVersionDto), - typeof(DocumentDto), - typeof(ContentTypeTemplateDto), - typeof(DataTypeDto), - typeof(DictionaryDto), - typeof(LanguageDto), - typeof(LanguageTextDto), - typeof(DomainDto), - typeof(LogDto), - typeof(MacroDto), - typeof(MacroPropertyDto), - typeof(MemberPropertyTypeDto), - typeof(MemberDto), - typeof(Member2MemberGroupDto), - typeof(PropertyTypeGroupDto), - typeof(PropertyTypeDto), - typeof(PropertyDataDto), - typeof(RelationTypeDto), - typeof(RelationDto), - typeof(TagDto), - typeof(TagRelationshipDto), - typeof(ContentType2ContentTypeDto), - typeof(ContentTypeAllowedContentTypeDto), - typeof(User2NodeNotifyDto), - typeof(ServerRegistrationDto), - typeof(AccessDto), - typeof(AccessRuleDto), - typeof(CacheInstructionDto), - typeof(ExternalLoginDto), - typeof(ExternalLoginTokenDto), - typeof(TwoFactorLoginDto), - typeof(RedirectUrlDto), - typeof(LockDto), - typeof(UserGroupDto), - typeof(User2UserGroupDto), - typeof(UserGroup2NodePermissionDto), - typeof(UserGroup2AppDto), - typeof(UserStartNodeDto), - typeof(ContentNuDto), - typeof(DocumentVersionDto), - typeof(KeyValueDto), - typeof(UserLoginDto), - typeof(ConsentDto), - typeof(AuditEntryDto), - typeof(ContentVersionCultureVariationDto), - typeof(DocumentCultureVariationDto), - typeof(ContentScheduleDto), - typeof(LogViewerQueryDto), - typeof(ContentVersionCleanupPolicyDto), - typeof(UserGroup2NodeDto), - typeof(CreatedPackageSchemaDto) - }; + typeof(UserDto), + typeof(NodeDto), + typeof(ContentTypeDto), + typeof(TemplateDto), + typeof(ContentDto), + typeof(ContentVersionDto), + typeof(MediaVersionDto), + typeof(DocumentDto), + typeof(ContentTypeTemplateDto), + typeof(DataTypeDto), + typeof(DictionaryDto), + typeof(LanguageDto), + typeof(LanguageTextDto), + typeof(DomainDto), + typeof(LogDto), + typeof(MacroDto), + typeof(MacroPropertyDto), + typeof(MemberPropertyTypeDto), + typeof(MemberDto), + typeof(Member2MemberGroupDto), + typeof(PropertyTypeGroupDto), + typeof(PropertyTypeDto), + typeof(PropertyDataDto), + typeof(RelationTypeDto), + typeof(RelationDto), + typeof(TagDto), + typeof(TagRelationshipDto), + typeof(ContentType2ContentTypeDto), + typeof(ContentTypeAllowedContentTypeDto), + typeof(User2NodeNotifyDto), + typeof(ServerRegistrationDto), + typeof(AccessDto), + typeof(AccessRuleDto), + typeof(CacheInstructionDto), + typeof(ExternalLoginDto), + typeof(ExternalLoginTokenDto), + typeof(TwoFactorLoginDto), + typeof(RedirectUrlDto), + typeof(LockDto), + typeof(UserGroupDto), + typeof(User2UserGroupDto), + typeof(UserGroup2NodePermissionDto), + typeof(UserGroup2AppDto), + typeof(UserStartNodeDto), + typeof(ContentNuDto), + typeof(DocumentVersionDto), + typeof(KeyValueDto), + typeof(UserLoginDto), + typeof(ConsentDto), + typeof(AuditEntryDto), + typeof(ContentVersionCultureVariationDto), + typeof(DocumentCultureVariationDto), + typeof(ContentScheduleDto), + typeof(LogViewerQueryDto), + typeof(ContentVersionCleanupPolicyDto), + typeof(UserGroup2NodeDto), + typeof(CreatedPackageSchemaDto) + }; - private readonly IUmbracoDatabase _database; - private readonly IEventAggregator _eventAggregator; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IOptionsMonitor _defaultDataCreationSettings; + private readonly IUmbracoDatabase _database; + private readonly IOptionsMonitor _defaultDataCreationSettings; + private readonly IEventAggregator _eventAggregator; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IUmbracoVersion _umbracoVersion; - [Obsolete("Please use constructor taking all parameters. Scheduled for removal in V11.")] - public DatabaseSchemaCreator( - IUmbracoDatabase? database, - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator) - : this (database, logger, loggerFactory, umbracoVersion, eventAggregator, StaticServiceProvider.Instance.GetRequiredService>()) + [Obsolete("Please use constructor taking all parameters. Scheduled for removal in V11.")] + public DatabaseSchemaCreator( + IUmbracoDatabase? database, + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator) + : this(database, logger, loggerFactory, umbracoVersion, eventAggregator, + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public DatabaseSchemaCreator( + IUmbracoDatabase? database, + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator, + IOptionsMonitor defaultDataCreationSettings) + { + _database = database ?? throw new ArgumentNullException(nameof(database)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); + _eventAggregator = eventAggregator; + _defaultDataCreationSettings = defaultDataCreationSettings; + + if (_database?.SqlContext?.SqlSyntax == null) { + throw new InvalidOperationException("No SqlContext has been assigned to the database"); } + } - public DatabaseSchemaCreator( - IUmbracoDatabase? database, - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator, - IOptionsMonitor defaultDataCreationSettings) + private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; + + /// + /// Drops all Umbraco tables in the db. + /// + internal void UninstallDatabaseSchema() + { + _logger.LogInformation("Start UninstallDatabaseSchema"); + + foreach (Type table in _orderedTables.AsEnumerable().Reverse()) { - _database = database ?? throw new ArgumentNullException(nameof(database)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - _umbracoVersion = umbracoVersion ?? throw new ArgumentNullException(nameof(umbracoVersion)); - _eventAggregator = eventAggregator; - _defaultDataCreationSettings = defaultDataCreationSettings; + TableNameAttribute? tableNameAttribute = table.FirstAttribute(); + var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; - if (_database?.SqlContext?.SqlSyntax == null) + _logger.LogInformation("Uninstall {TableName}", tableName); + + try { - throw new InvalidOperationException("No SqlContext has been assigned to the database"); - } - } - - private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; - - /// - /// Drops all Umbraco tables in the db. - /// - internal void UninstallDatabaseSchema() - { - _logger.LogInformation("Start UninstallDatabaseSchema"); - - foreach (Type table in OrderedTables.AsEnumerable().Reverse()) - { - TableNameAttribute? tableNameAttribute = table.FirstAttribute(); - var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; - - _logger.LogInformation("Uninstall {TableName}", tableName); - - try + if (TableExists(tableName)) { - if (TableExists(tableName)) - { - DropTable(tableName); - } - } - catch (Exception ex) - { - //swallow this for now, not sure how best to handle this with diff databases... though this is internal - // and only used for unit tests. If this fails its because the table doesn't exist... generally! - _logger.LogError(ex, "Could not drop table {TableName}", tableName); + DropTable(tableName); } } + catch (Exception ex) + { + //swallow this for now, not sure how best to handle this with diff databases... though this is internal + // and only used for unit tests. If this fails its because the table doesn't exist... generally! + _logger.LogError(ex, "Could not drop table {TableName}", tableName); + } + } + } + + /// + /// Initializes the database by creating the umbraco db schema. + /// + /// This needs to execute as part of a transaction. + public void InitializeDatabaseSchema() + { + if (!_database.InTransaction) + { + throw new InvalidOperationException("Database is not in a transaction."); } - /// - /// Initializes the database by creating the umbraco db schema. - /// - /// This needs to execute as part of a transaction. - public void InitializeDatabaseSchema() + var eventMessages = new EventMessages(); + var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages); + FireBeforeCreation(creatingNotification); + + if (creatingNotification.Cancel == false) { - if (!_database.InTransaction) + var dataCreation = new DatabaseDataCreator( + _database, _loggerFactory.CreateLogger(), + _umbracoVersion, + _defaultDataCreationSettings); + foreach (Type table in _orderedTables) { - throw new InvalidOperationException("Database is not in a transaction."); - } - - var eventMessages = new EventMessages(); - var creatingNotification = new DatabaseSchemaCreatingNotification(eventMessages); - FireBeforeCreation(creatingNotification); - - if (creatingNotification.Cancel == false) - { - var dataCreation = new DatabaseDataCreator( - _database, _loggerFactory.CreateLogger(), - _umbracoVersion, - _defaultDataCreationSettings); - foreach (Type table in OrderedTables) - { - CreateTable(false, table, dataCreation); - } - } - - DatabaseSchemaCreatedNotification createdNotification = - new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); - FireAfterCreation(createdNotification); - } - - /// - /// Validates the schema of the current database. - /// - internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(OrderedTables); - - internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) - { - var result = new DatabaseSchemaResult(); - - result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) - .Select(x => new DbIndexDefinition(x))); - - result.TableDefinitions.AddRange(orderedTables - .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); - - ValidateDbTables(result); - ValidateDbColumns(result); - ValidateDbIndexes(result); - ValidateDbConstraints(result); - - return result; - } - - /// - /// This validates the Primary/Foreign keys in the database - /// - /// - /// - /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database - /// with non PK/FK constraints. - /// Any unique "constraints" in the database are done with unique indexes. - /// - private void ValidateDbConstraints(DatabaseSchemaResult result) - { - //Check constraints in configured database against constraints in schema - var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); - var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); - var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); - - var unknownConstraintsInDatabase = constraintsInDatabase.Where( - x => x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && x.Item3.InvariantStartsWith("IX_") == false - ).Select(x => x.Item3).ToList(); - - var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).Where(x => x is not null).ToList(); - var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)).Where(x => x.IsNullOrWhiteSpace() == false).ToList(); - - // Add valid and invalid foreign key differences to the result object - // We'll need to do invariant contains with case insensitivity because foreign key, primary key is not standardized - // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. - foreach (var unknown in unknownConstraintsInDatabase) - { - if (foreignKeysInSchema!.InvariantContains(unknown) || primaryKeysInSchema!.InvariantContains(unknown)) - { - result.ValidConstraints.Add(unknown); - } - else - { - result.Errors.Add(new Tuple("Unknown", unknown)); - } - } - - // Foreign keys: - IEnumerable validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var foreignKey in validForeignKeyDifferences) - { - if (foreignKey is not null) - { - result.ValidConstraints.Add(foreignKey); - } - } - - IEnumerable invalidForeignKeyDifferences = foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var foreignKey in invalidForeignKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", foreignKey ?? "NULL")); - } - - // Primary keys: - // Add valid and invalid primary key differences to the result object - IEnumerable validPrimaryKeyDifferences = primaryKeysInDatabase!.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)!; - foreach (var primaryKey in validPrimaryKeyDifferences) - { - result.ValidConstraints.Add(primaryKey); - } - - IEnumerable invalidPrimaryKeyDifferences = primaryKeysInDatabase!.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)! - .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase))!; - foreach (var primaryKey in invalidPrimaryKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", primaryKey)); + CreateTable(false, table, dataCreation); } } - private void ValidateDbColumns(DatabaseSchemaResult result) + DatabaseSchemaCreatedNotification createdNotification = + new DatabaseSchemaCreatedNotification(eventMessages).WithStateFrom(creatingNotification); + FireAfterCreation(createdNotification); + } + + /// + /// Validates the schema of the current database. + /// + internal DatabaseSchemaResult ValidateSchema() => ValidateSchema(_orderedTables); + + internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables) + { + var result = new DatabaseSchemaResult(); + + result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database) + .Select(x => new DbIndexDefinition(x))); + + result.TableDefinitions.AddRange(orderedTables + .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); + + ValidateDbTables(result); + ValidateDbColumns(result); + ValidateDbIndexes(result); + ValidateDbConstraints(result); + + return result; + } + + /// + /// This validates the Primary/Foreign keys in the database + /// + /// + /// + /// This does not validate any database constraints that are not PKs or FKs because Umbraco does not create a database + /// with non PK/FK constraints. + /// Any unique "constraints" in the database are done with unique indexes. + /// + private void ValidateDbConstraints(DatabaseSchemaResult result) + { + //Check constraints in configured database against constraints in schema + var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); + var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")) + .Select(x => x.Item3).ToList(); + var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")) + .Select(x => x.Item3).ToList(); + + var unknownConstraintsInDatabase = constraintsInDatabase.Where( + x => x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && + x.Item3.InvariantStartsWith("IX_") == false + ).Select(x => x.Item3).ToList(); + + var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)) + .Where(x => x is not null).ToList(); + var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) + .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); + + // Add valid and invalid foreign key differences to the result object + // We'll need to do invariant contains with case insensitivity because foreign key, primary key is not standardized + // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. + foreach (var unknown in unknownConstraintsInDatabase) { - //Check columns in configured database against columns in schema - IEnumerable columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); - var columnsPerTableInDatabase = - columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); - var columnsPerTableInSchema = result.TableDefinitions - .SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); - //Add valid and invalid column differences to the result object - IEnumerable validColumnDifferences = - columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var column in validColumnDifferences) + if (foreignKeysInSchema!.InvariantContains(unknown) || primaryKeysInSchema!.InvariantContains(unknown)) { - result.ValidColumns.Add(column); - } - - IEnumerable invalidColumnDifferences = - columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, - StringComparer.InvariantCultureIgnoreCase)); - foreach (var column in invalidColumnDifferences) - { - result.Errors.Add(new Tuple("Column", column)); - } - } - - private void ValidateDbTables(DatabaseSchemaResult result) - { - //Check tables in configured database against tables in schema - var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); - var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); - //Add valid and invalid table differences to the result object - IEnumerable validTableDifferences = - tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var tableName in validTableDifferences) - { - if (tableName is not null) - { - result.ValidTables.Add(tableName); - } - } - - IEnumerable invalidTableDifferences = - tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var tableName in invalidTableDifferences) - { - result.Errors.Add(new Tuple("Table", tableName ?? "NULL")); - } - } - - private void ValidateDbIndexes(DatabaseSchemaResult result) - { - //These are just column indexes NOT constraints or Keys - //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); - var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); - var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); - - //Add valid and invalid index differences to the result object - IEnumerable validColIndexDifferences = - colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var index in validColIndexDifferences) - { - if (index is not null) - { - result.ValidIndexes.Add(index); - } - } - - IEnumerable invalidColIndexDifferences = - colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var index in invalidColIndexDifferences) - { - result.Errors.Add(new Tuple("Index", index ?? "NULL")); - } - } - - #region Notifications - - /// - /// Publishes the notification. - /// - /// Cancelable notification marking the creation having begun. - internal virtual void FireBeforeCreation(DatabaseSchemaCreatingNotification notification) => - _eventAggregator.Publish(notification); - - /// - /// Publishes the notification. - /// - /// Notification marking the creation having completed. - internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notification) => - _eventAggregator.Publish(notification); - - #endregion - - #region Utilities - - /// - /// Returns whether a table with the specified exists in the database. - /// - /// The name of the table. - /// true if the table exists; otherwise false. - /// - /// - /// if (schemaHelper.TableExist("MyTable")) - /// { - /// // do something when the table exists - /// } - /// - /// - public bool TableExists(string? tableName) => tableName is not null && SqlSyntax.DoesTableExist(_database, tableName); - - /// - /// Returns whether the table for the specified exists in the database. - /// - /// The type representing the DTO/table. - /// true if the table exists; otherwise false. - /// - /// - /// if (schemaHelper.TableExist<MyDto>) - /// { - /// // do something when the table exists - /// } - /// - /// - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - public bool TableExists() - { - TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); - return table != null && TableExists(table.Name); - } - - /// - /// Creates a new table in the database based on the type of . - /// - /// The type representing the DTO/table. - /// Whether the table should be overwritten if it already exists. - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. - /// - internal void CreateTable(bool overwrite = false) - where T : new() - { - Type tableType = typeof(T); - CreateTable( - overwrite, - tableType, - new DatabaseDataCreator( - _database, - _loggerFactory.CreateLogger(), - _umbracoVersion, - _defaultDataCreationSettings)); - } - - /// - /// Creates a new table in the database for the specified . - /// - /// Whether the table should be overwritten if it already exists. - /// The representing the table. - /// - /// - /// If has been decorated with an , the name from - /// that attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// If a table with the same name already exists, the parameter will determine - /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will - /// not do anything if the parameter is false. - /// This need to execute as part of a transaction. - /// - internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) - { - if (!_database.InTransaction) - { - throw new InvalidOperationException("Database is not in a transaction."); - } - - TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); - var tableName = tableDefinition.Name; - var tableExist = TableExists(tableName); - if (string.IsNullOrEmpty(tableName)) - { - throw new SqlNullValueException("Tablename was null"); - } - if (overwrite && tableExist) - { - _logger.LogInformation("Table {TableName} already exists, but will be recreated", tableName); - - DropTable(tableName); - tableExist = false; - } - - if (tableExist) - { - // The table exists and was not recreated/overwritten. - _logger.LogInformation("Table {TableName} already exists - no changes were made", tableName); - return; - } - - //Execute the Create Table sql - SqlSyntax.HandleCreateTable(_database, tableDefinition); - - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - { - // This should probably delegate to whole thing to the syntax provider - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); - } - - //Call the NewTable-event to trigger the insert of base/default data - //OnNewTable(tableName, _db, e, _logger); - - dataCreation.InitializeBaseData(tableName); - - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - { - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); - } - - if (overwrite) - { - _logger.LogInformation("Table {TableName} was recreated", tableName); + result.ValidConstraints.Add(unknown); } else { - _logger.LogInformation("New table {TableName} was created", tableName); + result.Errors.Add(new Tuple("Unknown", unknown)); } } - /// - /// Drops the table for the specified . - /// - /// The type representing the DTO/table. - /// - /// - /// schemaHelper.DropTable<MyDto>); - /// - /// - /// - /// If has been decorated with an , the name from that - /// attribute will be used for the table name. If the attribute is not present, the name - /// will be used instead. - /// - public void DropTable(string? tableName) + // Foreign keys: + IEnumerable validForeignKeyDifferences = + foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var foreignKey in validForeignKeyDifferences) { - var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); - _database.Execute(sql); + if (foreignKey is not null) + { + result.ValidConstraints.Add(foreignKey); + } } - #endregion + IEnumerable invalidForeignKeyDifferences = foreignKeysInDatabase + .Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var foreignKey in invalidForeignKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", foreignKey ?? "NULL")); + } + + // Primary keys: + // Add valid and invalid primary key differences to the result object + IEnumerable validPrimaryKeyDifferences = + primaryKeysInDatabase!.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)!; + foreach (var primaryKey in validPrimaryKeyDifferences) + { + result.ValidConstraints.Add(primaryKey); + } + + IEnumerable invalidPrimaryKeyDifferences = + primaryKeysInDatabase!.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase)! + .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase))!; + foreach (var primaryKey in invalidPrimaryKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", primaryKey)); + } } + + private void ValidateDbColumns(DatabaseSchemaResult result) + { + //Check columns in configured database against columns in schema + IEnumerable columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); + var columnsPerTableInDatabase = + columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); + var columnsPerTableInSchema = result.TableDefinitions + .SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); + //Add valid and invalid column differences to the result object + IEnumerable validColumnDifferences = + columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var column in validColumnDifferences) + { + result.ValidColumns.Add(column); + } + + IEnumerable invalidColumnDifferences = + columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, + StringComparer.InvariantCultureIgnoreCase)); + foreach (var column in invalidColumnDifferences) + { + result.Errors.Add(new Tuple("Column", column)); + } + } + + private void ValidateDbTables(DatabaseSchemaResult result) + { + //Check tables in configured database against tables in schema + var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); + var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); + //Add valid and invalid table differences to the result object + IEnumerable validTableDifferences = + tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var tableName in validTableDifferences) + { + if (tableName is not null) + { + result.ValidTables.Add(tableName); + } + } + + IEnumerable invalidTableDifferences = + tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var tableName in invalidTableDifferences) + { + result.Errors.Add(new Tuple("Table", tableName ?? "NULL")); + } + } + + private void ValidateDbIndexes(DatabaseSchemaResult result) + { + //These are just column indexes NOT constraints or Keys + //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); + var colIndexesInDatabase = result.IndexDefinitions.Select(x => x.IndexName).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + + //Add valid and invalid index differences to the result object + IEnumerable validColIndexDifferences = + colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var index in validColIndexDifferences) + { + if (index is not null) + { + result.ValidIndexes.Add(index); + } + } + + IEnumerable invalidColIndexDifferences = + colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var index in invalidColIndexDifferences) + { + result.Errors.Add(new Tuple("Index", index ?? "NULL")); + } + } + + #region Notifications + + /// + /// Publishes the notification. + /// + /// Cancelable notification marking the creation having begun. + internal virtual void FireBeforeCreation(DatabaseSchemaCreatingNotification notification) => + _eventAggregator.Publish(notification); + + /// + /// Publishes the notification. + /// + /// Notification marking the creation having completed. + internal virtual void FireAfterCreation(DatabaseSchemaCreatedNotification notification) => + _eventAggregator.Publish(notification); + + #endregion + + #region Utilities + + /// + /// Returns whether a table with the specified exists in the database. + /// + /// The name of the table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist("MyTable")) + /// { + /// // do something when the table exists + /// } + /// + /// + public bool TableExists(string? tableName) => + tableName is not null && SqlSyntax.DoesTableExist(_database, tableName); + + /// + /// Returns whether the table for the specified exists in the database. + /// + /// The type representing the DTO/table. + /// true if the table exists; otherwise false. + /// + /// + /// if (schemaHelper.TableExist<MyDto>) + /// { + /// // do something when the table exists + /// } + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + public bool TableExists() + { + TableDefinition table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + return table != null && TableExists(table.Name); + } + + /// + /// Creates a new table in the database based on the type of . + /// + /// The type representing the DTO/table. + /// Whether the table should be overwritten if it already exists. + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// + internal void CreateTable(bool overwrite = false) + where T : new() + { + Type tableType = typeof(T); + CreateTable( + overwrite, + tableType, + new DatabaseDataCreator( + _database, + _loggerFactory.CreateLogger(), + _umbracoVersion, + _defaultDataCreationSettings)); + } + + /// + /// Creates a new table in the database for the specified . + /// + /// Whether the table should be overwritten if it already exists. + /// The representing the table. + /// + /// + /// If has been decorated with an , the name from + /// that attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// If a table with the same name already exists, the parameter will determine + /// whether the table is overwritten. If true, the table will be overwritten, whereas this method will + /// not do anything if the parameter is false. + /// This need to execute as part of a transaction. + /// + internal void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) + { + if (!_database.InTransaction) + { + throw new InvalidOperationException("Database is not in a transaction."); + } + + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); + var tableName = tableDefinition.Name; + var tableExist = TableExists(tableName); + if (string.IsNullOrEmpty(tableName)) + { + throw new SqlNullValueException("Tablename was null"); + } + + if (overwrite && tableExist) + { + _logger.LogInformation("Table {TableName} already exists, but will be recreated", tableName); + + DropTable(tableName); + tableExist = false; + } + + if (tableExist) + { + // The table exists and was not recreated/overwritten. + _logger.LogInformation("Table {TableName} already exists - no changes were made", tableName); + return; + } + + //Execute the Create Table sql + SqlSyntax.HandleCreateTable(_database, tableDefinition); + + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { + // This should probably delegate to whole thing to the syntax provider + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); + } + + //Call the NewTable-event to trigger the insert of base/default data + //OnNewTable(tableName, _db, e, _logger); + + dataCreation.InitializeBaseData(tableName); + + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + { + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); + } + + if (overwrite) + { + _logger.LogInformation("Table {TableName} was recreated", tableName); + } + else + { + _logger.LogInformation("New table {TableName} was created", tableName); + } + } + + /// + /// Drops the table for the specified . + /// + /// The type representing the DTO/table. + /// + /// + /// schemaHelper.DropTable<MyDto>); + /// + /// + /// + /// If has been decorated with an , the name from that + /// attribute will be used for the table name. If the attribute is not present, the name + /// will be used instead. + /// + public void DropTable(string? tableName) + { + var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); + _database.Execute(sql); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs index d4d0507c0a..6c28f08eb6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreatorFactory.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -8,46 +7,44 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Creates the initial database schema during install. +/// +public class DatabaseSchemaCreatorFactory { - /// - /// Creates the initial database schema during install. - /// - public class DatabaseSchemaCreatorFactory + private readonly IEventAggregator _eventAggregator; + private readonly IOptionsMonitor _installDefaultDataSettings; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IUmbracoVersion _umbracoVersion; + + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in V11.")] + public DatabaseSchemaCreatorFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator) + : this(logger, loggerFactory, umbracoVersion, eventAggregator, + StaticServiceProvider.Instance.GetRequiredService>()) { - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IUmbracoVersion _umbracoVersion; - private readonly IEventAggregator _eventAggregator; - private readonly IOptionsMonitor _installDefaultDataSettings; - - [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in V11.")] - public DatabaseSchemaCreatorFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator) - : this(logger, loggerFactory, umbracoVersion, eventAggregator, StaticServiceProvider.Instance.GetRequiredService>()) - { - } - - public DatabaseSchemaCreatorFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IUmbracoVersion umbracoVersion, - IEventAggregator eventAggregator, - IOptionsMonitor installDefaultDataSettings) - { - _logger = logger; - _loggerFactory = loggerFactory; - _umbracoVersion = umbracoVersion; - _eventAggregator = eventAggregator; - _installDefaultDataSettings = installDefaultDataSettings; - } - - public DatabaseSchemaCreator Create(IUmbracoDatabase? database) - { - return new DatabaseSchemaCreator(database, _logger, _loggerFactory, _umbracoVersion, _eventAggregator, _installDefaultDataSettings); - } } + + public DatabaseSchemaCreatorFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IUmbracoVersion umbracoVersion, + IEventAggregator eventAggregator, + IOptionsMonitor installDefaultDataSettings) + { + _logger = logger; + _loggerFactory = loggerFactory; + _umbracoVersion = umbracoVersion; + _eventAggregator = eventAggregator; + _installDefaultDataSettings = installDefaultDataSettings; + } + + public DatabaseSchemaCreator Create(IUmbracoDatabase? database) => new DatabaseSchemaCreator(database, _logger, + _loggerFactory, _umbracoVersion, _eventAggregator, _installDefaultDataSettings); } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs index 83c4fd4cef..5865713cbb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaResult.cs @@ -1,103 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Migrations.Install +namespace Umbraco.Cms.Infrastructure.Migrations.Install; + +/// +/// Represents ... +/// +public class DatabaseSchemaResult { - /// - /// Represents ... - /// - public class DatabaseSchemaResult + public DatabaseSchemaResult() { - public DatabaseSchemaResult() + Errors = new List>(); + TableDefinitions = new List(); + ValidTables = new List(); + ValidColumns = new List(); + ValidConstraints = new List(); + ValidIndexes = new List(); + IndexDefinitions = new List(); + } + + public List> Errors { get; } + + public List TableDefinitions { get; } + + public List ValidTables { get; } + + // TODO: what are these exactly? TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? + internal List IndexDefinitions { get; } + + public List ValidColumns { get; } + + public List ValidConstraints { get; } + + public List ValidIndexes { get; } + + /// + /// Determines whether the database contains an installed version. + /// + /// + /// A database contains an installed version when it contains at least one valid table. + /// + public bool DetermineHasInstalledVersion() => ValidTables.Count > 0; + + /// + /// Gets a summary of the schema validation result + /// + /// A string containing a human readable string with a summary message + public string GetSummary() + { + var sb = new StringBuilder(); + if (Errors.Any() == false) { - Errors = new List>(); - TableDefinitions = new List(); - ValidTables = new List(); - ValidColumns = new List(); - ValidConstraints = new List(); - ValidIndexes = new List(); - IndexDefinitions = new List(); - } - - public List> Errors { get; } - - public List TableDefinitions { get; } - - // TODO: what are these exactly? TableDefinitions are those that should be there, IndexDefinitions are those that... are in DB? - internal List IndexDefinitions { get; } - - public List ValidTables { get; } - - public List ValidColumns { get; } - - public List ValidConstraints { get; } - - public List ValidIndexes { get; } - - /// - /// Determines whether the database contains an installed version. - /// - /// - /// A database contains an installed version when it contains at least one valid table. - /// - public bool DetermineHasInstalledVersion() - { - return ValidTables.Count > 0; - } - - /// - /// Gets a summary of the schema validation result - /// - /// A string containing a human readable string with a summary message - public string GetSummary() - { - var sb = new StringBuilder(); - if (Errors.Any() == false) - { - sb.AppendLine("The database schema validation didn't find any errors."); - return sb.ToString(); - } - - //Table error summary - if (Errors.Any(x => x.Item1.Equals("Table"))) - { - sb.AppendLine("The following tables were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Table")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Column error summary - if (Errors.Any(x => x.Item1.Equals("Column"))) - { - sb.AppendLine("The following columns were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Column")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Constraint error summary - if (Errors.Any(x => x.Item1.Equals("Constraint"))) - { - sb.AppendLine("The following constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Constraint")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Index error summary - if (Errors.Any(x => x.Item1.Equals("Index"))) - { - sb.AppendLine("The following indexes were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Index")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - //Unknown constraint error summary - if (Errors.Any(x => x.Item1.Equals("Unknown"))) - { - sb.AppendLine("The following unknown constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); - sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Unknown")).Select(x => x.Item2))); - sb.AppendLine(" "); - } - + sb.AppendLine("The database schema validation didn't find any errors."); return sb.ToString(); } + + // Table error summary + if (Errors.Any(x => x.Item1.Equals("Table"))) + { + sb.AppendLine("The following tables were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Table")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Column error summary + if (Errors.Any(x => x.Item1.Equals("Column"))) + { + sb.AppendLine("The following columns were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Column")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Constraint error summary + if (Errors.Any(x => x.Item1.Equals("Constraint"))) + { + sb.AppendLine( + "The following constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Constraint")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Index error summary + if (Errors.Any(x => x.Item1.Equals("Index"))) + { + sb.AppendLine("The following indexes were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Index")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + // Unknown constraint error summary + if (Errors.Any(x => x.Item1.Equals("Unknown"))) + { + sb.AppendLine( + "The following unknown constraints (Primary Keys, Foreign Keys and Indexes) were found in the database, but are not in the current schema:"); + sb.AppendLine(string.Join(",", Errors.Where(x => x.Item1.Equals("Unknown")).Select(x => x.Item2))); + sb.AppendLine(" "); + } + + return sb.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs b/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs index a25c161587..3e80d9ebc5 100644 --- a/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/MergeBuilder.cs @@ -1,94 +1,91 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Infrastructure.Migrations +/// +/// Represents a migration plan builder for merges. +/// +public class MergeBuilder { + private readonly List _migrations = new(); + private readonly MigrationPlan _plan; + private bool _with; + private string? _withLast; + /// - /// Represents a migration plan builder for merges. + /// Initializes a new instance of the class. /// - public class MergeBuilder + internal MergeBuilder(MigrationPlan plan) => _plan = plan; + + /// + /// Adds a transition to a target state through an empty migration. + /// + public MergeBuilder To(string targetState) + => To(targetState); + + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + /// + /// Adds a transition to a target state through a migration. + /// + public MergeBuilder To(string targetState, Type migration) { - private readonly MigrationPlan _plan; - private readonly List _migrations = new List(); - private string? _withLast; - private bool _with; - - /// - /// Initializes a new instance of the class. - /// - internal MergeBuilder(MigrationPlan plan) + if (_with) { - _plan = plan; + _withLast = targetState; + targetState = _plan.CreateRandomState(); + } + else + { + _migrations.Add(migration); } - /// - /// Adds a transition to a target state through an empty migration. - /// - public MergeBuilder To(string targetState) - => To(targetState); + _plan.To(targetState, migration); + return this; + } - /// - /// Adds a transition to a target state through a migration. - /// - public MergeBuilder To(string targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); - - /// - /// Adds a transition to a target state through a migration. - /// - public MergeBuilder To(string targetState, Type migration) + /// + /// Begins the second branch of the merge. + /// + public MergeBuilder With() + { + if (_with) { - if (_with) - { - _withLast = targetState; - targetState = _plan.CreateRandomState(); - } - else - { - _migrations.Add(migration); - } - - _plan.To(targetState, migration); - return this; + throw new InvalidOperationException("Cannot invoke With() twice."); } - /// - /// Begins the second branch of the merge. - /// - public MergeBuilder With() + _with = true; + return this; + } + + /// + /// Completes the merge. + /// + public MigrationPlan As(string targetState) + { + if (!_with) { - if (_with) - throw new InvalidOperationException("Cannot invoke With() twice."); - _with = true; - return this; + throw new InvalidOperationException("Cannot invoke As() without invoking With() first."); } - /// - /// Completes the merge. - /// - public MigrationPlan As(string targetState) + // reach final state + _plan.To(targetState); + + // restart at former end of branch2 + _plan.From(_withLast); + + // and replay all branch1 migrations + foreach (Type migration in _migrations) { - if (!_with) - { - throw new InvalidOperationException("Cannot invoke As() without invoking With() first."); - } - - // reach final state - _plan.To(targetState); - - // restart at former end of branch2 - _plan.From(_withLast); - - // and replay all branch1 migrations - foreach (var migration in _migrations) - { - _plan.To(_plan.CreateRandomState(), migration); - } - // reaching final state - _plan.To(targetState); - - return _plan; + _plan.To(_plan.CreateRandomState(), migration); } + + // reaching final state + _plan.To(targetState); + + return _plan; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs index 4ffc3ab1e4..a4c4c0c99a 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase.cs @@ -11,118 +11,122 @@ using Umbraco.Cms.Infrastructure.Migrations.Expressions.Update; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides a base class to all migrations. +/// +public abstract partial class MigrationBase : IDiscoverable { /// - /// Provides a base class to all migrations. + /// Initializes a new instance of the class. /// - public abstract partial class MigrationBase : IDiscoverable + /// A migration context. + protected MigrationBase(IMigrationContext context) + => Context = context; + + /// + /// Builds an Alter expression. + /// + public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); + + /// + /// Gets the migration context. + /// + protected IMigrationContext Context { get; } + + /// + /// Gets the logger. + /// + protected ILogger Logger => Context.Logger; + + /// + /// Gets the Sql syntax. + /// + protected ISqlSyntaxProvider SqlSyntax => Context.SqlContext.SqlSyntax; + + /// + /// Gets the database instance. + /// + protected IUmbracoDatabase Database => Context.Database; + + /// + /// Gets the database type. + /// + protected DatabaseType DatabaseType => Context.Database.DatabaseType; + + /// + /// Builds a Create expression. + /// + public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); + + /// + /// Builds a Delete expression. + /// + public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); + + /// + /// Builds an Execute expression. + /// + public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); + + /// + /// Builds an Insert expression. + /// + public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); + + /// + /// Builds a Rename expression. + /// + public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); + + /// + /// Builds an Update expression. + /// + public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); + + /// + /// Runs the migration. + /// + public void Run() { - /// - /// Initializes a new instance of the class. - /// - /// A migration context. - protected MigrationBase(IMigrationContext context) - => Context = context; + Migrate(); - /// - /// Gets the migration context. - /// - protected IMigrationContext Context { get; } - - /// - /// Gets the logger. - /// - protected ILogger Logger => Context.Logger; - - /// - /// Gets the Sql syntax. - /// - protected ISqlSyntaxProvider SqlSyntax => Context.SqlContext.SqlSyntax; - - /// - /// Gets the database instance. - /// - protected IUmbracoDatabase Database => Context.Database; - - /// - /// Gets the database type. - /// - protected DatabaseType DatabaseType => Context.Database.DatabaseType; - - /// - /// Creates a new Sql statement. - /// - protected Sql Sql() => Context.SqlContext.Sql(); - - /// - /// Creates a new Sql statement with arguments. - /// - protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); - - /// - /// Executes the migration. - /// - protected abstract void Migrate(); - - /// - /// Runs the migration. - /// - public void Run() - { - Migrate(); - - // ensure there is no building expression - // ie we did not forget to .Do() an expression - if (Context.BuildingExpression) - { - throw new IncompleteMigrationExpressionException("The migration has run, but leaves an expression that has not run."); - } - } - - // ensures we are not already building, + // ensure there is no building expression // ie we did not forget to .Do() an expression - private protected T BeginBuild(T builder) + if (Context.BuildingExpression) { - if (Context.BuildingExpression) - throw new IncompleteMigrationExpressionException("Cannot create a new expression: the previous expression has not run."); - Context.BuildingExpression = true; - return builder; + throw new IncompleteMigrationExpressionException( + "The migration has run, but leaves an expression that has not run."); + } + } + + /// + /// Creates a new Sql statement. + /// + protected Sql Sql() => Context.SqlContext.Sql(); + + /// + /// Creates a new Sql statement with arguments. + /// + protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args); + + /// + /// Executes the migration. + /// + protected abstract void Migrate(); + + // ensures we are not already building, + // ie we did not forget to .Do() an expression + private protected T BeginBuild(T builder) + { + if (Context.BuildingExpression) + { + throw new IncompleteMigrationExpressionException( + "Cannot create a new expression: the previous expression has not run."); } - /// - /// Builds an Alter expression. - /// - public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context)); - - /// - /// Builds a Create expression. - /// - public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context)); - - /// - /// Builds a Delete expression. - /// - public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context)); - - /// - /// Builds an Execute expression. - /// - public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context)); - - /// - /// Builds an Insert expression. - /// - public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context)); - - /// - /// Builds a Rename expression. - /// - public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context)); - - /// - /// Builds an Update expression. - /// - public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context)); + Context.BuildingExpression = true; + return builder; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs index 22f7771685..49775bcd0a 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; @@ -13,55 +13,72 @@ namespace Umbraco.Cms.Infrastructure.Migrations public abstract partial class MigrationBase { // provides extra methods for migrations - protected void AddColumn(string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); AddColumn(table, table.Name!, columnName); } protected void AddColumnIfNotExists(IEnumerable columns, string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); if (columns.Any(x => x.TableName.InvariantEquals(table.Name) && !x.ColumnName.InvariantEquals(columnName))) + { AddColumn(table, table.Name!, columnName); + } } protected void AddColumn(string tableName, string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); AddColumn(table, tableName, columnName); } protected void AddColumnIfNotExists(IEnumerable columns, string tableName, string columnName) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); if (columns.Any(x => x.TableName.InvariantEquals(tableName) && !x.ColumnName.InvariantEquals(columnName))) + { AddColumn(table, tableName, columnName); + } + } + + protected void AddColumn(string columnName, out IEnumerable sqls) + { + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + AddColumn(table, table.Name!, columnName, out sqls); } private void AddColumn(TableDefinition table, string tableName, string columnName) { - if (ColumnExists(tableName, columnName)) return; + if (ColumnExists(tableName, columnName)) + { + return; + } - var column = table.Columns.First(x => x.Name == columnName); + ColumnDefinition? column = table.Columns.First(x => x.Name == columnName); var createSql = SqlSyntax.Format(column); Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(tableName), createSql)).Do(); } - protected void AddColumn(string columnName, out IEnumerable sqls) - { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); - AddColumn(table, table.Name!, columnName, out sqls); - } - protected void AddColumn(string tableName, string columnName, out IEnumerable sqls) { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); AddColumn(table, tableName, columnName, out sqls); } + protected void AlterColumn(string tableName, string columnName) + { + TableDefinition? table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + ColumnDefinition? column = table.Columns.First(x => x.Name == columnName); + SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out IEnumerable? sqls); + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + } + private void AddColumn(TableDefinition table, string tableName, string columnName, out IEnumerable sqls) { if (ColumnExists(tableName, columnName)) @@ -70,20 +87,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations return; } - var column = table.Columns.First(x => x.Name == columnName); + ColumnDefinition? column = table.Columns.First(x => x.Name == columnName); var createSql = SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out sqls); Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(tableName), createSql)).Do(); } - protected void AlterColumn(string tableName, string columnName) - { - var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); - var column = table.Columns.First(x => x.Name == columnName); - SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out var sqls); - foreach (var sql in sqls) - Execute.Sql(sql).Do(); - } - protected void ReplaceColumn(string tableName, string currentName, string newName) { Execute.Sql(SqlSyntax.FormatColumnRename(tableName, currentName, newName)).Do(); @@ -92,26 +100,26 @@ namespace Umbraco.Cms.Infrastructure.Migrations protected bool TableExists(string tableName) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); + IEnumerable? tables = SqlSyntax.GetTablesInSchema(Context.Database); return tables.Any(x => x.InvariantEquals(tableName)); } protected bool IndexExists(string indexName) { - var indexes = SqlSyntax.GetDefinedIndexes(Context.Database); + IEnumerable>? indexes = SqlSyntax.GetDefinedIndexes(Context.Database); return indexes.Any(x => x.Item2.InvariantEquals(indexName)); } protected bool ColumnExists(string tableName, string columnName) { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); + ColumnInfo[]? columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); return columns.Any(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); } protected string? ColumnType(string tableName, string columnName) { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); - var column = columns.FirstOrDefault(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); + ColumnInfo[]? columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray(); + ColumnInfo? column = columns.FirstOrDefault(x => x.TableName.InvariantEquals(tableName) && x.ColumnName.InvariantEquals(columnName)); return column?.DataType; } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs index e68dc7a700..40db38e053 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationBuilder.cs @@ -1,20 +1,13 @@ -using System; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +public class MigrationBuilder : IMigrationBuilder { - public class MigrationBuilder : IMigrationBuilder - { - private readonly IServiceProvider _container; + private readonly IServiceProvider _container; - public MigrationBuilder(IServiceProvider container) - { - _container = container; - } + public MigrationBuilder(IServiceProvider container) => _container = container; - public MigrationBase Build(Type migrationType, IMigrationContext context) - { - return (MigrationBase) _container.CreateInstance(migrationType, context); - } - } + public MigrationBase Build(Type migrationType, IMigrationContext context) => + (MigrationBase)_container.CreateInstance(migrationType, context); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs index 975df9120d..eaf2eb4f4d 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs @@ -1,55 +1,50 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Implements . +/// +internal class MigrationContext : IMigrationContext { + private readonly List _postMigrations = new(); + /// - /// Implements . + /// Initializes a new instance of the class. /// - internal class MigrationContext : IMigrationContext + public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger logger) { - private readonly List _postMigrations = new List(); - - /// - /// Initializes a new instance of the class. - /// - public MigrationContext(MigrationPlan plan, IUmbracoDatabase? database, ILogger logger) - { - Plan = plan; - Database = database ?? throw new ArgumentNullException(nameof(database)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _postMigrations.AddRange(plan.PostMigrationTypes); - } - - /// - public ILogger Logger { get; } - - public MigrationPlan Plan { get; } - - /// - public IUmbracoDatabase Database { get; } - - /// - public ISqlContext SqlContext => Database.SqlContext; - - /// - public int Index { get; set; } - - /// - public bool BuildingExpression { get; set; } - - // this is only internally exposed - public IReadOnlyList PostMigrations => _postMigrations; - - /// - public void AddPostMigration() - where TMigration : MigrationBase - { - // just adding - will be de-duplicated when executing - _postMigrations.Add(typeof(TMigration)); - } + Plan = plan; + Database = database ?? throw new ArgumentNullException(nameof(database)); + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _postMigrations.AddRange(plan.PostMigrationTypes); } + + // this is only internally exposed + public IReadOnlyList PostMigrations => _postMigrations; + + /// + public ILogger Logger { get; } + + public MigrationPlan Plan { get; } + + /// + public IUmbracoDatabase Database { get; } + + /// + public ISqlContext SqlContext => Database.SqlContext; + + /// + public int Index { get; set; } + + /// + public bool BuildingExpression { get; set; } + + /// + public void AddPostMigration() + where TMigration : MigrationBase => + + // just adding - will be de-duplicated when executing + _postMigrations.Add(typeof(TMigration)); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs index 4838467197..193d15b172 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationExpressionBase.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Text; using Microsoft.Extensions.Logging; using NPoco; @@ -8,155 +5,175 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Provides a base class for migration expressions. +/// +public abstract class MigrationExpressionBase : IMigrationExpression { + private bool _executed; + private List? _expressions; + + protected MigrationExpressionBase(IMigrationContext context) => + Context = context ?? throw new ArgumentNullException(nameof(context)); + + public DatabaseType DatabaseType => Context.Database.DatabaseType; + + protected IMigrationContext Context { get; } + + protected ILogger Logger => Context.Logger; + + protected ISqlSyntaxProvider SqlSyntax => Context.Database.SqlContext.SqlSyntax; + + protected IUmbracoDatabase Database => Context.Database; + + public List Expressions => _expressions ??= new List(); + /// - /// Provides a base class for migration expressions. + /// This might be useful in the future if we add it to the interface, but for now it's used to hack the DeleteAppTables + /// & DeleteForeignKeyExpression + /// to ensure they are not executed twice. /// - public abstract class MigrationExpressionBase : IMigrationExpression + internal string? Name { get; set; } + + public virtual void Execute() { - private bool _executed; - private List? _expressions; - - protected MigrationExpressionBase(IMigrationContext context) + if (_executed) { - Context = context ?? throw new ArgumentNullException(nameof(context)); + throw new InvalidOperationException("This expression has already been executed."); } - protected IMigrationContext Context { get; } + _executed = true; + Context.BuildingExpression = false; - protected ILogger Logger => Context.Logger; + var sql = GetSql(); - protected ISqlSyntaxProvider SqlSyntax => Context.Database.SqlContext.SqlSyntax; - - protected IUmbracoDatabase Database => Context.Database; - - public DatabaseType DatabaseType => Context.Database.DatabaseType; - - public List Expressions => _expressions ?? (_expressions = new List()); - - protected virtual string? GetSql() + if (string.IsNullOrWhiteSpace(sql)) { - return ToString(); + Logger.LogInformation("SQL [{ContextIndex}]: ", Context.Index); } - - public virtual void Execute() + else { - if (_executed) - throw new InvalidOperationException("This expression has already been executed."); - _executed = true; - Context.BuildingExpression = false; - - var sql = GetSql(); - - if (string.IsNullOrWhiteSpace(sql)) + // split multiple statements - required for SQL CE + // http://stackoverflow.com/questions/13665491/sql-ce-inconsistent-with-multiple-statements + var stmtBuilder = new StringBuilder(); + using (var reader = new StringReader(sql)) { - Logger.LogInformation("SQL [{ContextIndex}]: ", Context.Index); - } - else - { - // split multiple statements - required for SQL CE - // http://stackoverflow.com/questions/13665491/sql-ce-inconsistent-with-multiple-statements - var stmtBuilder = new StringBuilder(); - using (var reader = new StringReader(sql)) + string? line; + while ((line = reader.ReadLine()) != null) { - string? line; - while ((line = reader.ReadLine()) != null) + if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) { - if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase)) - ExecuteStatement(stmtBuilder); - else - stmtBuilder.Append(line); - } - - if (stmtBuilder.Length > 0) ExecuteStatement(stmtBuilder); + } + else + { + stmtBuilder.Append(line); + } + } + + if (stmtBuilder.Length > 0) + { + ExecuteStatement(stmtBuilder); } } - - Context.Index++; - - if (_expressions == null) - return; - - foreach (var expression in _expressions) - expression.Execute(); } - protected void Execute(Sql? sql) + Context.Index++; + + if (_expressions == null) { - if (_executed) - throw new InvalidOperationException("This expression has already been executed."); - _executed = true; - Context.BuildingExpression = false; - - if (sql == null) - { - Logger.LogInformation($"SQL [{Context.Index}]: "); - } - else - { - Logger.LogInformation($"SQL [{Context.Index}]: {sql.ToText()}"); - Database.Execute(sql); - } - - Context.Index++; - - if (_expressions == null) - return; - - foreach (var expression in _expressions) - expression.Execute(); + return; } - private void ExecuteStatement(StringBuilder stmtBuilder) + foreach (IMigrationExpression expression in _expressions) { - var stmt = stmtBuilder.ToString(); - Logger.LogInformation("SQL [{ContextIndex}]: {Sql}", Context.Index, stmt); - Database.Execute(stmt); - stmtBuilder.Clear(); + expression.Execute(); + } + } + + protected virtual string? GetSql() => ToString(); + + protected void Execute(Sql? sql) + { + if (_executed) + { + throw new InvalidOperationException("This expression has already been executed."); } - protected void AppendStatementSeparator(StringBuilder stmtBuilder) + _executed = true; + Context.BuildingExpression = false; + + if (sql == null) { - stmtBuilder.AppendLine(";"); - if (DatabaseType.IsSqlServer()) - stmtBuilder.AppendLine("GO"); + Logger.LogInformation($"SQL [{Context.Index}]: "); + } + else + { + Logger.LogInformation($"SQL [{Context.Index}]: {sql.ToText()}"); + Database.Execute(sql); } - /// - /// This might be useful in the future if we add it to the interface, but for now it's used to hack the DeleteAppTables & DeleteForeignKeyExpression - /// to ensure they are not executed twice. - /// - internal string? Name { get; set; } + Context.Index++; - protected string GetQuotedValue(object? val) + if (_expressions == null) { - if (val == null) return "NULL"; + return; + } - var type = val.GetType(); + foreach (IMigrationExpression expression in _expressions) + { + expression.Execute(); + } + } - switch (Type.GetTypeCode(type)) - { - case TypeCode.Boolean: - return ((bool)val) ? "1" : "0"; - case TypeCode.Single: - case TypeCode.Double: - case TypeCode.Decimal: - case TypeCode.SByte: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Byte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return val.ToString()!; - case TypeCode.DateTime: - return SqlSyntax.GetQuotedValue(SqlSyntax.FormatDateTime((DateTime) val)); - default: - return SqlSyntax.GetQuotedValue(val.ToString()!); - } + protected void AppendStatementSeparator(StringBuilder stmtBuilder) + { + stmtBuilder.AppendLine(";"); + if (DatabaseType.IsSqlServer()) + { + stmtBuilder.AppendLine("GO"); + } + } + + private void ExecuteStatement(StringBuilder stmtBuilder) + { + var stmt = stmtBuilder.ToString(); + Logger.LogInformation("SQL [{ContextIndex}]: {Sql}", Context.Index, stmt); + Database.Execute(stmt); + stmtBuilder.Clear(); + } + + protected string GetQuotedValue(object? val) + { + if (val == null) + { + return "NULL"; + } + + Type type = val.GetType(); + + switch (Type.GetTypeCode(type)) + { + case TypeCode.Boolean: + return (bool)val ? "1" : "0"; + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Byte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + return val.ToString()!; + case TypeCode.DateTime: + return SqlSyntax.GetQuotedValue(SqlSyntax.FormatDateTime((DateTime)val)); + default: + return SqlSyntax.GetQuotedValue(val.ToString()!); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs index 091eebe496..68b870bb7c 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlan.cs @@ -1,394 +1,469 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Extensions; -using Type = System.Type; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +/// +/// Represents a migration plan. +/// +public class MigrationPlan { + private readonly List _postMigrationTypes = new(); + private readonly Dictionary _transitions = new(StringComparer.InvariantCultureIgnoreCase); + private string? _finalState; + + private string? _prevState; /// - /// Represents a migration plan. + /// Initializes a new instance of the class. /// - public class MigrationPlan + /// The name of the plan. + public MigrationPlan(string name) { - private readonly Dictionary _transitions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly List _postMigrationTypes = new List(); - - private string? _prevState; - private string? _finalState; - - /// - /// Initializes a new instance of the class. - /// - /// The name of the plan. - public MigrationPlan(string name) + if (name == null) { - if (name == null) + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(name)); + } + + Name = name; + } + + /// + /// If set to true the plan executor will ignore any current state persisted and + /// run the plan from its initial state to its end state. + /// + public virtual bool IgnoreCurrentState { get; } = false; + + /// + /// Gets the transitions. + /// + public IReadOnlyDictionary Transitions => _transitions; + + public IReadOnlyList PostMigrationTypes => _postMigrationTypes; + + /// + /// Gets the name of the plan. + /// + public string Name { get; } + + /// + /// Gets the initial state. + /// + /// + /// The initial state is the state when the plan has never + /// run. By default, it is the empty string, but plans may override + /// it if they have other ways of determining where to start from. + /// + public virtual string InitialState => string.Empty; + + /// + /// Gets the final state. + /// + public string FinalState + { + get + { + // modifying the plan clears _finalState + // Validate() either sets _finalState, or throws + if (_finalState == null) { - throw new ArgumentNullException(nameof(name)); + Validate(); } - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); - } - - Name = name; - } - - /// - /// If set to true the plan executor will ignore any current state persisted and - /// run the plan from its initial state to its end state. - /// - public virtual bool IgnoreCurrentState { get; } = false; - - /// - /// Gets the transitions. - /// - public IReadOnlyDictionary Transitions => _transitions; - - public IReadOnlyList PostMigrationTypes => _postMigrationTypes; - - /// - /// Gets the name of the plan. - /// - public string Name { get; } - - // adds a transition - private MigrationPlan Add(string? sourceState, string targetState, Type? migration) - { - if (sourceState == null) - throw new ArgumentNullException(nameof(sourceState), $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(MigrationPlan.From)} must not have been called."); - if (targetState == null) - throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (sourceState == targetState) - throw new ArgumentException("Source and target state cannot be identical."); - if (migration == null) - throw new ArgumentNullException(nameof(migration)); - if (!migration.Implements()) - throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); - - sourceState = sourceState.Trim(); - targetState = targetState.Trim(); - - // throw if we already have a transition for that state which is not null, - // null is used to keep track of the last step of the chain - if (_transitions.ContainsKey(sourceState) && _transitions[sourceState] != null) - throw new InvalidOperationException($"A transition from state \"{sourceState}\" has already been defined."); - - // register the transition - _transitions[sourceState] = new Transition(sourceState, targetState, migration); - - // register the target state if we don't know it already - // this is how we keep track of the final state - because - // transitions could be defined in any order, that might - // be overridden afterwards. - if (!_transitions.ContainsKey(targetState)) - _transitions.Add(targetState, null); - - _prevState = targetState; - _finalState = null; // force re-validation - - return this; - } - - /// - /// Adds a transition to a target state through an empty migration. - /// - public MigrationPlan To(string targetState) - => To(targetState); - - public MigrationPlan To(Guid targetState) - => To(targetState.ToString()); - - /// - /// Adds a transition to a target state through a migration. - /// - public MigrationPlan To(string targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); - - public MigrationPlan To(Guid targetState) - where TMigration : MigrationBase - => To(targetState, typeof(TMigration)); - - /// - /// Adds a transition to a target state through a migration. - /// - public MigrationPlan To(string targetState, Type? migration) - => Add(_prevState, targetState, migration); - - public MigrationPlan To(Guid targetState, Type migration) - => Add(_prevState, targetState.ToString(), migration); - - /// - /// Sets the starting state. - /// - public MigrationPlan From(string? sourceState) - { - _prevState = sourceState ?? throw new ArgumentNullException(nameof(sourceState)); - return this; - } - - /// - /// Adds a transition to a target state through a migration, replacing a previous migration. - /// - /// The new migration. - /// The migration to use to recover from the previous target state. - /// The previous target state, which we need to recover from through . - /// The new target state. - public MigrationPlan ToWithReplace(string recoverState, string targetState) - where TMigrationNew : MigrationBase - where TMigrationRecover : MigrationBase - { - To(targetState); - From(recoverState).To(targetState); - return this; - } - - /// - /// Adds a transition to a target state through a migration, replacing a previous migration. - /// - /// The new migration. - /// The previous target state, which we can recover from directly. - /// The new target state. - public MigrationPlan ToWithReplace(string recoverState, string targetState) - where TMigrationNew : MigrationBase - { - To(targetState); - From(recoverState).To(targetState); - return this; - } - - /// - /// Adds transitions to a target state by cloning transitions from a start state to an end state. - /// - public MigrationPlan ToWithClone(string startState, string endState, string targetState) - { - if (startState == null) - throw new ArgumentNullException(nameof(startState)); - if (string.IsNullOrWhiteSpace(startState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(startState)); - if (endState == null) - throw new ArgumentNullException(nameof(endState)); - if (string.IsNullOrWhiteSpace(endState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(endState)); - if (targetState == null) - throw new ArgumentNullException(nameof(targetState)); - if (string.IsNullOrWhiteSpace(targetState)) - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(targetState)); - if (startState == endState) - throw new ArgumentException("Start and end states cannot be identical."); - - startState = startState.Trim(); - endState = endState.Trim(); - targetState = targetState.Trim(); - - var state = startState; - var visited = new HashSet(); - - while (state != endState) - { - if (state is null || visited.Contains(state)) - throw new InvalidOperationException("A loop was detected in the copied chain."); - visited.Add(state); - - if (!_transitions.TryGetValue(state, out var transition)) - throw new InvalidOperationException($"There is no transition from state \"{state}\"."); - - var newTargetState = transition?.TargetState == endState - ? targetState - : CreateRandomState(); - To(newTargetState, transition?.MigrationType); - state = transition?.TargetState; - } - - return this; - } - - /// - /// Adds a post-migration to the plan. - /// - public virtual MigrationPlan AddPostMigration() - where TMigration : MigrationBase - { - // TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead. - // The only place we use this is to clear cookies in the installer which could be done - // via notification. Then we can clean up all the code related to post migrations which is - // not insignificant. - - _postMigrationTypes.Add(typeof(TMigration)); - return this; - } - - /// - /// Creates a random, unique state. - /// - public virtual string CreateRandomState() - => Guid.NewGuid().ToString("B").ToUpper(); - - /// - /// Begins a merge. - /// - public MergeBuilder Merge() => new MergeBuilder(this); - - /// - /// Gets the initial state. - /// - /// The initial state is the state when the plan has never - /// run. By default, it is the empty string, but plans may override - /// it if they have other ways of determining where to start from. - public virtual string InitialState => string.Empty; - - /// - /// Gets the final state. - /// - public string FinalState - { - get - { - // modifying the plan clears _finalState - // Validate() either sets _finalState, or throws - if (_finalState == null) - Validate(); - - return _finalState!; - } - } - - /// - /// Validates the plan. - /// - /// The plan's final state. - public void Validate() - { - if (_finalState != null) - return; - - // quick check for dead ends - a dead end is a transition that has a target state - // that is not null and does not match any source state. such a target state has - // been registered as a source state with a null transition. so there should be only - // one. - string? finalState = null; - foreach (var kvp in _transitions.Where(x => x.Value == null)) - { - if (finalState == null) - finalState = kvp.Key; - else - throw new InvalidOperationException($"Multiple final states have been detected in the plan (\"{finalState}\", \"{kvp.Key}\")." - + " Make sure the plan contains only one final state."); - } - - // now check for loops - var verified = new List(); - foreach (var transition in _transitions.Values) - { - if (transition == null || verified.Contains(transition.SourceState)) - continue; - - var visited = new List { transition.SourceState }; - var nextTransition = _transitions[transition.TargetState]; - while (nextTransition != null && !verified.Contains(nextTransition.SourceState)) - { - if (visited.Contains(nextTransition.SourceState)) - throw new InvalidOperationException($"A loop has been detected in the plan around state \"{nextTransition.SourceState}\"." - + " Make sure the plan does not contain circular transition paths."); - visited.Add(nextTransition.SourceState); - nextTransition = _transitions[nextTransition.TargetState]; - } - verified.AddRange(visited); - } - - _finalState = finalState!; - } - - /// - /// Throws an exception when the initial state is unknown. - /// - public virtual void ThrowOnUnknownInitialState(string state) - { - throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); - } - - /// - /// Follows a path (for tests and debugging). - /// - /// Does the same thing Execute does, but does not actually execute migrations. - internal IReadOnlyList FollowPath(string? fromState = null, string? toState = null) - { - toState = toState?.NullOrWhiteSpaceAsNull(); - - Validate(); - - var origState = fromState ?? string.Empty; - var states = new List { origState }; - - if (!_transitions.TryGetValue(origState, out var transition)) - throw new InvalidOperationException($"Unknown state \"{origState}\"."); - - while (transition != null) - { - var nextState = transition.TargetState; - origState = nextState; - states.Add(origState); - - if (nextState == toState) - { - transition = null; - continue; - } - - if (!_transitions.TryGetValue(origState, out transition)) - throw new InvalidOperationException($"Unknown state \"{origState}\"."); - } - - // safety check - if (origState != (toState ?? _finalState)) - throw new InvalidOperationException($"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); - - return states; - } - - /// - /// Represents a plan transition. - /// - public class Transition - { - /// - /// Initializes a new instance of the class. - /// - public Transition(string sourceState, string targetState, Type migrationTtype) - { - SourceState = sourceState; - TargetState = targetState; - MigrationType = migrationTtype; - } - - /// - /// Gets the source state. - /// - public string SourceState { get; } - - /// - /// Gets the target state. - /// - public string TargetState { get; } - - /// - /// Gets the migration type. - /// - public Type MigrationType { get; } - - /// - public override string ToString() - { - return MigrationType == typeof(NoopMigration) - ? $"{(SourceState == string.Empty ? "" : SourceState)} --> {TargetState}" - : $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}"; - } + return _finalState!; } } + + /// + /// Adds a transition to a target state through an empty migration. + /// + public MigrationPlan To(string targetState) + => To(targetState); + + // adds a transition + private MigrationPlan Add(string? sourceState, string targetState, Type? migration) + { + if (sourceState == null) + { + throw new ArgumentNullException( + nameof(sourceState), + $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(From)} must not have been called."); + } + + if (targetState == null) + { + throw new ArgumentNullException(nameof(targetState)); + } + + if (string.IsNullOrWhiteSpace(targetState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(targetState)); + } + + if (sourceState == targetState) + { + throw new ArgumentException("Source and target state cannot be identical."); + } + + if (migration == null) + { + throw new ArgumentNullException(nameof(migration)); + } + + if (!migration.Implements()) + { + throw new ArgumentException($"Type {migration.Name} does not implement IMigration.", nameof(migration)); + } + + sourceState = sourceState.Trim(); + targetState = targetState.Trim(); + + // throw if we already have a transition for that state which is not null, + // null is used to keep track of the last step of the chain + if (_transitions.ContainsKey(sourceState) && _transitions[sourceState] != null) + { + throw new InvalidOperationException($"A transition from state \"{sourceState}\" has already been defined."); + } + + // register the transition + _transitions[sourceState] = new Transition(sourceState, targetState, migration); + + // register the target state if we don't know it already + // this is how we keep track of the final state - because + // transitions could be defined in any order, that might + // be overridden afterwards. + if (!_transitions.ContainsKey(targetState)) + { + _transitions.Add(targetState, null); + } + + _prevState = targetState; + _finalState = null; // force re-validation + + return this; + } + + public MigrationPlan To(Guid targetState) + => To(targetState.ToString()); + + /// + /// Adds a transition to a target state through a migration. + /// + public MigrationPlan To(string targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + public MigrationPlan To(Guid targetState) + where TMigration : MigrationBase + => To(targetState, typeof(TMigration)); + + /// + /// Adds a transition to a target state through a migration. + /// + public MigrationPlan To(string targetState, Type? migration) + => Add(_prevState, targetState, migration); + + public MigrationPlan To(Guid targetState, Type migration) + => Add(_prevState, targetState.ToString(), migration); + + /// + /// Sets the starting state. + /// + public MigrationPlan From(string? sourceState) + { + _prevState = sourceState ?? throw new ArgumentNullException(nameof(sourceState)); + return this; + } + + /// + /// Adds a transition to a target state through a migration, replacing a previous migration. + /// + /// The new migration. + /// The migration to use to recover from the previous target state. + /// + /// The previous target state, which we need to recover from through + /// . + /// + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew : MigrationBase + where TMigrationRecover : MigrationBase + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds a transition to a target state through a migration, replacing a previous migration. + /// + /// The new migration. + /// The previous target state, which we can recover from directly. + /// The new target state. + public MigrationPlan ToWithReplace(string recoverState, string targetState) + where TMigrationNew : MigrationBase + { + To(targetState); + From(recoverState).To(targetState); + return this; + } + + /// + /// Adds transitions to a target state by cloning transitions from a start state to an end state. + /// + public MigrationPlan ToWithClone(string startState, string endState, string targetState) + { + if (startState == null) + { + throw new ArgumentNullException(nameof(startState)); + } + + if (string.IsNullOrWhiteSpace(startState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(startState)); + } + + if (endState == null) + { + throw new ArgumentNullException(nameof(endState)); + } + + if (string.IsNullOrWhiteSpace(endState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(endState)); + } + + if (targetState == null) + { + throw new ArgumentNullException(nameof(targetState)); + } + + if (string.IsNullOrWhiteSpace(targetState)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(targetState)); + } + + if (startState == endState) + { + throw new ArgumentException("Start and end states cannot be identical."); + } + + startState = startState.Trim(); + endState = endState.Trim(); + targetState = targetState.Trim(); + + var state = startState; + var visited = new HashSet(); + + while (state != endState) + { + if (state is null || visited.Contains(state)) + { + throw new InvalidOperationException("A loop was detected in the copied chain."); + } + + visited.Add(state); + + if (!_transitions.TryGetValue(state, out Transition? transition)) + { + throw new InvalidOperationException($"There is no transition from state \"{state}\"."); + } + + var newTargetState = transition?.TargetState == endState + ? targetState + : CreateRandomState(); + To(newTargetState, transition?.MigrationType); + state = transition?.TargetState; + } + + return this; + } + + /// + /// Adds a post-migration to the plan. + /// + public virtual MigrationPlan AddPostMigration() + where TMigration : MigrationBase + { + // TODO: Post migrations are obsolete/irrelevant. Notifications should be used instead. + // The only place we use this is to clear cookies in the installer which could be done + // via notification. Then we can clean up all the code related to post migrations which is + // not insignificant. + _postMigrationTypes.Add(typeof(TMigration)); + return this; + } + + /// + /// Creates a random, unique state. + /// + public virtual string CreateRandomState() + => Guid.NewGuid().ToString("B").ToUpper(); + + /// + /// Begins a merge. + /// + public MergeBuilder Merge() => new(this); + + /// + /// Validates the plan. + /// + /// The plan's final state. + public void Validate() + { + if (_finalState != null) + { + return; + } + + // quick check for dead ends - a dead end is a transition that has a target state + // that is not null and does not match any source state. such a target state has + // been registered as a source state with a null transition. so there should be only + // one. + string? finalState = null; + foreach (KeyValuePair kvp in _transitions.Where(x => x.Value == null)) + { + if (finalState == null) + { + finalState = kvp.Key; + } + else + { + throw new InvalidOperationException( + $"Multiple final states have been detected in the plan (\"{finalState}\", \"{kvp.Key}\")." + + " Make sure the plan contains only one final state."); + } + } + + // now check for loops + var verified = new List(); + foreach (Transition? transition in _transitions.Values) + { + if (transition == null || verified.Contains(transition.SourceState)) + { + continue; + } + + var visited = new List { transition.SourceState }; + Transition? nextTransition = _transitions[transition.TargetState]; + while (nextTransition != null && !verified.Contains(nextTransition.SourceState)) + { + if (visited.Contains(nextTransition.SourceState)) + { + throw new InvalidOperationException( + $"A loop has been detected in the plan around state \"{nextTransition.SourceState}\"." + + " Make sure the plan does not contain circular transition paths."); + } + + visited.Add(nextTransition.SourceState); + nextTransition = _transitions[nextTransition.TargetState]; + } + + verified.AddRange(visited); + } + + _finalState = finalState!; + } + + /// + /// Throws an exception when the initial state is unknown. + /// + public virtual void ThrowOnUnknownInitialState(string state) => + throw new InvalidOperationException($"The migration plan does not support migrating from state \"{state}\"."); + + /// + /// Follows a path (for tests and debugging). + /// + /// Does the same thing Execute does, but does not actually execute migrations. + internal IReadOnlyList FollowPath(string? fromState = null, string? toState = null) + { + toState = toState?.NullOrWhiteSpaceAsNull(); + + Validate(); + + var origState = fromState ?? string.Empty; + var states = new List { origState }; + + if (!_transitions.TryGetValue(origState, out Transition? transition)) + { + throw new InvalidOperationException($"Unknown state \"{origState}\"."); + } + + while (transition != null) + { + var nextState = transition.TargetState; + origState = nextState; + states.Add(origState); + + if (nextState == toState) + { + transition = null; + continue; + } + + if (!_transitions.TryGetValue(origState, out transition)) + { + throw new InvalidOperationException($"Unknown state \"{origState}\"."); + } + } + + // safety check + if (origState != (toState ?? _finalState)) + { + throw new InvalidOperationException( + $"Internal error, reached state {origState} which is not state {toState ?? _finalState}"); + } + + return states; + } + + /// + /// Represents a plan transition. + /// + public class Transition + { + /// + /// Initializes a new instance of the class. + /// + public Transition(string sourceState, string targetState, Type migrationTtype) + { + SourceState = sourceState; + TargetState = targetState; + MigrationType = migrationTtype; + } + + /// + /// Gets the source state. + /// + public string SourceState { get; } + + /// + /// Gets the target state. + /// + public string TargetState { get; } + + /// + /// Gets the migration type. + /// + public Type MigrationType { get; } + + /// + public override string ToString() => + MigrationType == typeof(NoopMigration) + ? $"{(SourceState == string.Empty ? "" : SourceState)} --> {TargetState}" + : $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}"; + } } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index a89f89c7bc..caf498132e 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -1,118 +1,118 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Scoping; -using Umbraco.Extensions; -using Type = System.Type; -namespace Umbraco.Cms.Infrastructure.Migrations +namespace Umbraco.Cms.Infrastructure.Migrations; + +public class MigrationPlanExecutor : IMigrationPlanExecutor { - public class MigrationPlanExecutor : IMigrationPlanExecutor - { - private readonly ICoreScopeProvider _scopeProvider; - private readonly IScopeAccessor _scopeAccessor; - private readonly ILoggerFactory _loggerFactory; - private readonly IMigrationBuilder _migrationBuilder; - private readonly ILogger _logger; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMigrationBuilder _migrationBuilder; + private readonly IScopeAccessor _scopeAccessor; + private readonly ICoreScopeProvider _scopeProvider; - public MigrationPlanExecutor( - ICoreScopeProvider scopeProvider, - IScopeAccessor scopeAccessor, - ILoggerFactory loggerFactory, - IMigrationBuilder migrationBuilder) + public MigrationPlanExecutor( + ICoreScopeProvider scopeProvider, + IScopeAccessor scopeAccessor, + ILoggerFactory loggerFactory, + IMigrationBuilder migrationBuilder) + { + _scopeProvider = scopeProvider; + _scopeAccessor = scopeAccessor; + _loggerFactory = loggerFactory; + _migrationBuilder = migrationBuilder; + _logger = _loggerFactory.CreateLogger(); + } + + /// + /// Executes the plan. + /// + /// A scope. + /// The state to start execution at. + /// A migration builder. + /// A logger. + /// + /// The final state. + /// The plan executes within the scope, which must then be completed. + public string Execute(MigrationPlan plan, string fromState) + { + plan.Validate(); + + _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); + + fromState ??= string.Empty; + var nextState = fromState; + + _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState); + + if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition)) { - _scopeProvider = scopeProvider; - _scopeAccessor = scopeAccessor; - _loggerFactory = loggerFactory; - _migrationBuilder = migrationBuilder; - _logger = _loggerFactory.CreateLogger(); + plan.ThrowOnUnknownInitialState(nextState); } - /// - /// Executes the plan. - /// - /// A scope. - /// The state to start execution at. - /// A migration builder. - /// A logger. - /// - /// The final state. - /// The plan executes within the scope, which must then be completed. - public string Execute(MigrationPlan plan, string fromState) + using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) { - plan.Validate(); - - _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); - - fromState ??= string.Empty; - var nextState = fromState; - - _logger.LogInformation("At {OrigState}", string.IsNullOrWhiteSpace(nextState) ? "origin" : nextState); - - if (!plan.Transitions.TryGetValue(nextState, out MigrationPlan.Transition? transition)) + // We want to suppress scope (service, etc...) notifications during a migration plan + // execution. This is because if a package that doesn't have their migration plan + // executed is listening to service notifications to perform some persistence logic, + // that packages notification handlers may explode because that package isn't fully installed yet. + using (scope.Notifications.Suppress()) { - plan.ThrowOnUnknownInitialState(nextState); - } + var context = new MigrationContext(plan, _scopeAccessor.AmbientScope?.Database, + _loggerFactory.CreateLogger()); - using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true)) - { - // We want to suppress scope (service, etc...) notifications during a migration plan - // execution. This is because if a package that doesn't have their migration plan - // executed is listening to service notifications to perform some persistence logic, - // that packages notification handlers may explode because that package isn't fully installed yet. - using (scope.Notifications.Suppress()) + while (transition != null) { - var context = new MigrationContext(plan, _scopeAccessor.AmbientScope?.Database, _loggerFactory.CreateLogger()); + _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - while (transition != null) + MigrationBase migration = _migrationBuilder.Build(transition.MigrationType, context); + migration.Run(); + + nextState = transition.TargetState; + + _logger.LogInformation("At {OrigState}", nextState); + + // throw a raw exception here: this should never happen as the plan has + // been validated - this is just a paranoid safety test + if (!plan.Transitions.TryGetValue(nextState, out transition)) { - _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); - - var migration = _migrationBuilder.Build(transition.MigrationType, context); - migration.Run(); - - nextState = transition.TargetState; - - _logger.LogInformation("At {OrigState}", nextState); - - // throw a raw exception here: this should never happen as the plan has - // been validated - this is just a paranoid safety test - if (!plan.Transitions.TryGetValue(nextState, out transition)) - { - throw new InvalidOperationException($"Unknown state \"{nextState}\"."); - } - } - - // prepare and de-duplicate post-migrations, only keeping the 1st occurence - var temp = new HashSet(); - var postMigrationTypes = context.PostMigrations - .Where(x => !temp.Contains(x)) - .Select(x => { temp.Add(x); return x; }); - - // run post-migrations - foreach (var postMigrationType in postMigrationTypes) - { - _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); - var postMigration = _migrationBuilder.Build(postMigrationType, context); - postMigration.Run(); + throw new InvalidOperationException($"Unknown state \"{nextState}\"."); } } + + // prepare and de-duplicate post-migrations, only keeping the 1st occurence + var temp = new HashSet(); + IEnumerable postMigrationTypes = context.PostMigrations + .Where(x => !temp.Contains(x)) + .Select(x => + { + temp.Add(x); + return x; + }); + + // run post-migrations + foreach (Type postMigrationType in postMigrationTypes) + { + _logger.LogInformation($"PostMigration: {postMigrationType.FullName}."); + MigrationBase postMigration = _migrationBuilder.Build(postMigrationType, context); + postMigration.Run(); + } } - - _logger.LogInformation("Done (pending scope completion)."); - - // safety check - again, this should never happen as the plan has been validated, - // and this is just a paranoid safety test - var finalState = plan.FinalState; - if (nextState != finalState) - { - throw new InvalidOperationException($"Internal error, reached state {nextState} which is not final state {finalState}"); - } - - return nextState; } + + _logger.LogInformation("Done (pending scope completion)."); + + // safety check - again, this should never happen as the plan has been validated, + // and this is just a paranoid safety test + var finalState = plan.FinalState; + if (nextState != finalState) + { + throw new InvalidOperationException( + $"Internal error, reached state {nextState} which is not final state {finalState}"); + } + + return nextState; } } diff --git a/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs b/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs index 0cc2fbad25..9ce64977c0 100644 --- a/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/NoopMigration.cs @@ -1,14 +1,14 @@ -namespace Umbraco.Cms.Infrastructure.Migrations -{ - public class NoopMigration : MigrationBase - { - public NoopMigration(IMigrationContext context) : base(context) - { - } +namespace Umbraco.Cms.Infrastructure.Migrations; - protected override void Migrate() - { - // nop - } +public class NoopMigration : MigrationBase +{ + public NoopMigration(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // nop } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs index 2c2888c19d..75875b9384 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatedNotification.cs @@ -1,13 +1,11 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +internal class DatabaseSchemaCreatedNotification : StatefulNotification { - internal class DatabaseSchemaCreatedNotification : StatefulNotification - { - public DatabaseSchemaCreatedNotification(EventMessages eventMessages) => EventMessages = eventMessages; + public DatabaseSchemaCreatedNotification(EventMessages eventMessages) => EventMessages = eventMessages; - public EventMessages EventMessages { get; } - - } + public EventMessages EventMessages { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs index 1be96c9a9a..d4dfb35df7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/DatabaseSchemaCreatingNotification.cs @@ -1,12 +1,12 @@ using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +internal class DatabaseSchemaCreatingNotification : CancelableNotification { - internal class DatabaseSchemaCreatingNotification : CancelableNotification + public DatabaseSchemaCreatingNotification(EventMessages messages) + : base(messages) { - public DatabaseSchemaCreatingNotification(EventMessages messages) : base(messages) - { - } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs index 50ee5c3582..22c7e0710d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs +++ b/src/Umbraco.Infrastructure/Migrations/Notifications/MigrationPlansExecutedNotification.cs @@ -1,18 +1,14 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Notifications; -namespace Umbraco.Cms.Infrastructure.Migrations.Notifications +namespace Umbraco.Cms.Infrastructure.Migrations.Notifications; + +/// +/// Published when one or more migration plans have been successfully executed. +/// +public class MigrationPlansExecutedNotification : INotification { - /// - /// Published when one or more migration plans have been successfully executed. - /// - public class MigrationPlansExecutedNotification : INotification - { - public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) - => ExecutedPlans = executedPlans; + public MigrationPlansExecutedNotification(IReadOnlyList executedPlans) + => ExecutedPlans = executedPlans; - public IReadOnlyList ExecutedPlans { get; } - - - } + public IReadOnlyList ExecutedPlans { get; } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs index 2a61351d1f..c991d35f01 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/ClearCsrfCookies.cs @@ -1,22 +1,21 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Web; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Clears Csrf tokens. +/// +public class ClearCsrfCookies : MigrationBase { - /// - /// Clears Csrf tokens. - /// - public class ClearCsrfCookies : MigrationBase + private readonly ICookieManager _cookieManager; + + public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager) + : base(context) => _cookieManager = cookieManager; + + protected override void Migrate() { - private readonly ICookieManager _cookieManager; - - public ClearCsrfCookies(IMigrationContext context, ICookieManager cookieManager) - : base(context) => _cookieManager = cookieManager; - - protected override void Migrate() - { - _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); - _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); - } + _cookieManager.ExpireCookie(Constants.Web.AngularCookieName); + _cookieManager.ExpireCookie(Constants.Web.CsrfValidationCookieName); } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs index 3531959dbb..a75b01edaf 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/DeleteLogViewerQueryFile.cs @@ -1,34 +1,30 @@ -using System.IO; using Umbraco.Cms.Core.Hosting; + // using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +/// +/// Deletes the old file that saved log queries +/// +public class DeleteLogViewerQueryFile : MigrationBase { + private readonly IHostingEnvironment _hostingEnvironment; + /// - /// Deletes the old file that saved log queries + /// Initializes a new instance of the class. /// - public class DeleteLogViewerQueryFile : MigrationBase + public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) => + _hostingEnvironment = hostingEnvironment; + + /// + protected override void Migrate() { - private readonly IHostingEnvironment _hostingEnvironment; - - /// - /// Initializes a new instance of the class. - /// - public DeleteLogViewerQueryFile(IMigrationContext context, IHostingEnvironment hostingEnvironment) - : base(context) - { - _hostingEnvironment = hostingEnvironment; - } - - /// - protected override void Migrate() - { - // var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); - // - // if(File.Exists(logViewerQueryFile)) - // { - // File.Delete(logViewerQueryFile); - // } - } + // var logViewerQueryFile = MigrateLogViewerQueriesFromFileToDb.GetLogViewerQueryFile(_hostingEnvironment); + // + // if(File.Exists(logViewerQueryFile)) + // { + // File.Delete(logViewerQueryFile); + // } } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs index 94a2bc3aad..35e1fb7a30 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/IPublishedSnapshotRebuilder.cs @@ -1,18 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Rebuilds the published snapshot. +/// +/// +/// +/// This interface exists because the entire published snapshot lives in Umbraco.Web +/// but we may want to trigger rebuilds from Umbraco.Core. These two assemblies should +/// be refactored, really. +/// +/// +public interface IPublishedSnapshotRebuilder { /// - /// Rebuilds the published snapshot. + /// Rebuilds. /// - /// - /// This interface exists because the entire published snapshot lives in Umbraco.Web - /// but we may want to trigger rebuilds from Umbraco.Core. These two assemblies should - /// be refactored, really. - /// - public interface IPublishedSnapshotRebuilder - { - /// - /// Rebuilds. - /// - void Rebuild(); - } + void Rebuild(); } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs index b4afea633e..f70fd0ddb3 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/PublishedSnapshotRebuilder.cs @@ -1,31 +1,32 @@ -using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Implements in Umbraco.Web (rebuilding). +/// +public class PublishedSnapshotRebuilder : IPublishedSnapshotRebuilder { + private readonly DistributedCache _distributedCache; + private readonly IPublishedSnapshotService _publishedSnapshotService; + /// - /// Implements in Umbraco.Web (rebuilding). + /// Initializes a new instance of the class. /// - public class PublishedSnapshotRebuilder : IPublishedSnapshotRebuilder + public PublishedSnapshotRebuilder( + IPublishedSnapshotService publishedSnapshotService, + DistributedCache distributedCache) { - private readonly IPublishedSnapshotService _publishedSnapshotService; - private readonly DistributedCache _distributedCache; + _publishedSnapshotService = publishedSnapshotService; + _distributedCache = distributedCache; + } - /// - /// Initializes a new instance of the class. - /// - public PublishedSnapshotRebuilder(IPublishedSnapshotService publishedSnapshotService, DistributedCache distributedCache) - { - _publishedSnapshotService = publishedSnapshotService; - _distributedCache = distributedCache; - } - - /// - public void Rebuild() - { - _publishedSnapshotService.Rebuild(); - _distributedCache.RefreshAllPublishedSnapshot(); - } + /// + public void Rebuild() + { + _publishedSnapshotService.Rebuild(); + _distributedCache.RefreshAllPublishedSnapshot(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs b/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs index e2de75b7ec..f8f81acd7b 100644 --- a/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs +++ b/src/Umbraco.Infrastructure/Migrations/PostMigrations/RebuildPublishedSnapshot.cs @@ -1,20 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations +namespace Umbraco.Cms.Infrastructure.Migrations.PostMigrations; + +/// +/// Rebuilds the published snapshot. +/// +public class RebuildPublishedSnapshot : MigrationBase { + private readonly IPublishedSnapshotRebuilder _rebuilder; + /// - /// Rebuilds the published snapshot. + /// Initializes a new instance of the class. /// - public class RebuildPublishedSnapshot : MigrationBase - { - private readonly IPublishedSnapshotRebuilder _rebuilder; + public RebuildPublishedSnapshot(IMigrationContext context, IPublishedSnapshotRebuilder rebuilder) + : base(context) + => _rebuilder = rebuilder; - /// - /// Initializes a new instance of the class. - /// - public RebuildPublishedSnapshot(IMigrationContext context, IPublishedSnapshotRebuilder rebuilder) - : base(context) - => _rebuilder = rebuilder; - - /// - protected override void Migrate() => _rebuilder.Rebuild(); - } + /// + protected override void Migrate() => _rebuilder.Rebuild(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs index bacd875f3f..3bd00715d1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/CreateKeysAndIndexes.cs @@ -1,22 +1,25 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; + +public class CreateKeysAndIndexes : MigrationBase { - public class CreateKeysAndIndexes : MigrationBase + public CreateKeysAndIndexes(IMigrationContext context) + : base(context) { - public CreateKeysAndIndexes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // remove those that may already have keys + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.KeyValue).Do(); + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.PropertyData).Do(); + + // re-create *all* keys and indexes + foreach (Type x in DatabaseSchemaCreator._orderedTables) { - // remove those that may already have keys - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue).Do(); - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData).Do(); - - // re-create *all* keys and indexes - foreach (var x in DatabaseSchemaCreator.OrderedTables) - Create.KeysAndIndexes(x).Do(); + Create.KeysAndIndexes(x).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs index 14e4a5236a..23f3928147 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Common/DeleteKeysAndIndexes.cs @@ -1,75 +1,80 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; + +public class DeleteKeysAndIndexes : MigrationBase { - public class DeleteKeysAndIndexes : MigrationBase + public DeleteKeysAndIndexes(IMigrationContext context) + : base(context) { - public DeleteKeysAndIndexes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // all v7.14 tables + var tables = new[] { - // all v7.14 tables - var tables = new[] - { - "cmsContent", - "cmsContentType", - "cmsContentType2ContentType", - "cmsContentTypeAllowedContentType", - "cmsContentVersion", - "cmsContentXml", - "cmsDataType", - "cmsDataTypePreValues", - "cmsDictionary", - "cmsDocument", - "cmsDocumentType", - "cmsLanguageText", - "cmsMacro", - "cmsMacroProperty", - "cmsMedia", - "cmsMember", - "cmsMember2MemberGroup", - "cmsMemberType", - "cmsPreviewXml", - "cmsPropertyData", - "cmsPropertyType", - "cmsPropertyTypeGroup", - "cmsTagRelationship", - "cmsTags", - "cmsTask", - "cmsTaskType", - "cmsTemplate", - "umbracoAccess", - "umbracoAccessRule", - "umbracoAudit", - "umbracoCacheInstruction", - "umbracoConsent", - "umbracoDomains", - "umbracoExternalLogin", - "umbracoLanguage", - "umbracoLock", - "umbracoLog", - "umbracoMigration", - "umbracoNode", - "umbracoRedirectUrl", - "umbracoRelation", - "umbracoRelationType", - "umbracoServer", - "umbracoUser", - "umbracoUser2NodeNotify", - "umbracoUser2UserGroup", - "umbracoUserGroup", - "umbracoUserGroup2App", - "umbracoUserGroup2NodePermission", - "umbracoUserLogin", - "umbracoUserStartNode", - }; + "cmsContent", + "cmsContentType", + "cmsContentType2ContentType", + "cmsContentTypeAllowedContentType", + "cmsContentVersion", + "cmsContentXml", + "cmsDataType", + "cmsDataTypePreValues", + "cmsDictionary", + "cmsDocument", + "cmsDocumentType", + "cmsLanguageText", + "cmsMacro", + "cmsMacroProperty", + "cmsMedia", + "cmsMember", + "cmsMember2MemberGroup", + "cmsMemberType", + "cmsPreviewXml", + "cmsPropertyData", + "cmsPropertyType", + "cmsPropertyTypeGroup", + "cmsTagRelationship", + "cmsTags", + "cmsTask", + "cmsTaskType", + "cmsTemplate", + "umbracoAccess", + "umbracoAccessRule", + "umbracoAudit", + "umbracoCacheInstruction", + "umbracoConsent", + "umbracoDomains", + "umbracoExternalLogin", + "umbracoLanguage", + "umbracoLock", + "umbracoLog", + "umbracoMigration", + "umbracoNode", + "umbracoRedirectUrl", + "umbracoRelation", + "umbracoRelationType", + "umbracoServer", + "umbracoUser", + "umbracoUser2NodeNotify", + "umbracoUser2UserGroup", + "umbracoUserGroup", + "umbracoUserGroup2App", + "umbracoUserGroup2NodePermission", + "umbracoUserLogin", + "umbracoUserStartNode", + }; - // delete *all* keys and indexes - because of FKs - // on known v7 tables only - foreach (var table in tables) - Delete.KeysAndIndexes(table, false, true).Do(); - foreach (var table in tables) - Delete.KeysAndIndexes(table, true, false).Do(); + // delete *all* keys and indexes - because of FKs + // on known v7 tables only + foreach (var table in tables) + { + Delete.KeysAndIndexes(table, false).Do(); + } + + foreach (var table in tables) + { + Delete.KeysAndIndexes(table, true, false).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 37c2ab6c0e..dfe52f247d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; @@ -21,274 +20,273 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +/// +/// Represents the Umbraco CMS migration plan. +/// +/// +public class UmbracoPlan : MigrationPlan { + private const string InitPrefix = "{init-"; + private const string InitSuffix = "}"; + private readonly IUmbracoVersion _umbracoVersion; + /// - /// Represents the Umbraco CMS migration plan. + /// Initializes a new instance of the class. /// - /// - public class UmbracoPlan : MigrationPlan + /// The Umbraco version. + public UmbracoPlan(IUmbracoVersion umbracoVersion) + : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) { - private const string InitPrefix = "{init-"; - private const string InitSuffix = "}"; - private readonly IUmbracoVersion _umbracoVersion; + _umbracoVersion = umbracoVersion; + DefinePlan(); + } - /// - /// Initializes a new instance of the class. - /// - /// The Umbraco version. - public UmbracoPlan(IUmbracoVersion umbracoVersion) - : base(Constants.Conventions.Migrations.UmbracoUpgradePlanName) + /// + /// + /// The default initial state in plans is string.Empty. + /// + /// When upgrading from version 7, we want to use specific initial states + /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper + /// migrations. + /// + /// + /// This is also where we detect the current version, and reject invalid + /// upgrades (from a tool old version, or going back in time, etc). + /// + /// + public override string InitialState + { + get { - _umbracoVersion = umbracoVersion; - DefinePlan(); - } + SemVersion currentVersion = _umbracoVersion.SemanticVersion; - /// - /// - /// The default initial state in plans is string.Empty. - /// - /// When upgrading from version 7, we want to use specific initial states - /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper - /// migrations. - /// - /// - /// This is also where we detect the current version, and reject invalid - /// upgrades (from a tool old version, or going back in time, etc). - /// - /// - public override string InitialState - { - get - { - SemVersion currentVersion = _umbracoVersion.SemanticVersion; - - // only from 8.0.0 and above - var minVersion = new SemVersion(8); - if (currentVersion < minVersion) - { - throw new InvalidOperationException( - $"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." - + $" Please upgrade first to at least {minVersion}."); - } - - // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, - // and we don't want users to workaround my putting in version 7.14.0 them self. - if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) - { - return GetInitState(minVersion); - } - - // initial state is eg "{init-7.14.0}" - return GetInitState(currentVersion); - } - } - - /// - /// Gets the initial state corresponding to a version. - /// - /// The version. - /// - /// The initial state. - /// - private static string GetInitState(SemVersion version) => InitPrefix + version + InitSuffix; - - /// - /// Tries to extract a version from an initial state. - /// - /// The state. - /// The version. - /// - /// true when the state contains a version; otherwise, false.D - /// - private static bool TryGetInitStateVersion(string state, [MaybeNullWhen(false)] out string version) - { - if (state.StartsWith(InitPrefix) && state.EndsWith(InitSuffix)) - { - version = state.TrimStart(InitPrefix).TrimEnd(InitSuffix); - return true; - } - - version = null; - return false; - } - - /// - public override void ThrowOnUnknownInitialState(string state) - { - if (TryGetInitStateVersion(state, out var initVersion)) + // only from 8.0.0 and above + var minVersion = new SemVersion(8); + if (currentVersion < minVersion) { throw new InvalidOperationException( - $"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." - + $" Please verify which versions support migrating from {initVersion}."); + $"Version {currentVersion} cannot be migrated to {_umbracoVersion.SemanticVersion}." + + $" Please upgrade first to at least {minVersion}."); } - base.ThrowOnUnknownInitialState(state); - } + // Force versions between 7.14.*-7.15.* into into 7.14 initial state. Because there is no db-changes, + // and we don't want users to workaround my putting in version 7.14.0 them self. + if (minVersion <= currentVersion && currentVersion < new SemVersion(7, 16)) + { + return GetInitState(minVersion); + } - /// - /// Defines the plan. - /// - protected void DefinePlan() - { - // MODIFYING THE PLAN - // - // Please take great care when modifying the plan! - // - // * Creating a migration for version 8: - // Append the migration to the main chain, using a new guid, before the "//FINAL" comment - // - // If the new migration causes a merge conflict, because someone else also added another - // new migration, you NEED to fix the conflict by providing one default path, and paths - // out of the conflict states (see examples below). - // - // * Porting from version 7: - // Append the ported migration to the main chain, using a new guid (same as above). - // Create a new special chain from the {init-...} state to the main chain. - - - // plan starts at 7.14.0 (anything before 7.14.0 is not supported) - From(GetInitState(new SemVersion(7, 14))); - - // begin migrating from v7 - remove all keys and indexes - To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); - - To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); - To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); - To("{5CB66059-45F4-48BA-BCBD-C5035D79206B}"); - To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); - To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); - To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); - To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); - To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); - To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); - To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); - ToWithReplace("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", - "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 - To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); - - Merge() - .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") - .With() - .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") - .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); - - To("{1350617A-4930-4D61-852F-E3AA9E692173}"); - To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); - To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); - To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); - To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); - To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); - To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); - To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); - To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); - To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); - To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); - To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); - To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); - To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); - To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); - To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); - To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); - - Merge() - .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") - .With() - .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") - .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); - - To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); - To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); - - // finish migrating from v7 - recreate all keys and indexes - To("{3F9764F5-73D0-4D45-8804-1240A66E43A2}"); - - To("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}"); - To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); - // release-8.0.0 - - // to 8.0.1 - To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); - // release-8.0.1 - - // to 8.1.0 - To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); - To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); - To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); - - // to 8.6.0 - To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); - To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); - To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); - To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); - To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); - - // to 8.7.0 - To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); - - // to 8.9.0 - To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); - - // to 8.10.0 - To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); - - // NOTE: we need to do a merge migration here because as of 'now', - // v9-beta* is already out and 8.15 isn't out yet - // so we need to ensure that migrations from 8.15 are included in the next - // v9*. - - // to 8.15.0 - To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); - To("{4695D0C9-0729-4976-985B-048D503665D8}"); - To("{5C424554-A32D-4852-8ED1-A13508187901}"); - - // to 8.17.0 - To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}"); - - // Hack to support migration from 8.18 - To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}"); - - // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. - // - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} - // - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} - // - 9.0.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} - To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); - To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); - To("{5AAE6276-80DB-4ACF-B845-199BC6C37538}"); - - // to 9.0.0 RC1 - To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); - To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}"); - To("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}"); - To("{1303BDCF-2295-4645-9526-2F32E8B35ABD}"); - To("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); - To( - "{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. - - // TO 9.0.0-rc4 - To( - "5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards - - // TO 9.1.0 - To("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}"); - - // TO 9.2.0 - To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); - To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); - - - - // TO 9.3.0 - To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); - To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); - To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); - - // TO 9.4.0 - To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); - To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); - - // TO 10.0.0 - To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); + // initial state is eg "{init-7.14.0}" + return GetInitState(currentVersion); } } + + /// + public override void ThrowOnUnknownInitialState(string state) + { + if (TryGetInitStateVersion(state, out var initVersion)) + { + throw new InvalidOperationException( + $"Version {_umbracoVersion.SemanticVersion} does not support migrating from {initVersion}." + + $" Please verify which versions support migrating from {initVersion}."); + } + + base.ThrowOnUnknownInitialState(state); + } + + /// + /// Gets the initial state corresponding to a version. + /// + /// The version. + /// + /// The initial state. + /// + private static string GetInitState(SemVersion version) => InitPrefix + version + InitSuffix; + + /// + /// Tries to extract a version from an initial state. + /// + /// The state. + /// The version. + /// + /// true when the state contains a version; otherwise, false.D + /// + private static bool TryGetInitStateVersion(string state, [MaybeNullWhen(false)] out string version) + { + if (state.StartsWith(InitPrefix) && state.EndsWith(InitSuffix)) + { + version = state.TrimStart(InitPrefix).TrimEnd(InitSuffix); + return true; + } + + version = null; + return false; + } + + /// + /// Defines the plan. + /// + protected void DefinePlan() + { + // MODIFYING THE PLAN + // + // Please take great care when modifying the plan! + // + // * Creating a migration for version 8: + // Append the migration to the main chain, using a new guid, before the "//FINAL" comment + // + // If the new migration causes a merge conflict, because someone else also added another + // new migration, you NEED to fix the conflict by providing one default path, and paths + // out of the conflict states (see examples below). + // + // * Porting from version 7: + // Append the ported migration to the main chain, using a new guid (same as above). + // Create a new special chain from the {init-...} state to the main chain. + + // plan starts at 7.14.0 (anything before 7.14.0 is not supported) + From(GetInitState(new SemVersion(7, 14))); + + // begin migrating from v7 - remove all keys and indexes + To("{B36B9ABD-374E-465B-9C5F-26AB0D39326F}"); + + To("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); + To("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); + To("{5CB66059-45F4-48BA-BCBD-C5035D79206B}"); + To("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); + To("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); + To("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); + To("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}"); + To("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); + To("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); + To("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); + ToWithReplace( + "{941B2ABA-2D06-4E04-81F5-74224F1DB037}", + "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); // kill AddVariationTable1 + To("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); + + Merge() + .To("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}") + .With() + .To("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}") + .As("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + + To("{1350617A-4930-4D61-852F-E3AA9E692173}"); + To("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}"); + To("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); + To("{290C18EE-B3DE-4769-84F1-1F467F3F76DA}"); + To("{6A2C7C1B-A9DB-4EA9-B6AB-78E7D5B722A7}"); + To("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}"); + To("{23275462-446E-44C7-8C2C-3B8C1127B07D}"); + To("{6B251841-3069-4AD5-8AE9-861F9523E8DA}"); + To("{EE429F1B-9B26-43CA-89F8-A86017C809A3}"); + To("{08919C4B-B431-449C-90EC-2B8445B5C6B1}"); + To("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}"); + To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); + To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); + To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); + To("{ED28B66A-E248-4D94-8CDB-9BDF574023F0}"); + To("{38C809D5-6C34-426B-9BEA-EFD39162595C}"); + To("{6017F044-8E70-4E10-B2A3-336949692ADD}"); + + Merge() + .To("{CDBEDEE4-9496-4903-9CF2-4104E00FF960}") + .With() + .To("{940FD19A-00A8-4D5C-B8FF-939143585726}") + .As("{0576E786-5C30-4000-B969-302B61E90CA3}"); + + To("{48AD6CCD-C7A4-4305-A8AB-38728AD23FC5}"); + To("{DF470D86-E5CA-42AC-9780-9D28070E25F9}"); + + // finish migrating from v7 - recreate all keys and indexes + To("{3F9764F5-73D0-4D45-8804-1240A66E43A2}"); + + To("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}"); + To("{78BAF571-90D0-4D28-8175-EF96316DA789}"); + + // release-8.0.0 + + // to 8.0.1 + To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}"); + + // release-8.0.1 + + // to 8.1.0 + To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); + To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); + To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); + + // to 8.6.0 + To("{4759A294-9860-46BC-99F9-B4C975CAE580}"); + To("{0BC866BC-0665-487A-9913-0290BD0169AD}"); + To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); + To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}"); + To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); + + // to 8.7.0 + To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); + + // to 8.9.0 + To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}"); + + // to 8.10.0 + To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}"); + + // NOTE: we need to do a merge migration here because as of 'now', + // v9-beta* is already out and 8.15 isn't out yet + // so we need to ensure that migrations from 8.15 are included in the next + // v9*. + + // to 8.15.0 + To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}"); + To("{4695D0C9-0729-4976-985B-048D503665D8}"); + To("{5C424554-A32D-4852-8ED1-A13508187901}"); + + // to 8.17.0 + To("{153865E9-7332-4C2A-9F9D-F20AEE078EC7}"); + + // Hack to support migration from 8.18 + To("{03482BB0-CF13-475C-845E-ECB8319DBE3C}"); + + // This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step. + // - 8.15.0 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8} + // - 8.15.0 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901} + // - 9.0.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12} + To("{622E5172-42E1-4662-AD80-9504AF5A4E53}"); + To("{10F7BB61-C550-426B-830B-7F954F689CDF}"); + To("{5AAE6276-80DB-4ACF-B845-199BC6C37538}"); + + // to 9.0.0 RC1 + To("{22D801BA-A1FF-4539-BFCC-2139B55594F8}"); + To("{50A43237-A6F4-49E2-A7A6-5DAD65C84669}"); + To("{3D8DADEF-0FDA-4377-A5F0-B52C2110E8F2}"); + To("{1303BDCF-2295-4645-9526-2F32E8B35ABD}"); + To("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}"); + To( + "{A2686B49-A082-4B22-97FD-AAB154D46A57}"); // Re-run this migration to make sure it has executed to account for migrations going out of sync between versions. + + // TO 9.0.0-rc4 + To( + "5E02F241-5253-403D-B5D3-7DB00157E20F"); // Jaddie: This GUID is missing the { }, although this likely can't be changed now as it will break installs going forwards + + // TO 9.1.0 + To("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}"); + + // TO 9.2.0 + To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); + To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); + + // TO 9.3.0 + To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); + To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); + To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); + + // TO 9.4.0 + To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); + To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); + + // TO 10.0.0 + To("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}"); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index 15225b868a..8c2c1aeaa7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -1,79 +1,85 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade; + +/// +/// Used to run a +/// +public class Upgrader { /// - /// Used to run a + /// Initializes a new instance of the class. /// - public class Upgrader + public Upgrader(MigrationPlan plan) => Plan = plan; + + /// + /// Gets the name of the migration plan. + /// + public string Name => Plan.Name; + + /// + /// Gets the migration plan. + /// + public MigrationPlan Plan { get; } + + /// + /// Gets the key for the state value. + /// + public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; + + /// + /// Executes. + /// + /// A scope provider. + /// A key-value service. + public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, ICoreScopeProvider scopeProvider, + IKeyValueService keyValueService) { - /// - /// Initializes a new instance of the class. - /// - public Upgrader(MigrationPlan plan) => Plan = plan; - - /// - /// Gets the name of the migration plan. - /// - public string Name => Plan.Name; - - /// - /// Gets the migration plan. - /// - public MigrationPlan Plan { get; } - - /// - /// Gets the key for the state value. - /// - public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; - - /// - /// Executes. - /// - /// A scope provider. - /// A key-value service. - public ExecutedMigrationPlan Execute(IMigrationPlanExecutor migrationPlanExecutor, ICoreScopeProvider scopeProvider, IKeyValueService keyValueService) + if (scopeProvider == null) { - if (scopeProvider == null) throw new ArgumentNullException(nameof(scopeProvider)); - if (keyValueService == null) throw new ArgumentNullException(nameof(keyValueService)); + throw new ArgumentNullException(nameof(scopeProvider)); + } - using (ICoreScope scope = scopeProvider.CreateCoreScope()) - { - // read current state - var currentState = keyValueService.GetValue(StateValueKey); - var forceState = false; + if (keyValueService == null) + { + throw new ArgumentNullException(nameof(keyValueService)); + } - if (currentState == null || Plan.IgnoreCurrentState) - { - currentState = Plan.InitialState; - forceState = true; - } + using (ICoreScope scope = scopeProvider.CreateCoreScope()) + { + // read current state + var currentState = keyValueService.GetValue(StateValueKey); + var forceState = false; - // execute plan - var state = migrationPlanExecutor.Execute(Plan, currentState); - if (string.IsNullOrWhiteSpace(state)) - { - throw new InvalidOperationException("Plan execution returned an invalid null or empty state."); - } - - // save new state - if (forceState) - { - keyValueService.SetValue(StateValueKey, state); - } - else if (currentState != state) - { - keyValueService.SetValue(StateValueKey, currentState, state); - } - - scope.Complete(); - - return new ExecutedMigrationPlan(Plan, currentState, state); + if (currentState == null || Plan.IgnoreCurrentState) + { + currentState = Plan.InitialState; + forceState = true; } + + // execute plan + var state = migrationPlanExecutor.Execute(Plan, currentState); + if (string.IsNullOrWhiteSpace(state)) + { + throw new InvalidOperationException("Plan execution returned an invalid null or empty state."); + } + + // save new state + if (forceState) + { + keyValueService.SetValue(StateValueKey, state); + } + else if (currentState != state) + { + keyValueService.SetValue(StateValueKey, currentState, state); + } + + scope.Complete(); + + return new ExecutedMigrationPlan(Plan, currentState, state); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs index 5bc58c9b25..1a3cda316d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using System.Text; using NPoco; using Umbraco.Cms.Core; @@ -28,7 +26,9 @@ public class AddMemberPropertiesAsColumns : MigrationBase AddColumnIfNotExists(columns, "lastPasswordChangeDate"); Sql newestContentVersionQuery = Database.SqlContext.Sql() - .Select($"MAX({GetQuotedSelector("cv", "id")}) as {SqlSyntax.GetQuotedColumnName("id")}", GetQuotedSelector("cv", "nodeId")) + .Select( + $"MAX({GetQuotedSelector("cv", "id")}) as {SqlSyntax.GetQuotedColumnName("id")}", + GetQuotedSelector("cv", "nodeId")) .From("cv") .GroupBy(GetQuotedSelector("cv", "nodeId")); @@ -62,15 +62,21 @@ public class AddMemberPropertiesAsColumns : MigrationBase .From("pt") .Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberLastPasswordChangeDate'"); - StringBuilder queryBuilder = new StringBuilder(); + var queryBuilder = new StringBuilder(); queryBuilder.AppendLine($"UPDATE {Constants.DatabaseSchema.Tables.Member}"); queryBuilder.AppendLine("SET"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.FailedPasswordAttempts)} = {GetQuotedSelector("umbracoPropertyData", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsApproved)} = {GetQuotedSelector("pdmp", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsLockedOut)} = {GetQuotedSelector("pdlo", "intValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLockoutDate)} = {GetQuotedSelector("pdlout", "dateValue")},"); - queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLoginDate)} = {GetQuotedSelector("pdlin", "dateValue")},"); - queryBuilder.Append($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastPasswordChangeDate)} = {GetQuotedSelector("pdlpc", "dateValue")}"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.FailedPasswordAttempts)} = {GetQuotedSelector("umbracoPropertyData", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsApproved)} = {GetQuotedSelector("pdmp", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.IsLockedOut)} = {GetQuotedSelector("pdlo", "intValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLockoutDate)} = {GetQuotedSelector("pdlout", "dateValue")},"); + queryBuilder.AppendLine( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastLoginDate)} = {GetQuotedSelector("pdlin", "dateValue")},"); + queryBuilder.Append( + $"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate(x => x.LastPasswordChangeDate)} = {GetQuotedSelector("pdlpc", "dateValue")}"); Sql updateMemberColumnsQuery = Database.SqlContext.Sql(queryBuilder.ToString()) .From() @@ -87,37 +93,43 @@ public class AddMemberPropertiesAsColumns : MigrationBase .LeftJoin() .On((left, right) => left.DataTypeId == right.NodeId) .LeftJoin() - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id) + .On((left, middle, right) => + left.PropertyTypeId == middle.Id && left.VersionId == right.Id) .LeftJoin(memberApprovedQuery, "memberApprovedType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtmp") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtmp") .LeftJoin("pdmp") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdmp") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdmp") .LeftJoin(memberLockedOutQuery, "memberLockedOutType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlo") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlo") .LeftJoin("pdlo") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlo") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlo") .LeftJoin(memberLastLockoutDateQuery, "lastLockOutDateType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlout") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlout") .LeftJoin("pdlout") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlout") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlout") .LeftJoin(memberLastLoginDateQuery, "lastLoginDateType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlin") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlin") .LeftJoin("pdlin") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlin") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlin") .LeftJoin(memberLastPasswordChangeDateQuery, "lastPasswordChangeType") .On((left, right) => left.ContentTypeId == right.ContentTypeId) .LeftJoin("dtlpc") .On((left, right) => left.DataTypeId == right.NodeId, null, "dtlpc") .LeftJoin("pdlpc") - .On((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlpc") + .On( + (left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlpc") .Where(x => x.NodeObjectType == Constants.ObjectTypes.Member); Database.Execute(updateMemberColumnsQuery); @@ -131,7 +143,7 @@ public class AddMemberPropertiesAsColumns : MigrationBase "umbracoMemberLockedOut", "umbracoMemberLastLockoutDate", "umbracoMemberLastLogin", - "umbracoMemberLastPasswordChangeDate" + "umbracoMemberLastPasswordChangeDate", }; Sql idQuery = Database.SqlContext.Sql().Select(x => x.Id) @@ -157,8 +169,7 @@ public class AddMemberPropertiesAsColumns : MigrationBase private object[] GetSubQueryColumns() => new object[] { - SqlSyntax.GetQuotedColumnName("contentTypeId"), - SqlSyntax.GetQuotedColumnName("dataTypeId"), + SqlSyntax.GetQuotedColumnName("contentTypeId"), SqlSyntax.GetQuotedColumnName("dataTypeId"), SqlSyntax.GetQuotedColumnName("id"), }; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs index b53fd867b2..a216abf045 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentNuTable.cs @@ -1,20 +1,23 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +internal class AddContentNuTable : MigrationBase { - class AddContentNuTable : MigrationBase + public AddContentNuTable(IMigrationContext context) + : base(context) { - public AddContentNuTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains("cmsContentNu")) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains("cmsContentNu")) return; - - Create.Table(true).Do(); + return; } + + Create.Table(true).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs index 28f6e8e6de..36f4dcb5e0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddContentTypeIsElementColumn.cs @@ -1,15 +1,13 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddContentTypeIsElementColumn : MigrationBase { - public class AddContentTypeIsElementColumn : MigrationBase + public AddContentTypeIsElementColumn(IMigrationContext context) + : base(context) { - public AddContentTypeIsElementColumn(IMigrationContext context) : base(context) - { } - - protected override void Migrate() - { - AddColumn("isElement"); - } } + + protected override void Migrate() => AddColumn("isElement"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs index f8332fb0e2..96937d3991 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLockObjects.cs @@ -1,43 +1,44 @@ -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddLockObjects : MigrationBase { - public class AddLockObjects : MigrationBase + public AddLockObjects(IMigrationContext context) + : base(context) { - public AddLockObjects(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // some may already exist, just ensure everything we need is here - EnsureLockObject(Cms.Core.Constants.Locks.Servers, "Servers"); - EnsureLockObject(Cms.Core.Constants.Locks.ContentTypes, "ContentTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.ContentTree, "ContentTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MediaTree, "MediaTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MemberTree, "MemberTree"); - EnsureLockObject(Cms.Core.Constants.Locks.MediaTypes, "MediaTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.MemberTypes, "MemberTypes"); - EnsureLockObject(Cms.Core.Constants.Locks.Domains, "Domains"); - } - - private void EnsureLockObject(int id, string name) - { - EnsureLockObject(Database, id, name); - } - - internal static void EnsureLockObject(IUmbracoDatabase db, int id, string name) - { - // not if it already exists - var exists = db.Exists(id); - if (exists) return; - - // be safe: delete old umbracoNode lock objects if any - db.Execute($"DELETE FROM umbracoNode WHERE id={id};"); - - // then create umbracoLock object - db.Execute($"INSERT umbracoLock (id, name, value) VALUES ({id}, '{name}', 1);"); - } } + + internal static void EnsureLockObject(IUmbracoDatabase db, int id, string name) + { + // not if it already exists + var exists = db.Exists(id); + if (exists) + { + return; + } + + // be safe: delete old umbracoNode lock objects if any + db.Execute($"DELETE FROM umbracoNode WHERE id={id};"); + + // then create umbracoLock object + db.Execute($"INSERT umbracoLock (id, name, value) VALUES ({id}, '{name}', 1);"); + } + + protected override void Migrate() + { + // some may already exist, just ensure everything we need is here + EnsureLockObject(Constants.Locks.Servers, "Servers"); + EnsureLockObject(Constants.Locks.ContentTypes, "ContentTypes"); + EnsureLockObject(Constants.Locks.ContentTree, "ContentTree"); + EnsureLockObject(Constants.Locks.MediaTree, "MediaTree"); + EnsureLockObject(Constants.Locks.MemberTree, "MemberTree"); + EnsureLockObject(Constants.Locks.MediaTypes, "MediaTypes"); + EnsureLockObject(Constants.Locks.MemberTypes, "MemberTypes"); + EnsureLockObject(Constants.Locks.Domains, "Domains"); + } + + private void EnsureLockObject(int id, string name) => EnsureLockObject(Database, id, name); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs index 4ef9d4ff14..8546566999 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddLogTableColumns.cs @@ -1,20 +1,19 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddLogTableColumns : MigrationBase { - public class AddLogTableColumns : MigrationBase + public AddLogTableColumns(IMigrationContext context) + : base(context) { - public AddLogTableColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "entityType"); - AddColumnIfNotExists(columns, "parameters"); - } + AddColumnIfNotExists(columns, "entityType"); + AddColumnIfNotExists(columns, "parameters"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs index fc708b1f4b..e147d185fe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddPackagesSectionAccess.cs @@ -1,19 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class AddPackagesSectionAccess : MigrationBase - { - public AddPackagesSectionAccess(IMigrationContext context) - : base(context) - { } +using Umbraco.Cms.Core; - protected override void Migrate() - { - // Any user group which had access to the Developer section should have access to Packages - Database.Execute($@" - insert into {Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App} - select userGroupId, '{Cms.Core.Constants.Applications.Packages}' - from {Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App} - where app='developer'"); - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddPackagesSectionAccess : MigrationBase +{ + public AddPackagesSectionAccess(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() => + + // Any user group which had access to the Developer section should have access to Packages + Database.Execute($@" + insert into {Constants.DatabaseSchema.Tables.UserGroup2App} + select userGroupId, '{Constants.Applications.Packages}' + from {Constants.DatabaseSchema.Tables.UserGroup2App} + where app='developer'"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs index 69431867b1..6e5e462d8d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs @@ -1,129 +1,166 @@ -using System; using System.Globalization; -using System.Linq; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddTypedLabels : MigrationBase { - public class AddTypedLabels : MigrationBase + public AddTypedLabels(IMigrationContext context) + : base(context) { - public AddTypedLabels(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // insert other label datatypes - - void InsertNodeDto(int id, int sortOrder, string uniqueId, string text) - { - var nodeDto = new NodeDto - { - NodeId = id, - Trashed = false, - ParentId = -1, - UserId = -1, - Level = 1, - Path = "-1,-" + id, - SortOrder = sortOrder, - UniqueId = new Guid(uniqueId), - Text = text, - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - CreateDate = DateTime.Now - }; - - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); - } - - if (SqlSyntax.SupportsIdentityInsert()) - Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} ON ")); - - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", "Label (datetime)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); - InsertNodeDto(Cms.Core.Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); - - if (SqlSyntax.SupportsIdentityInsert()) - Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} OFF ")); - - void InsertDataTypeDto(int id, string dbType, string? configuration = null) - { - var dataTypeDto = new DataTypeDto - { - NodeId = id, - EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.Label, - DbType = dbType - }; - - if (configuration != null) - dataTypeDto.Configuration = configuration; - - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); - } - - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelInt, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelBigint, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDateTime, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelDecimal, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); - InsertDataTypeDto(Cms.Core.Constants.DataTypes.LabelTime, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); - - // flip known property types - - var labelPropertyTypes = Database.Fetch(Sql() - .Select(x => x.Id, x => x.Alias) - .From() - .Where(x => x.DataTypeId == Cms.Core.Constants.DataTypes.LabelString)); - - var intPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Width, Cms.Core.Constants.Conventions.Media.Height, Cms.Core.Constants.Conventions.Member.FailedPasswordAttempts }; - var bigintPropertyAliases = new[] { Cms.Core.Constants.Conventions.Media.Bytes }; - var dtPropertyAliases = new[] { Cms.Core.Constants.Conventions.Member.LastLockoutDate, Cms.Core.Constants.Conventions.Member.LastLoginDate, Cms.Core.Constants.Conventions.Member.LastPasswordChangeDate }; - - var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); - - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelBigint)).WhereIn(x => x.Id, bigintPropertyTypes)); - Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Cms.Core.Constants.DataTypes.LabelDateTime)).WhereIn(x => x.Id, dtPropertyTypes)); - - // update values for known property types - // depending on the size of the site, that *may* take time - // but we want to parse in C# not in the database - var values = Database.Fetch(Sql() - .Select(x => x.Id, x => x.VarcharValue) - .From() - .WhereIn(x => x.PropertyTypeId, intPropertyTypes)); - foreach (var value in values) - Database.Execute(Sql() - .Update(u => u - .Set(x => x.IntegerValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (int?)null : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) - .Set(x => x.TextValue, null) - .Set(x => x.VarcharValue, null)) - .Where(x => x.Id == value.Id)); - - values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue).From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); - foreach (var value in values) - Database.Execute(Sql() - .Update(u => u - .Set(x => x.DateValue, string.IsNullOrWhiteSpace(value.VarcharValue) ? (DateTime?)null : DateTime.Parse(value.VarcharValue, CultureInfo.InvariantCulture, DateTimeStyles.None)) - .Set(x => x.TextValue, null) - .Set(x => x.VarcharValue, null)) - .Where(x => x.Id == value.Id)); - - // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it - } - - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class PropertyDataValue - { - public int Id { get; set; } - public string? VarcharValue { get;set; } - } - // ReSharper restore UnusedAutoPropertyAccessor.Local } + + protected override void Migrate() + { + // insert other label datatypes + void InsertNodeDto(int id, int sortOrder, string uniqueId, string text) + { + var nodeDto = new NodeDto + { + NodeId = id, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1,-" + id, + SortOrder = sortOrder, + UniqueId = new Guid(uniqueId), + Text = text, + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }; + + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + } + + if (SqlSyntax.SupportsIdentityInsert()) + { + Database.Execute(new Sql( + $"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} ON ")); + } + + InsertNodeDto(Constants.DataTypes.LabelInt, 36, "8e7f995c-bd81-4627-9932-c40e568ec788", "Label (integer)"); + InsertNodeDto(Constants.DataTypes.LabelBigint, 36, "930861bf-e262-4ead-a704-f99453565708", "Label (bigint)"); + InsertNodeDto(Constants.DataTypes.LabelDateTime, 37, "0e9794eb-f9b5-4f20-a788-93acd233a7e4", + "Label (datetime)"); + InsertNodeDto(Constants.DataTypes.LabelTime, 38, "a97cec69-9b71-4c30-8b12-ec398860d7e8", "Label (time)"); + InsertNodeDto(Constants.DataTypes.LabelDecimal, 39, "8f1ef1e1-9de4-40d3-a072-6673f631ca64", "Label (decimal)"); + + if (SqlSyntax.SupportsIdentityInsert()) + { + Database.Execute(new Sql( + $"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} OFF ")); + } + + void InsertDataTypeDto(int id, string dbType, string? configuration = null) + { + var dataTypeDto = new DataTypeDto + { + NodeId = id, + EditorAlias = Constants.PropertyEditors.Aliases.Label, + DbType = dbType, + }; + + if (configuration != null) + { + dataTypeDto.Configuration = configuration; + } + + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + + InsertDataTypeDto(Constants.DataTypes.LabelInt, "Integer", "{\"umbracoDataValueType\":\"INT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBigint, "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDateTime, "Date", "{\"umbracoDataValueType\":\"DATETIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelDecimal, "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelTime, "Date", "{\"umbracoDataValueType\":\"TIME\"}"); + + // flip known property types + List? labelPropertyTypes = Database.Fetch(Sql() + .Select(x => x.Id, x => x.Alias) + .From() + .Where(x => x.DataTypeId == Constants.DataTypes.LabelString)); + + var intPropertyAliases = new[] + { + Constants.Conventions.Media.Width, Constants.Conventions.Media.Height, + Constants.Conventions.Member.FailedPasswordAttempts, + }; + var bigintPropertyAliases = new[] { Constants.Conventions.Media.Bytes }; + var dtPropertyAliases = new[] + { + Constants.Conventions.Member.LastLockoutDate, Constants.Conventions.Member.LastLoginDate, + Constants.Conventions.Member.LastPasswordChangeDate, + }; + + var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id) + .ToArray(); + var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)) + .Select(pt => pt.Id).ToArray(); + var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id) + .ToArray(); + + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)) + .WhereIn(x => x.Id, intPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)) + .WhereIn(x => x.Id, intPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelBigint)) + .WhereIn(x => x.Id, bigintPropertyTypes)); + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelDateTime)) + .WhereIn(x => x.Id, dtPropertyTypes)); + + // update values for known property types + // depending on the size of the site, that *may* take time + // but we want to parse in C# not in the database + List? values = Database.Fetch(Sql() + .Select(x => x.Id, x => x.VarcharValue) + .From() + .WhereIn(x => x.PropertyTypeId, intPropertyTypes)); + foreach (PropertyDataValue? value in values) + { + Database.Execute(Sql() + .Update(u => u + .Set( + x => x.IntegerValue, + string.IsNullOrWhiteSpace(value.VarcharValue) + ? null + : int.Parse(value.VarcharValue, NumberStyles.Any, CultureInfo.InvariantCulture)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) + .Where(x => x.Id == value.Id)); + } + + values = Database.Fetch(Sql().Select(x => x.Id, x => x.VarcharValue) + .From().WhereIn(x => x.PropertyTypeId, dtPropertyTypes)); + foreach (PropertyDataValue? value in values) + { + Database.Execute(Sql() + .Update(u => u + .Set(x => x.DateValue, + string.IsNullOrWhiteSpace(value.VarcharValue) + ? null + : DateTime.Parse(value.VarcharValue, + CultureInfo.InvariantCulture, + DateTimeStyles.None)) + .Set(x => x.TextValue, null) + .Set(x => x.VarcharValue, null)) + .Where(x => x.Id == value.Id)); + } + + // anything that's custom... ppl will have to figure it out manually, there isn't much we can do about it + } + + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class PropertyDataValue + { + public int Id { get; set; } + + public string? VarcharValue { get; set; } + } + + // ReSharper restore UnusedAutoPropertyAccessor.Local } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs index 465b17d7fc..118c7f8bb2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables1A.cs @@ -1,45 +1,53 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class AddVariationTables1A : MigrationBase - { - public AddVariationTables1A(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; +public class AddVariationTables1A : MigrationBase +{ + public AddVariationTables1A(IMigrationContext context) + : base(context) + { + } + + // note - original AddVariationTables1 just did + // Create.Table().Do(); + // + // this is taking care of ppl left in this state + protected override void Migrate() + { // note - original AddVariationTables1 just did // Create.Table().Do(); // - // this is taking care of ppl left in this state + // it's been deprecated, not part of the main upgrade path, + // but we need to take care of ppl caught into the state - protected override void Migrate() + // was not used + Delete.Column("available").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + // was not used + Delete.Column("availableDate").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + + // special trick to add the column without constraints and return the sql to add them later + AddColumn("date", out IEnumerable sqls); + + // now we need to update the new column with some values because this column doesn't allow NULL values + Update.Table(ContentVersionCultureVariationDto.TableName).Set(new { date = DateTime.Now }).AllRows().Do(); + + // now apply constraints (NOT NULL) to new table + foreach (var sql in sqls) { - // note - original AddVariationTables1 just did - // Create.Table().Do(); - // - // it's been deprecated, not part of the main upgrade path, - // but we need to take care of ppl caught into the state - - // was not used - Delete.Column("available").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - // was not used - Delete.Column("availableDate").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - //special trick to add the column without constraints and return the sql to add them later - AddColumn("date", out var sqls); - //now we need to update the new column with some values because this column doesn't allow NULL values - Update.Table(ContentVersionCultureVariationDto.TableName).Set(new {date = DateTime.Now}).AllRows().Do(); - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - // name, languageId are now non-nullable - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "name"); - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "languageId"); - - Create.Table().Do(); + Execute.Sql(sql).Do(); } + + // name, languageId are now non-nullable + AlterColumn( + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, + "name"); + AlterColumn( + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, + "languageId"); + + Create.Table().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs index 263dffd2b9..76d20b4667 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/AddVariationTables2.cs @@ -1,17 +1,17 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class AddVariationTables2 : MigrationBase { - public class AddVariationTables2 : MigrationBase + public AddVariationTables2(IMigrationContext context) + : base(context) { - public AddVariationTables2(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Create.Table(true).Do(); - Create.Table(true).Do(); - } + protected override void Migrate() + { + Create.Table(true).Do(); + Create.Table(true).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs index 6171c3df13..edfeb204f8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -1,66 +1,62 @@ -using System; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class ContentVariationMigration : MigrationBase { - public class ContentVariationMigration : MigrationBase + public ContentVariationMigration(IMigrationContext context) + : base(context) { - public ContentVariationMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + static byte GetNewValue(byte oldValue) { - byte GetNewValue(byte oldValue) + switch (oldValue) { - switch (oldValue) - { - case 0: // Unknown - case 1: // InvariantNeutral - return 0; // Unknown - case 2: // CultureNeutral - case 3: // CultureNeutral | InvariantNeutral - return 1; // Culture - case 4: // InvariantSegment - case 5: // InvariantSegment | InvariantNeutral - return 2; // Segment - case 6: // InvariantSegment | CultureNeutral - case 7: // InvariantSegment | CultureNeutral | InvariantNeutral - case 8: // CultureSegment - case 9: // CultureSegment | InvariantNeutral - case 10: // CultureSegment | CultureNeutral - case 11: // CultureSegment | CultureNeutral | InvariantNeutral - case 12: // etc - case 13: - case 14: - case 15: - return 3; // Culture | Segment - default: - throw new NotSupportedException($"Invalid value {oldValue}."); - } - } - - var propertyTypes = Database.Fetch(Sql().Select().From()); - foreach (var dto in propertyTypes) - { - dto.Variations = GetNewValue(dto.Variations); - Database.Update(dto); - } - - var contentTypes = Database.Fetch(Sql().Select().From()); - foreach (var dto in contentTypes) - { - dto.Variations = GetNewValue(dto.Variations); - Database.Update(dto); + case 0: // Unknown + case 1: // InvariantNeutral + return 0; // Unknown + case 2: // CultureNeutral + case 3: // CultureNeutral | InvariantNeutral + return 1; // Culture + case 4: // InvariantSegment + case 5: // InvariantSegment | InvariantNeutral + return 2; // Segment + case 6: // InvariantSegment | CultureNeutral + case 7: // InvariantSegment | CultureNeutral | InvariantNeutral + case 8: // CultureSegment + case 9: // CultureSegment | InvariantNeutral + case 10: // CultureSegment | CultureNeutral + case 11: // CultureSegment | CultureNeutral | InvariantNeutral + case 12: // etc + case 13: + case 14: + case 15: + return 3; // Culture | Segment + default: + throw new NotSupportedException($"Invalid value {oldValue}."); } } - // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration - - - - + List? propertyTypes = + Database.Fetch(Sql().Select().From()); + foreach (PropertyTypeDto80? dto in propertyTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } + List? contentTypes = + Database.Fetch(Sql().Select().From()); + foreach (ContentTypeDto80? dto in contentTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } } + + // we *need* to use these private DTOs here, which does *not* have extra properties, which would kill the migration } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index a6ff99f2c7..50ac54436a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -1,146 +1,162 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class ConvertRelatedLinksToMultiUrlPicker : MigrationBase { - public class ConvertRelatedLinksToMultiUrlPicker : MigrationBase + public ConvertRelatedLinksToMultiUrlPicker(IMigrationContext context) + : base(context) { - public ConvertRelatedLinksToMultiUrlPicker(IMigrationContext context) : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + Sql sqlDataTypes = Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks + || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); + + List? dataTypes = Database.Fetch(sqlDataTypes); + var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); + + if (dataTypeIds.Count == 0) { - var sqlDataTypes = Sql() - .Select() - .From() - .Where(x => x.EditorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); + return; + } - var dataTypes = Database.Fetch(sqlDataTypes); - var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); + foreach (DataTypeDto? dataType in dataTypes) + { + dataType.EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker; + Database.Update(dataType); + } - if (dataTypeIds.Count == 0) return; + Sql sqlPropertyTpes = Sql() + .Select() + .From() + .Where(x => dataTypeIds.Contains(x.DataTypeId)); - foreach (var dataType in dataTypes) + var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); + + if (propertyTypeIds.Count == 0) + { + return; + } + + Sql sqlPropertyData = Sql() + .Select() + .From() + .Where(x => propertyTypeIds.Contains(x.PropertyTypeId)); + + List? properties = Database.Fetch(sqlPropertyData); + + // Create a Multi URL Picker datatype for the converted RelatedLinks data + foreach (PropertyDataDto? property in properties) + { + var value = property.Value?.ToString(); + if (string.IsNullOrWhiteSpace(value)) { - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MultiUrlPicker; - Database.Update(dataType); + continue; } - var sqlPropertyTpes = Sql() - .Select() - .From() - .Where(x => dataTypeIds.Contains(x.DataTypeId)); - - var propertyTypeIds = Database.Fetch(sqlPropertyTpes).Select(x => x.Id).ToList(); - - if (propertyTypeIds.Count == 0) return; - - var sqlPropertyData = Sql() - .Select() - .From() - .Where(x => propertyTypeIds.Contains(x.PropertyTypeId)); - - var properties = Database.Fetch(sqlPropertyData); - - // Create a Multi URL Picker datatype for the converted RelatedLinks data - - foreach (var property in properties) + List? relatedLinks = JsonConvert.DeserializeObject>(value); + var links = new List(); + if (relatedLinks is null) { - var value = property.Value?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - continue; + return; + } - var relatedLinks = JsonConvert.DeserializeObject>(value); - var links = new List(); - if (relatedLinks is null) + foreach (RelatedLink relatedLink in relatedLinks) + { + GuidUdi? udi = null; + if (relatedLink.IsInternal) { - return; - } - - foreach (var relatedLink in relatedLinks) - { - GuidUdi? udi = null; - if (relatedLink.IsInternal) + var linkIsUdi = UdiParser.TryParse(relatedLink.Link, out udi); + if (linkIsUdi == false) { - var linkIsUdi = UdiParser.TryParse(relatedLink.Link, out udi); - if (linkIsUdi == false) + // oh no.. probably an integer, yikes! + if (int.TryParse(relatedLink.Link, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var intId)) { - // oh no.. probably an integer, yikes! - if (int.TryParse(relatedLink.Link, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId)) - { - var sqlNodeData = Sql() - .Select() - .From() - .Where(x => x.NodeId == intId); + Sql sqlNodeData = Sql() + .Select() + .From() + .Where(x => x.NodeId == intId); - var node = Database.Fetch(sqlNodeData).FirstOrDefault(); - if (node != null) - // Note: RelatedLinks did not allow for picking media items, - // so if there's a value this will be a content item - hence - // the hardcoded "document" here - udi = new GuidUdi("document", node.UniqueId); + NodeDto? node = Database.Fetch(sqlNodeData).FirstOrDefault(); + if (node != null) + + // Note: RelatedLinks did not allow for picking media items, + // so if there's a value this will be a content item - hence + // the hardcoded "document" here + { + udi = new GuidUdi("document", node.UniqueId); } } } - - var link = new LinkDto - { - Name = relatedLink.Caption, - Target = relatedLink.NewWindow ? "_blank" : null, - Udi = udi, - // Should only have a URL if it's an external link otherwise it wil be a UDI - Url = relatedLink.IsInternal == false ? relatedLink.Link : null - }; - - links.Add(link); } - var json = JsonConvert.SerializeObject(links); + var link = new LinkDto + { + Name = relatedLink.Caption, + Target = relatedLink.NewWindow ? "_blank" : null, + Udi = udi, - // Update existing data - property.TextValue = json; - Database.Update(property); + // Should only have a URL if it's an external link otherwise it wil be a UDI + Url = relatedLink.IsInternal == false ? relatedLink.Link : null, + }; + + links.Add(link); } + var json = JsonConvert.SerializeObject(links); + // Update existing data + property.TextValue = json; + Database.Update(property); } } - - internal class RelatedLink - { - public int? Id { get; internal set; } - internal bool IsDeleted { get; set; } - [JsonProperty("caption")] - public string? Caption { get; set; } - [JsonProperty("link")] - public string? Link { get; set; } - [JsonProperty("newWindow")] - public bool NewWindow { get; set; } - [JsonProperty("isInternal")] - public bool IsInternal { get; set; } - } - - [DataContract] - internal class LinkDto - { - [DataMember(Name = "name")] - public string? Name { get; set; } - - [DataMember(Name = "target")] - public string? Target { get; set; } - - [DataMember(Name = "udi")] - public GuidUdi? Udi { get; set; } - - [DataMember(Name = "url")] - public string? Url { get; set; } - } +} + +internal class RelatedLink +{ + public int? Id { get; internal set; } + + [JsonProperty("caption")] + public string? Caption { get; set; } + + internal bool IsDeleted { get; set; } + + [JsonProperty("link")] + public string? Link { get; set; } + + [JsonProperty("newWindow")] + public bool NewWindow { get; set; } + + [JsonProperty("isInternal")] + public bool IsInternal { get; set; } +} + +[DataContract] +internal class LinkDto +{ + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "target")] + public string? Target { get; set; } + + [DataMember(Name = "udi")] + public GuidUdi? Udi { get; set; } + + [DataMember(Name = "url")] + public string? Url { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index c254ecc8df..d97b7ebcb5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -1,138 +1,144 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DataTypeMigration : MigrationBase { - - public class DataTypeMigration : MigrationBase + private static readonly ISet _legacyAliases = new HashSet { - private readonly PreValueMigratorCollection _preValueMigrators; - private readonly PropertyEditorCollection _propertyEditors; - private readonly ILogger _logger; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + Constants.PropertyEditors.Legacy.Aliases.Date, + Constants.PropertyEditors.Legacy.Aliases.Textbox, + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + }; - private static readonly ISet LegacyAliases = new HashSet() + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly ILogger _logger; + private readonly PreValueMigratorCollection _preValueMigrators; + private readonly PropertyEditorCollection _propertyEditors; + + public DataTypeMigration( + IMigrationContext context, + PreValueMigratorCollection preValueMigrators, + PropertyEditorCollection propertyEditors, + ILogger logger, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : base(context) + { + _preValueMigrators = preValueMigrators; + _propertyEditors = propertyEditors; + _logger = logger; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + } + + protected override void Migrate() + { + // drop and create columns + Delete.Column("pk").FromTable("cmsDataType").Do(); + + // rename the table + Rename.Table("cmsDataType").To(Constants.DatabaseSchema.Tables.DataType).Do(); + + // create column + AddColumn(Constants.DatabaseSchema.Tables.DataType, "config"); + Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty))).Do(); + + // renames + Execute.Sql(Sql() + .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) + .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias")).Do(); + + // from preValues to configuration... + Sql sql = Sql() + .Select() + .AndSelect(x => x.Id, x => x.Alias, x => x.SortOrder, x => x.Value) + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .OrderBy(x => x.NodeId) + .AndBy(x => x.SortOrder); + + IEnumerable> dtos = Database.Fetch(sql).GroupBy(x => x.NodeId); + + foreach (IGrouping group in dtos) { - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Date, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Textbox, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, - }; - - public DataTypeMigration(IMigrationContext context, - PreValueMigratorCollection preValueMigrators, - PropertyEditorCollection propertyEditors, - ILogger logger, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : base(context) - { - _preValueMigrators = preValueMigrators; - _propertyEditors = propertyEditors; - _logger = logger; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - } - - protected override void Migrate() - { - // drop and create columns - Delete.Column("pk").FromTable("cmsDataType").Do(); - - // rename the table - Rename.Table("cmsDataType").To(Cms.Core.Constants.DatabaseSchema.Tables.DataType).Do(); - - // create column - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "config"); - Execute.Sql(Sql().Update(u => u.Set(x => x.Configuration, string.Empty))).Do(); - - // renames - Execute.Sql(Sql() - .Update(u => u.Set(x => x.EditorAlias, "Umbraco.ColorPicker")) - .Where(x => x.EditorAlias == "Umbraco.ColorPickerAlias")).Do(); - - // from preValues to configuration... - var sql = Sql() + DataTypeDto? dataType = Database.Fetch(Sql() .Select() - .AndSelect(x => x.Id, x => x.Alias, x => x.SortOrder, x => x.Value) .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .OrderBy(x => x.NodeId) - .AndBy(x => x.SortOrder); + .Where(x => x.NodeId == group.Key)).First(); - var dtos = Database.Fetch(sql).GroupBy(x => x.NodeId); - - foreach (var group in dtos) + // check for duplicate aliases + var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (aliases.Distinct().Count() != aliases.Length) { - var dataType = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.NodeId == group.Key)).First(); - - // check for duplicate aliases - var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); - if (aliases.Distinct().Count() != aliases.Length) - throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); - - // handle null/empty aliases - int index = 0; - var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); - - // migrate the preValues to configuration - var migrator = _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); - var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); - var json = _configurationEditorJsonSerializer.Serialize(config); - - // validate - and kill the migration if it fails - var newAlias = migrator.GetNewAlias(dataType.EditorAlias); - if (newAlias == null) - { - if (!LegacyAliases.Contains(dataType.EditorAlias)) - { - _logger.LogWarning( - "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, dataType.EditorAlias); - } - } - else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor)) - { - if (!LegacyAliases.Contains(newAlias)) - { - _logger.LogWarning("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" - + " because no property editor with that alias was found." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); - } - } - else - { - var configEditor = propertyEditor.GetConfigurationEditor(); - try - { - var _ = configEditor.FromDatabase(json, _configurationEditorJsonSerializer); - } - catch (Exception e) - { - _logger.LogWarning(e, "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." - + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); - } - } - - // update - dataType.Configuration = _configurationEditorJsonSerializer.Serialize(config); - Database.Update(dataType); + throw new InvalidOperationException( + $"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); } + + // handle null/empty aliases + var index = 0; + var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); + + // migrate the preValues to configuration + IPreValueMigrator migrator = + _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); + var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); + var json = _configurationEditorJsonSerializer.Serialize(config); + + // validate - and kill the migration if it fails + var newAlias = migrator.GetNewAlias(dataType.EditorAlias); + if (newAlias == null) + { + if (!_legacyAliases.Contains(dataType.EditorAlias)) + { + _logger.LogWarning( + "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, dataType.EditorAlias); + } + } + else if (!_propertyEditors.TryGet(newAlias, out IDataEditor? propertyEditor)) + { + if (!_legacyAliases.Contains(newAlias)) + { + _logger.LogWarning( + "Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + + " because no property editor with that alias was found." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } + } + else + { + IConfigurationEditor configEditor = propertyEditor.GetConfigurationEditor(); + try + { + var _ = configEditor.FromDatabase(json, _configurationEditorJsonSerializer); + } + catch (Exception e) + { + _logger.LogWarning( + e, + "Failed to validate configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})." + + " Please fix the configuration and ensure it is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } + } + + // update + dataType.Configuration = _configurationEditorJsonSerializer.Serialize(config); + Database.Update(dataType); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs index 7e1711604a..13c9645ffe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs @@ -1,20 +1,23 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ContentPickerPreValueMigrator : DefaultPreValueMigrator { - class ContentPickerPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; + + public override string? GetNewAlias(string editorAlias) + => null; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; - - public override string? GetNewAlias(string editorAlias) - => null; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "showOpenButton" || + preValue.Alias == "ignoreUserStartNodes") { - if (preValue.Alias == "showOpenButton" || - preValue.Alias == "ignoreUserStartNodes") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); + return preValue.Value == "1"; } + + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs index 0383e7029e..eb7744cc18 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DecimalPreValueMigrator.cs @@ -1,21 +1,22 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DecimalPreValueMigrator : DefaultPreValueMigrator { - class DecimalPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Decimal"; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.Decimal"; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "min" || + preValue.Alias == "step" || + preValue.Alias == "max") { - if (preValue.Alias == "min" || - preValue.Alias == "step" || - preValue.Alias == "max") - return decimal.TryParse(preValue.Value, out var d) ? (decimal?) d : null; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return decimal.TryParse(preValue.Value, out var d) ? (decimal?)d : null; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs index 30507ac3ec..2faaca6086 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs @@ -1,43 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DefaultPreValueMigrator : IPreValueMigrator { - class DefaultPreValueMigrator : IPreValueMigrator + public virtual bool CanMigrate(string editorAlias) + => true; + + public virtual string? GetNewAlias(string editorAlias) + => editorAlias; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) { - public virtual bool CanMigrate(string editorAlias) - => true; - - public virtual string? GetNewAlias(string editorAlias) - => editorAlias; - - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + var preValuesA = preValues.Values.ToList(); + var aliases = preValuesA.Select(x => x.Alias).Distinct().ToArray(); + if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) { - var preValuesA = preValues.Values.ToList(); - var aliases = preValuesA.Select(x => x.Alias).Distinct().ToArray(); - if (aliases.Length == 1 && string.IsNullOrWhiteSpace(aliases[0])) + // array-based prevalues + return new Dictionary { - // array-based prevalues - return new Dictionary { ["values"] = preValuesA.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray() }; - } - - // assuming we don't want to fall back to array - if (aliases.Any(string.IsNullOrWhiteSpace)) - throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); - - // dictionary-base prevalues - return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); + ["values"] = preValuesA.OrderBy(x => x.SortOrder).Select(x => x.Value).ToArray(), + }; } - protected virtual IEnumerable GetPreValues(IEnumerable preValues) - => preValues; - - protected virtual object? GetPreValueValue(PreValueDto preValue) + // assuming we don't want to fall back to array + if (aliases.Any(string.IsNullOrWhiteSpace)) { - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + throw new InvalidOperationException( + $"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); } + + // dictionary-base prevalues + return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); } + + protected virtual IEnumerable GetPreValues(IEnumerable preValues) + => preValues; + + protected virtual object? GetPreValueValue(PreValueDto preValue) => preValue.Value?.DetectIsJson() ?? false + ? JsonConvert.DeserializeObject(preValue.Value) + : preValue.Value; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs index 6c0f3d4869..6588676283 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs @@ -1,31 +1,30 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class DropDownFlexiblePreValueMigrator : IPreValueMigrator { - class DropDownFlexiblePreValueMigrator : IPreValueMigrator + public bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.DropDown.Flexible"; + + public virtual string? GetNewAlias(string editorAlias) + => null; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) { - public bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.DropDown.Flexible"; - - public virtual string? GetNewAlias(string editorAlias) - => null; - - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + var config = new DropDownFlexibleConfiguration(); + foreach (PreValueDto preValue in preValues.Values) { - var config = new DropDownFlexibleConfiguration(); - foreach (var preValue in preValues.Values) + if (preValue.Alias == "multiple") { - if (preValue.Alias == "multiple") - { - config.Multiple = (preValue.Value == "1"); - } - else - { - config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); - } + config.Multiple = preValue.Value == "1"; + } + else + { + config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); } - return config; } + + return config; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs index 5489fd626e..11a126a60b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/IPreValueMigrator.cs @@ -1,36 +1,35 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +/// +/// Defines a service migrating preValues. +/// +public interface IPreValueMigrator { /// - /// Defines a service migrating preValues. + /// Determines whether this migrator can migrate a data type. /// - public interface IPreValueMigrator - { - /// - /// Determines whether this migrator can migrate a data type. - /// - /// The data type editor alias. - bool CanMigrate(string editorAlias); + /// The data type editor alias. + bool CanMigrate(string editorAlias); - /// - /// Gets the v8 codebase data type editor alias. - /// - /// The original v7 codebase editor alias. - /// - /// This is used to validate that the migrated configuration can be parsed - /// by the new property editor. Return null to bypass this validation, - /// when for instance we know it will fail, and another, later migration will - /// deal with it. - /// - string? GetNewAlias(string editorAlias); + /// + /// Gets the v8 codebase data type editor alias. + /// + /// The original v7 codebase editor alias. + /// + /// + /// This is used to validate that the migrated configuration can be parsed + /// by the new property editor. Return null to bypass this validation, + /// when for instance we know it will fail, and another, later migration will + /// deal with it. + /// + /// + string? GetNewAlias(string editorAlias); - /// - /// Gets the configuration object corresponding to preValue. - /// - /// The data type identifier. - /// The data type editor alias. - /// PreValues. - object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); - } + /// + /// Gets the configuration object corresponding to preValue. + /// + /// The data type identifier. + /// The data type editor alias. + /// PreValues. + object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs index c306e3eef3..7879e9c67d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ListViewPreValueMigrator.cs @@ -1,29 +1,26 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ListViewPreValueMigrator : DefaultPreValueMigrator { - class ListViewPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.ListView"; + + protected override IEnumerable GetPreValues(IEnumerable preValues) => + preValues.Where(preValue => preValue.Alias != "displayAtTabNumber"); + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.ListView"; - - protected override IEnumerable GetPreValues(IEnumerable preValues) + if (preValue.Alias == "pageSize") { - return preValues.Where(preValue => preValue.Alias != "displayAtTabNumber"); + return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? (int?)i + : null; } - protected override object? GetPreValueValue(PreValueDto preValue) - { - if (preValue.Alias == "pageSize") - { - return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? (int?)i : null; - } - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; - } + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs index 9f8e7da57a..eff4b82477 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs @@ -1,16 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator // PreValueMigratorBase { - class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MarkdownEditor; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "preview") { - if (preValue.Alias == "preview") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); + return preValue.Value == "1"; } + + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs index 364cc3e86b..c630693073 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs @@ -1,37 +1,37 @@ -using System.Linq; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class MediaPickerPreValueMigrator : DefaultPreValueMigrator // PreValueMigratorBase { - class MediaPickerPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + private readonly string[] _editors = { - private readonly string[] _editors = + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker, + }; + + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.MediaPicker; + + // you wish - but MediaPickerConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + return new MediaPickerConfiguration { ... }; + } + */ + + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "multiPicker" || + preValue.Alias == "onlyImages" || + preValue.Alias == "disableFolderSelect") { - Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, - Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - }; - - public override bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); - - public override string GetNewAlias(string editorAlias) - => Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker; - - // you wish - but MediaPickerConfiguration lives in Umbraco.Web - /* - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) - { - return new MediaPickerConfiguration { ... }; + return preValue.Value == "1"; } - */ - protected override object? GetPreValueValue(PreValueDto preValue) - { - if (preValue.Alias == "multiPicker" || - preValue.Alias == "onlyImages" || - preValue.Alias == "disableFolderSelect") - return preValue.Value == "1"; - - return base.GetPreValueValue(preValue); - } + return base.GetPreValueValue(preValue); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs index 761f55be4e..72c28dc8ad 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/NestedContentPreValueMigrator.cs @@ -1,34 +1,39 @@ -using System.Globalization; +using System.Globalization; using Newtonsoft.Json; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class NestedContentPreValueMigrator : DefaultPreValueMigrator // PreValueMigratorBase { - class NestedContentPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.NestedContent"; + + // you wish - but NestedContentConfiguration lives in Umbraco.Web + /* + public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.NestedContent"; + return new NestedContentConfiguration { ... }; + } + */ - // you wish - but NestedContentConfiguration lives in Umbraco.Web - /* - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + protected override object? GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "confirmDeletes" || + preValue.Alias == "showIcons" || + preValue.Alias == "hideLabel") { - return new NestedContentConfiguration { ... }; + return preValue.Value == "1"; } - */ - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "minItems" || + preValue.Alias == "maxItems") { - if (preValue.Alias == "confirmDeletes" || - preValue.Alias == "showIcons" || - preValue.Alias == "hideLabel") - return preValue.Value == "1"; - - if (preValue.Alias == "minItems" || - preValue.Alias == "maxItems") - return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? (int?)i : null; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return int.TryParse(preValue.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) + ? (int?)i + : null; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs index d3f4b06737..d3e639452e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueDto.cs @@ -1,24 +1,23 @@ -using NPoco; +using NPoco; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +[TableName("cmsDataTypePreValues")] +[ExplicitColumns] +public class PreValueDto { - [TableName("cmsDataTypePreValues")] - [ExplicitColumns] - public class PreValueDto - { - [Column("id")] - public int Id { get; set; } + [Column("id")] + public int Id { get; set; } - [Column("datatypeNodeId")] - public int NodeId { get; set; } + [Column("datatypeNodeId")] + public int NodeId { get; set; } - [Column("alias")] - public string Alias { get; set; } = null!; + [Column("alias")] + public string Alias { get; set; } = null!; - [Column("sortorder")] - public int SortOrder { get; set; } + [Column("sortorder")] + public int SortOrder { get; set; } - [Column("value")] - public string? Value { get; set; } - } + [Column("value")] + public string? Value { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs index d4f5f4c425..df9cb27ec3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorBase.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +public abstract class PreValueMigratorBase : IPreValueMigrator { - public abstract class PreValueMigratorBase : IPreValueMigrator - { - public abstract bool CanMigrate(string editorAlias); + public abstract bool CanMigrate(string editorAlias); - public virtual string GetNewAlias(string editorAlias) - => editorAlias; + public virtual string GetNewAlias(string editorAlias) + => editorAlias; - public abstract object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues); + public abstract object GetConfiguration(int dataTypeId, string editorAlias, + Dictionary preValues); - protected bool GetBoolValue(Dictionary preValues, string alias, bool defaultValue = false) - => preValues.TryGetValue(alias, out var preValue) ? preValue.Value == "1" : defaultValue; + protected bool GetBoolValue(Dictionary preValues, string alias, bool defaultValue = false) + => preValues.TryGetValue(alias, out PreValueDto? preValue) ? preValue.Value == "1" : defaultValue; - protected decimal GetDecimalValue(Dictionary preValues, string alias, decimal defaultValue = 0) - => preValues.TryGetValue(alias, out var preValue) && decimal.TryParse(preValue.Value, out var value) ? value : defaultValue; - } + protected decimal GetDecimalValue(Dictionary preValues, string alias, decimal defaultValue = 0) + => preValues.TryGetValue(alias, out PreValueDto? preValue) && decimal.TryParse(preValue.Value, out var value) + ? value + : defaultValue; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs index b304098188..81a6200991 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollection.cs @@ -1,27 +1,23 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +public class PreValueMigratorCollection : BuilderCollectionBase { - public class PreValueMigratorCollection : BuilderCollectionBase + private readonly ILogger _logger; + + public PreValueMigratorCollection( + Func> items, + ILogger logger) + : base(items) => + _logger = logger; + + public IPreValueMigrator? GetMigrator(string editorAlias) { - private readonly ILogger _logger; - - public PreValueMigratorCollection(Func> items, ILogger logger) - : base(items) - { - _logger = logger; - } - - public IPreValueMigrator? GetMigrator(string editorAlias) - { - var migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); - _logger.LogDebug("Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, migrator == null ? "" : migrator.GetType().Name); - return migrator; - } + IPreValueMigrator? migrator = this.FirstOrDefault(x => x.CanMigrate(editorAlias)); + _logger.LogDebug("Getting migrator for \"{EditorAlias}\" = {MigratorType}", editorAlias, + migrator == null ? "" : migrator.GetType().Name); + return migrator; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs index 2c90a0d504..ba335eef4b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +public class PreValueMigratorCollectionBuilder : OrderedCollectionBuilderBase { - public class PreValueMigratorCollectionBuilder : OrderedCollectionBuilderBase - { - protected override PreValueMigratorCollectionBuilder This => this; - } + protected override PreValueMigratorCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs index 5d05de56c3..273c8ae51b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs @@ -1,27 +1,23 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Exceptions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class RenamingPreValueMigrator : DefaultPreValueMigrator { - class RenamingPreValueMigrator : DefaultPreValueMigrator + private readonly string[] _editors = { "Umbraco.NoEdit" }; + + public override bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public override string GetNewAlias(string editorAlias) { - private readonly string[] _editors = + switch (editorAlias) { - "Umbraco.NoEdit" - }; - - public override bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); - - public override string GetNewAlias(string editorAlias) - { - switch (editorAlias) - { - case "Umbraco.NoEdit": - return Cms.Core.Constants.PropertyEditors.Aliases.Label; - default: - throw new PanicException($"The alias {editorAlias} is not supported"); - } + case "Umbraco.NoEdit": + return Constants.PropertyEditors.Aliases.Label; + default: + throw new PanicException($"The alias {editorAlias} is not supported"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs index 0abcd86a96..4e7c5b79f1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/RichTextPreValueMigrator.cs @@ -1,22 +1,24 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class RichTextPreValueMigrator : DefaultPreValueMigrator { - class RichTextPreValueMigrator : DefaultPreValueMigrator + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.TinyMCEv3"; + + public override string GetNewAlias(string editorAlias) + => Constants.PropertyEditors.Aliases.TinyMce; + + protected override object? GetPreValueValue(PreValueDto preValue) { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.TinyMCEv3"; - - public override string GetNewAlias(string editorAlias) - => Cms.Core.Constants.PropertyEditors.Aliases.TinyMce; - - protected override object? GetPreValueValue(PreValueDto preValue) + if (preValue.Alias == "hideLabel") { - if (preValue.Alias == "hideLabel") - return preValue.Value == "1"; - - return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; + return preValue.Value == "1"; } + + return preValue.Value?.DetectIsJson() ?? false ? JsonConvert.DeserializeObject(preValue.Value) : preValue.Value; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs index c193f27028..7f8632dd7a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/UmbracoSliderPreValueMigrator.cs @@ -1,24 +1,21 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes -{ - class UmbracoSliderPreValueMigrator : PreValueMigratorBase - { - public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.Slider"; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; - public override object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) +internal class UmbracoSliderPreValueMigrator : PreValueMigratorBase +{ + public override bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.Slider"; + + public override object GetConfiguration(int dataTypeId, string editorAlias, + Dictionary preValues) => + new SliderConfiguration { - return new SliderConfiguration - { - EnableRange = GetBoolValue(preValues, "enableRange"), - InitialValue = GetDecimalValue(preValues, "initVal1"), - InitialValue2 = GetDecimalValue(preValues, "initVal2"), - MaximumValue = GetDecimalValue(preValues, "maxVal"), - MinimumValue = GetDecimalValue(preValues, "minVal"), - StepIncrements = GetDecimalValue(preValues, "step") - }; - } - } + EnableRange = GetBoolValue(preValues, "enableRange"), + InitialValue = GetDecimalValue(preValues, "initVal1"), + InitialValue2 = GetDecimalValue(preValues, "initVal2"), + MaximumValue = GetDecimalValue(preValues, "maxVal"), + MinimumValue = GetDecimalValue(preValues, "minVal"), + StepIncrements = GetDecimalValue(preValues, "step"), + }; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs index 44b12addd2..9528cadc8b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -1,33 +1,29 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.PropertyEditors; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; + +internal class ValueListPreValueMigrator : IPreValueMigrator { - class ValueListPreValueMigrator : IPreValueMigrator + private readonly string[] _editors = { - private readonly string[] _editors = + "Umbraco.RadioButtonList", "Umbraco.CheckBoxList", "Umbraco.DropDown", "Umbraco.DropdownlistPublishingKeys", + "Umbraco.DropDownMultiple", "Umbraco.DropdownlistMultiplePublishKeys", + }; + + public bool CanMigrate(string editorAlias) + => _editors.Contains(editorAlias); + + public virtual string? GetNewAlias(string editorAlias) + => null; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var config = new ValueListConfiguration(); + foreach (PreValueDto preValue in preValues.Values) { - "Umbraco.RadioButtonList", - "Umbraco.CheckBoxList", - "Umbraco.DropDown", - "Umbraco.DropdownlistPublishingKeys", - "Umbraco.DropDownMultiple", - "Umbraco.DropdownlistMultiplePublishKeys" - }; - - public bool CanMigrate(string editorAlias) - => _editors.Contains(editorAlias); - - public virtual string? GetNewAlias(string editorAlias) - => null; - - public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) - { - var config = new ValueListConfiguration(); - foreach (var preValue in preValues.Values) - config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); - return config; + config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); } + + return config; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs index 0d4b6020a9..e80dd72765 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -13,125 +11,133 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase { - public class DropDownPropertyEditorsMigration : PropertyEditorsMigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + public DropDownPropertyEditorsMigration(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - public DropDownPropertyEditorsMigration(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) + public DropDownPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } + + protected override void Migrate() + { + var refreshCache = Migrate(GetDataTypes(".DropDown", false)); + + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) { + Context.AddPostMigration(); } + } - public DropDownPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, - IEditorConfigurationParser editorConfigurationParser) - : base(context) + private bool Migrate(IEnumerable dataTypes) + { + var refreshCache = false; + ConfigurationEditor? configurationEditor = null; + + foreach (DataTypeDto dataType in dataTypes) { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } + ValueListConfiguration config; - protected override void Migrate() - { - var refreshCache = Migrate(GetDataTypes(".DropDown", false)); - - // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), - // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - Context.AddPostMigration(); - } - - private bool Migrate(IEnumerable dataTypes) - { - var refreshCache = false; - ConfigurationEditor? configurationEditor = null; - - foreach (var dataType in dataTypes) + if (!dataType.Configuration.IsNullOrWhiteSpace()) { - ValueListConfiguration config; - - if (!dataType.Configuration.IsNullOrWhiteSpace()) + // parse configuration, and update everything accordingly + if (configurationEditor == null) { - // parse configuration, and update everything accordingly - if (configurationEditor == null) - configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); - try - { - config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration, _configurationEditorJsonSerializer); - } - catch (Exception ex) - { - Logger.LogError( - ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", - dataType.Configuration); - - // reset - config = new ValueListConfiguration(); - } - - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, true)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) - Database.Update(propertyDataDto); + configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); } - else + + try { - // default configuration + config = (ValueListConfiguration)configurationEditor.FromDatabase( + dataType.Configuration, + _configurationEditorJsonSerializer); + } + catch (Exception ex) + { + Logger.LogError( + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", + dataType.Configuration); + + // reset config = new ValueListConfiguration(); } - switch (dataType.EditorAlias) - { - case string ea when ea.InvariantEquals("Umbraco.DropDown"): - UpdateDataType(dataType, config, false); - break; - case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): - UpdateDataType(dataType, config, false); - break; - case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): - UpdateDataType(dataType, config, true); - break; - case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): - UpdateDataType(dataType, config, true); - break; - } + // get property data dtos + List? propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin() + .On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); - refreshCache = true; + // update dtos + IEnumerable updatedDtos = + propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, true)); + + // persist changes + foreach (PropertyDataDto? propertyDataDto in updatedDtos) + { + Database.Update(propertyDataDto); + } + } + else + { + // default configuration + config = new ValueListConfiguration(); } - return refreshCache; - } - - private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) - { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DropDownListFlexible; - - var flexConfig = new DropDownFlexibleConfiguration + switch (dataType.EditorAlias) { - Items = config.Items, - Multiple = isMultiple - }; - dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); + case string ea when ea.InvariantEquals("Umbraco.DropDown"): + UpdateDataType(dataType, config, false); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"): + UpdateDataType(dataType, config, false); + break; + case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"): + UpdateDataType(dataType, config, true); + break; + case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"): + UpdateDataType(dataType, config, true); + break; + } - Database.Update(dataType); + refreshCache = true; } + + return refreshCache; + } + + private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible; + + var flexConfig = new DropDownFlexibleConfiguration { Items = config.Items, Multiple = isMultiple }; + dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig, _configurationEditorJsonSerializer); + + Database.Update(dataType); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs index 0d1e0506cb..8eebd91772 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropMigrationsTable.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropMigrationsTable : MigrationBase - { - public DropMigrationsTable(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropMigrationsTable : MigrationBase +{ + public DropMigrationsTable(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (TableExists("umbracoMigration")) { - if (TableExists("umbracoMigration")) - Delete.Table("umbracoMigration").Do(); + Delete.Table("umbracoMigration").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs index 0195e51e6e..64152d0cb3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropPreValueTable.cs @@ -1,15 +1,18 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropPreValueTable : MigrationBase - { - public DropPreValueTable(IMigrationContext context) : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropPreValueTable : MigrationBase +{ + public DropPreValueTable(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // drop preValues table + if (TableExists("cmsDataTypePreValues")) { - // drop preValues table - if (TableExists("cmsDataTypePreValues")) - Delete.Table("cmsDataTypePreValues").Do(); + Delete.Table("cmsDataTypePreValues").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs index b4004c1c82..e38c0c3292 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs @@ -1,17 +1,22 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropTaskTables : MigrationBase - { - public DropTaskTables(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropTaskTables : MigrationBase +{ + public DropTaskTables(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (TableExists("cmsTask")) { - if (TableExists("cmsTask")) - Delete.Table("cmsTask").Do(); - if (TableExists("cmsTaskType")) - Delete.Table("cmsTaskType").Do(); + Delete.Table("cmsTask").Do(); + } + + if (TableExists("cmsTaskType")) + { + Delete.Table("cmsTaskType").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs index 9f65689a59..454644b1fb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs @@ -1,15 +1,17 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropTemplateDesignColumn : MigrationBase - { - public DropTemplateDesignColumn(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropTemplateDesignColumn : MigrationBase +{ + public DropTemplateDesignColumn(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (ColumnExists("cmsTemplate", "design")) { - if(ColumnExists("cmsTemplate", "design")) - Delete.Column("design").FromTable("cmsTemplate").Do(); + Delete.Column("design").FromTable("cmsTemplate").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs index 3e86e142aa..1cdb73d410 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/DropXmlTables.cs @@ -1,17 +1,22 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class DropXmlTables : MigrationBase - { - public DropXmlTables(IMigrationContext context) - : base(context) - { } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; - protected override void Migrate() +public class DropXmlTables : MigrationBase +{ + public DropXmlTables(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + if (TableExists("cmsContentXml")) { - if (TableExists("cmsContentXml")) - Delete.Table("cmsContentXml").Do(); - if (TableExists("cmsPreviewXml")) - Delete.Table("cmsPreviewXml").Do(); + Delete.Table("cmsContentXml").Do(); + } + + if (TableExists("cmsPreviewXml")) + { + Delete.Table("cmsPreviewXml").Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs index 48e00df2ff..4f3b685ef5 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FallbackLanguage.cs @@ -1,25 +1,30 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +/// +/// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for +/// a given language. +/// +public class FallbackLanguage : MigrationBase { - /// - /// Adds a new, self-joined field to umbracoLanguages to hold the fall-back language for - /// a given language. - /// - public class FallbackLanguage : MigrationBase + public FallbackLanguage(IMigrationContext context) + : base(context) { - public FallbackLanguage(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + ColumnInfo[] columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => + x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.Language) && + x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); - - if (columns.Any(x => x.TableName.InvariantEquals(Cms.Core.Constants.DatabaseSchema.Tables.Language) && x.ColumnName.InvariantEquals("fallbackLanguageId")) == false) - AddColumn("fallbackLanguageId"); + AddColumn("fallbackLanguageId"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs index 7a35dc12ed..f1bd804c59 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/FixLanguageIsoCodeLength.cs @@ -1,21 +1,19 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class FixLanguageIsoCodeLength : MigrationBase { - public class FixLanguageIsoCodeLength : MigrationBase + public FixLanguageIsoCodeLength(IMigrationContext context) + : base(context) { - public FixLanguageIsoCodeLength(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - // there is some confusion here when upgrading from v7 - // it should be 14 already but that's not always the case - - Alter.Table("umbracoLanguage") - .AlterColumn("languageISOCode") - .AsString(14) - .Nullable() - .Do(); - } } + + protected override void Migrate() => + + // there is some confusion here when upgrading from v7 + // it should be 14 already but that's not always the case + Alter.Table("umbracoLanguage") + .AlterColumn("languageISOCode") + .AsString(14) + .Nullable() + .Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs index f6aa86259f..a265195bc9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/LanguageColumns.cs @@ -1,17 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class LanguageColumns : MigrationBase { - public class LanguageColumns : MigrationBase + public LanguageColumns(IMigrationContext context) + : base(context) { - public LanguageColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.Language, "isDefaultVariantLang"); - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.Language, "mandatory"); - } + protected override void Migrate() + { + AddColumn(Constants.DatabaseSchema.Tables.Language, "isDefaultVariantLang"); + AddColumn(Constants.DatabaseSchema.Tables.Language, "mandatory"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs index 7958f4fbf8..64c8d4c8b4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeRedirectUrlVariant.cs @@ -1,16 +1,13 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MakeRedirectUrlVariant : MigrationBase { - public class MakeRedirectUrlVariant : MigrationBase + public MakeRedirectUrlVariant(IMigrationContext context) + : base(context) { - public MakeRedirectUrlVariant(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - AddColumn("culture"); - } } + + protected override void Migrate() => AddColumn("culture"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs index 74cdd88357..630a853aa2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MakeTagsVariant.cs @@ -1,16 +1,13 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MakeTagsVariant : MigrationBase { - public class MakeTagsVariant : MigrationBase + public MakeTagsVariant(IMigrationContext context) + : base(context) { - public MakeTagsVariant(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - AddColumn("languageId"); - } } + + protected override void Migrate() => AddColumn("languageId"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index db7766213c..f89a6d1497 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -10,87 +9,91 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class MergeDateAndDateTimePropertyEditor : MigrationBase { - public class MergeDateAndDateTimePropertyEditor : MigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) + public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } + + protected override void Migrate() + { + List dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date); + + foreach (DataTypeDto dataType in dataTypes) { - } - - public MergeDateAndDateTimePropertyEditor(IMigrationContext context, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, IEditorConfigurationParser editorConfigurationParser) - : base(context) - { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } - - protected override void Migrate() - { - var dataTypes = GetDataTypes(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Date); - - foreach (var dataType in dataTypes) + DateTimeConfiguration config; + try { - DateTimeConfiguration config; - try + config = (DateTimeConfiguration)new CustomDateTimeConfigurationEditor( + _ioHelper, + _editorConfigurationParser).FromDatabase( + dataType.Configuration, _configurationEditorJsonSerializer); + + // If the Umbraco.Date type is the default from V7 and it has never been updated, then the + // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that + // after the migration, so we force the format to the default from V7. + if (string.IsNullOrEmpty(dataType.Configuration)) { - config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor(_ioHelper, _editorConfigurationParser).FromDatabase( - dataType.Configuration, _configurationEditorJsonSerializer); - - // If the Umbraco.Date type is the default from V7 and it has never been updated, then the - // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that - // after the migration, so we force the format to the default from V7. - if (string.IsNullOrEmpty(dataType.Configuration)) - { - config.Format = "YYYY-MM-DD"; - } + config.Format = "YYYY-MM-DD"; } - catch (Exception ex) - { - Logger.LogError( - ex, - "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", - dataType.Configuration); - - continue; - } - - config.OffsetTime = false; - - dataType.EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.DateTime; - dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); - - Database.Update(dataType); } - } - - - - private List GetDataTypes(string editorAlias) - { - //need to convert the old drop down data types to use the new one - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias == editorAlias)); - return dataTypes; - } - - - - private class CustomDateTimeConfigurationEditor : ConfigurationEditor - { - public CustomDateTimeConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) + catch (Exception ex) { + Logger.LogError( + ex, + "Invalid property editor configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared", + dataType.Configuration); + + continue; } + + config.OffsetTime = false; + + dataType.EditorAlias = Constants.PropertyEditors.Aliases.DateTime; + dataType.Configuration = ConfigurationEditor.ToDatabase(config, _configurationEditorJsonSerializer); + + Database.Update(dataType); + } + } + + private List GetDataTypes(string editorAlias) + { + // need to convert the old drop down data types to use the new one + List? dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias == editorAlias)); + return dataTypes; + } + + private class CustomDateTimeConfigurationEditor : ConfigurationEditor + { + public CustomDateTimeConfigurationEditor( + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) + { } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs index bbd1646ad5..856c81af52 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/ContentTypeDto80.cs @@ -1,63 +1,63 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations the schema of this table changed and running SQL against the new table would +/// result in errors +/// +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class ContentTypeDto80 { + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors - /// - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class ContentTypeDto80 - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentType; + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 535)] + public int PrimaryKey { get; set; } - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] - public int PrimaryKey { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get; set; } - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get; set; } + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string? Thumbnail { get; set; } - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string? Thumbnail { get; set; } + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string? Description { get; set; } - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string? Description { get; set; } + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } - - [ResultColumn] - public NodeDto? NodeDto { get; set; } - } + [ResultColumn] + public NodeDto? NodeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs index 1e9e93aa53..4c537472db 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyDataDto80.cs @@ -1,143 +1,142 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations the schema of this table changed and running SQL against the new table would +/// result in errors +/// +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyDataDto80 { - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations the schema of this table changed and running SQL against the new table would result in errors - /// - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyDataDto80 + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", + ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto80))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string? Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.PropertyData; - public const int VarcharLength = 512; - public const int SegmentLength = 256; + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } - private decimal? _decimalValue; + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } - // pk, not used at the moment (never updating) - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string? VarcharValue { get; set; } - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] - public int VersionId { get; set; } + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TextValue { get; set; } - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto80))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] - public int PropertyTypeId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto80? PropertyTypeDto { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get; set; } - - [Column("segment")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(SegmentLength)] - public string? Segment { get; set; } - - [Column("intValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? IntegerValue { get; set; } - - [Column("decimalValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public decimal? DecimalValue + [Ignore] + public object? Value + { + get { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } - - [Column("dateValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? DateValue { get; set; } - - [Column("varcharValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(VarcharLength)] - public string? VarcharValue { get; set; } - - [Column("textValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TextValue { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] - public PropertyTypeDto80? PropertyTypeDto { get; set; } - - [Ignore] - public object? Value - { - get + if (IntegerValue.HasValue) { - if (IntegerValue.HasValue) - return IntegerValue.Value; - - if (DecimalValue.HasValue) - return DecimalValue.Value; - - if (DateValue.HasValue) - return DateValue.Value; - - if (!string.IsNullOrEmpty(VarcharValue)) - return VarcharValue; - - if (!string.IsNullOrEmpty(TextValue)) - return TextValue; - - return null; + return IntegerValue.Value; } - } - public PropertyDataDto80 Clone(int versionId) - { - return new PropertyDataDto80 + if (DecimalValue.HasValue) { - VersionId = versionId, - PropertyTypeId = PropertyTypeId, - LanguageId = LanguageId, - Segment = Segment, - IntegerValue = IntegerValue, - DecimalValue = DecimalValue, - DateValue = DateValue, - VarcharValue = VarcharValue, - TextValue = TextValue, - PropertyTypeDto = PropertyTypeDto - }; - } + return DecimalValue.Value; + } - protected bool Equals(PropertyDataDto other) - { - return Id == other.Id; - } + if (DateValue.HasValue) + { + return DateValue.Value; + } - public override bool Equals(object? other) - { - return - !ReferenceEquals(null, other) // other is not null - && (ReferenceEquals(this, other) // and either ref-equals, or same id - || other is PropertyDataDto pdata && pdata.Id == Id); - } + if (!string.IsNullOrEmpty(VarcharValue)) + { + return VarcharValue; + } - public override int GetHashCode() - { - // ReSharper disable once NonReadonlyMemberInGetHashCode - return Id; + if (!string.IsNullOrEmpty(TextValue)) + { + return TextValue; + } + + return null; } } + + public PropertyDataDto80 Clone(int versionId) => + new PropertyDataDto80 + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto + }; + + protected bool Equals(PropertyDataDto other) => Id == other.Id; + + public override bool Equals(object? other) => + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || (other is PropertyDataDto pdata && pdata.Id == Id)); + + public override int GetHashCode() => + // ReSharper disable once NonReadonlyMemberInGetHashCode + Id; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs index 4d61521d00..a5a7f48d6d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/Models/PropertyTypeDto80.cs @@ -1,76 +1,76 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; + +/// +/// Snapshot of the as it was at version 8.0 +/// +/// +/// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table +/// would result in errors +/// +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeDto80 { - /// - /// Snapshot of the as it was at version 8.0 - /// - /// - /// This is required during migrations before 8.6 since the schema has changed and running SQL against the new table would result in errors - /// - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeDto80 - { - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 50)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 50)] + public int Id { get; set; } - [Column("dataTypeId")] - [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } - [Column("propertyTypeGroupId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(PropertyTypeGroupDto))] - public int? PropertyTypeGroupId { get; set; } + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } - [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] - [Column("Alias")] - public string Alias { get; set; } = null!; + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string Alias { get; set; } = null!; - [Column("Name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } - [Column("sortOrder")] - [Constraint(Default = "0")] - public int SortOrder { get; set; } + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } - [Column("mandatory")] - [Constraint(Default = "0")] - public bool Mandatory { get; set; } + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } - [Column("validationRegExp")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? ValidationRegExp { get; set; } - [Column("Description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(2000)] - public string? Description { get; set; } + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string? Description { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] - public DataTypeDto? DataTypeDto { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto? DataTypeDto { get; set; } - [Column("UniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs index 935d51dacd..cef6eb974f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs @@ -1,54 +1,63 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class PropertyEditorsMigration : MigrationBase { - public class PropertyEditorsMigration : MigrationBase + public PropertyEditorsMigration(IMigrationContext context) + : base(context) { - public PropertyEditorsMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Cms.Core.Constants.PropertyEditors.Aliases.MemberPicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Cms.Core.Constants.PropertyEditors.Aliases.TextArea, false); - RenameDataType(Cms.Core.Constants.PropertyEditors.Legacy.Aliases.Textbox, Cms.Core.Constants.PropertyEditors.Aliases.TextBox, false); - } + protected override void Migrate() + { + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Aliases.ContentPicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Aliases.MediaPicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Aliases.MemberPicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + RenameDataType( + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Aliases.TextArea, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, + false); + } - private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) + private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) + { + if (checkCollision) { - if (checkCollision) + var oldCount = Database.ExecuteScalar(Sql() + .SelectCount() + .From() + .Where(x => x.EditorAlias == toAlias)); + + if (oldCount > 0) { - var oldCount = Database.ExecuteScalar(Sql() - .SelectCount() - .From() - .Where(x => x.EditorAlias == toAlias)); - - if (oldCount > 0) - { - // If we throw it means that the upgrade will exit and cannot continue. - // This will occur if a v7 site has the old "Obsolete" property editors that are already named with the `toAlias` name. - // TODO: We should have an additional upgrade step when going from 7 -> 8 like we did with 6 -> 7 that shows a compatibility report, - // this would include this check and then we can provide users with information on what they should do (i.e. before upgrading to v8 they will - // need to migrate these old obsolete editors to non-obsolete editors) - - throw new InvalidOperationException( - $"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used." + - $"This is generally because when upgrading from a v7 to v8 site, the v7 site contains Data Types that reference old and already Obsolete " + - $"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + - $"to the non-obsolete v7 property editors of the same type."); - } - + // If we throw it means that the upgrade will exit and cannot continue. + // This will occur if a v7 site has the old "Obsolete" property editors that are already named with the `toAlias` name. + // TODO: We should have an additional upgrade step when going from 7 -> 8 like we did with 6 -> 7 that shows a compatibility report, + // this would include this check and then we can provide users with information on what they should do (i.e. before upgrading to v8 they will + // need to migrate these old obsolete editors to non-obsolete editors) + throw new InvalidOperationException( + $"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used." + + "This is generally because when upgrading from a v7 to v8 site, the v7 site contains Data Types that reference old and already Obsolete " + + "Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + + "to the non-obsolete v7 property editors of the same type."); } - - Database.Execute(Sql() - .Update(u => u.Set(x => x.EditorAlias, toAlias)) - .Where(x => x.EditorAlias == fromAlias)); } + + Database.Execute(Sql() + .Update(u => u.Set(x => x.EditorAlias, toAlias)) + .Where(x => x.EditorAlias == fromAlias)); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs index 321da13df8..febf872e34 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs @@ -1,105 +1,115 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public abstract class PropertyEditorsMigrationBase : MigrationBase { - public abstract class PropertyEditorsMigrationBase : MigrationBase + protected PropertyEditorsMigrationBase(IMigrationContext context) + : base(context) { - protected PropertyEditorsMigrationBase(IMigrationContext context) - : base(context) - { } + } - internal List GetDataTypes(string editorAlias, bool strict = true) + internal List GetDataTypes(string editorAlias, bool strict = true) + { + Sql sql = Sql() + .Select() + .From(); + + sql = strict + ? sql.Where(x => x.EditorAlias == editorAlias) + : sql.Where(x => x.EditorAlias.Contains(editorAlias)); + + return Database.Fetch(sql); + } + + internal bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool isMultiple) + { + // Get the INT ids stored for this property/drop down + int[]? ids = null; + if (!propData.VarcharValue.IsNullOrWhiteSpace()) { - var sql = Sql() - .Select() - .From(); - - sql = strict - ? sql.Where(x => x.EditorAlias == editorAlias) - : sql.Where(x => x.EditorAlias.Contains(editorAlias)); - - return Database.Fetch(sql); + ids = ConvertStringValues(propData.VarcharValue); + } + else if (!propData.TextValue.IsNullOrWhiteSpace()) + { + ids = ConvertStringValues(propData.TextValue); + } + else if (propData.IntegerValue.HasValue) + { + ids = new[] { propData.IntegerValue.Value }; } - protected int[]? ConvertStringValues(string? val) + // if there are INT ids, convert them to values based on the configuration + if (ids == null || ids.Length <= 0) { - var splitVals = val?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - - var intVals = splitVals? - .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : int.MinValue) - .Where(x => x != int.MinValue) - .ToArray(); - - //only return if the number of values are the same (i.e. All INTs) - if (splitVals?.Length == intVals?.Length) - return intVals; - - return null; + return false; } - internal bool UpdatePropertyDataDto(PropertyDataDto propData, ValueListConfiguration config, bool isMultiple) + // map ids to values + var values = new List(); + var canConvert = true; + + foreach (var id in ids) { - //Get the INT ids stored for this property/drop down - int[]? ids = null; - if (!propData.VarcharValue.IsNullOrWhiteSpace()) + ValueListConfiguration.ValueListItem? val = config.Items.FirstOrDefault(x => x.Id == id); + if (val?.Value != null) { - ids = ConvertStringValues(propData.VarcharValue); - } - else if (!propData.TextValue.IsNullOrWhiteSpace()) - { - ids = ConvertStringValues(propData.TextValue); - } - else if (propData.IntegerValue.HasValue) - { - ids = new[] { propData.IntegerValue.Value }; + values.Add(val.Value); + continue; } - // if there are INT ids, convert them to values based on the configuration - if (ids == null || ids.Length <= 0) return false; - - // map ids to values - var values = new List(); - var canConvert = true; - - foreach (var id in ids) - { - var val = config.Items.FirstOrDefault(x => x.Id == id); - if (val?.Value != null) - { - values.Add(val.Value); - continue; - } - - Logger.LogWarning("Could not find PropertyData {PropertyDataId} value '{PropertyValue}' in the datatype configuration: {Values}.", - propData.Id, id, string.Join(", ", config.Items.Select(x => x.Id + ":" + x.Value))); - canConvert = false; - } - - if (!canConvert) return false; - - propData.VarcharValue = isMultiple ? JsonConvert.SerializeObject(values) : values[0]; - propData.TextValue = null; - propData.IntegerValue = null; - return true; + Logger.LogWarning( + "Could not find PropertyData {PropertyDataId} value '{PropertyValue}' in the datatype configuration: {Values}.", + propData.Id, id, string.Join(", ", config.Items.Select(x => x.Id + ":" + x.Value))); + canConvert = false; } - // dummy editor for deserialization - protected class ValueListConfigurationEditor : ConfigurationEditor + if (!canConvert) + { + return false; + } + + propData.VarcharValue = isMultiple ? JsonConvert.SerializeObject(values) : values[0]; + propData.TextValue = null; + propData.IntegerValue = null; + return true; + } + + protected int[]? ConvertStringValues(string? val) + { + var splitVals = val?.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + + var intVals = splitVals? + .Select(x => + int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i) ? i : int.MinValue) + .Where(x => x != int.MinValue) + .ToArray(); + + // only return if the number of values are the same (i.e. All INTs) + if (splitVals?.Length == intVals?.Length) + { + return intVals; + } + + return null; + } + + // dummy editor for deserialization + protected class ValueListConfigurationEditor : ConfigurationEditor + { + public ValueListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public ValueListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs index f7114fb0bd..ab9b01a3b2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RadioAndCheckboxPropertyEditorsMigration.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -13,101 +11,114 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigrationBase { - public class RadioAndCheckboxPropertyEditorsMigration : PropertyEditorsMigrationBase + private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + public RadioAndCheckboxPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) + : this(context, ioHelper, configurationEditorJsonSerializer, + StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer; - private readonly IEditorConfigurationParser _editorConfigurationParser; + } - public RadioAndCheckboxPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer) - : this(context, ioHelper, configurationEditorJsonSerializer, StaticServiceProvider.Instance.GetRequiredService()) + public RadioAndCheckboxPropertyEditorsMigration( + IMigrationContext context, + IIOHelper ioHelper, + IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(context) + { + _ioHelper = ioHelper; + _configurationEditorJsonSerializer = configurationEditorJsonSerializer; + _editorConfigurationParser = editorConfigurationParser; + } + + protected override void Migrate() + { + var refreshCache = false; + + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.RadioButtonList), false); + refreshCache |= Migrate(GetDataTypes(Constants.PropertyEditors.Aliases.CheckBoxList), true); + + // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), + // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table + if (refreshCache) { - } - - public RadioAndCheckboxPropertyEditorsMigration( - IMigrationContext context, - IIOHelper ioHelper, - IConfigurationEditorJsonSerializer configurationEditorJsonSerializer, - IEditorConfigurationParser editorConfigurationParser) - : base(context) - { - _ioHelper = ioHelper; - _configurationEditorJsonSerializer = configurationEditorJsonSerializer; - _editorConfigurationParser = editorConfigurationParser; - } - - protected override void Migrate() - { - var refreshCache = false; - - refreshCache |= Migrate(GetDataTypes(Cms.Core.Constants.PropertyEditors.Aliases.RadioButtonList), false); - refreshCache |= Migrate(GetDataTypes(Cms.Core.Constants.PropertyEditors.Aliases.CheckBoxList), true); - - // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), - // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - Context.AddPostMigration(); - } - - private bool Migrate(IEnumerable dataTypes, bool isMultiple) - { - var refreshCache = false; - ConfigurationEditor? configurationEditor = null; - - foreach (var dataType in dataTypes) - { - ValueListConfiguration config; - - if (dataType.Configuration.IsNullOrWhiteSpace()) - continue; - - // parse configuration, and update everything accordingly - if (configurationEditor == null) - configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); - try - { - config = (ValueListConfiguration) configurationEditor.FromDatabase(dataType.Configuration, _configurationEditorJsonSerializer); - } - catch (Exception ex) - { - Logger.LogError( - ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", - dataType.Configuration); - - continue; - } - - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, isMultiple)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) - Database.Update(propertyDataDto); - - UpdateDataType(dataType); - refreshCache = true; - } - - return refreshCache; - } - - private void UpdateDataType(DataTypeDto dataType) - { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); - Database.Update(dataType); + Context.AddPostMigration(); } } + + private bool Migrate(IEnumerable dataTypes, bool isMultiple) + { + var refreshCache = false; + ConfigurationEditor? configurationEditor = null; + + foreach (DataTypeDto dataType in dataTypes) + { + ValueListConfiguration config; + + if (dataType.Configuration.IsNullOrWhiteSpace()) + { + continue; + } + + // parse configuration, and update everything accordingly + if (configurationEditor == null) + { + configurationEditor = new ValueListConfigurationEditor(_ioHelper, _editorConfigurationParser); + } + + try + { + config = (ValueListConfiguration)configurationEditor.FromDatabase( + dataType.Configuration, + _configurationEditorJsonSerializer); + } + catch (Exception ex) + { + Logger.LogError( + ex, "Invalid configuration: \"{Configuration}\", cannot convert editor.", + dataType.Configuration); + + continue; + } + + // get property data dtos + List? propertyDataDtos = Database.Fetch(Sql() + .Select() + .From() + .InnerJoin() + .On((pt, pd) => pt.Id == pd.PropertyTypeId) + .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) + .Where(x => x.DataTypeId == dataType.NodeId)); + + // update dtos + IEnumerable updatedDtos = + propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, isMultiple)); + + // persist changes + foreach (PropertyDataDto? propertyDataDto in updatedDtos) + { + Database.Update(propertyDataDto); + } + + UpdateDataType(dataType); + refreshCache = true; + } + + return refreshCache; + } + + private void UpdateDataType(DataTypeDto dataType) + { + dataType.DbType = ValueStorageType.Nvarchar.ToString(); + Database.Update(dataType); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs index 005a2ef464..f8d731e166 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs @@ -1,39 +1,55 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RefactorMacroColumns : MigrationBase { - public class RefactorMacroColumns : MigrationBase + public RefactorMacroColumns(IMigrationContext context) + : base(context) { - public RefactorMacroColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.Macro, "macroXSLT")) { - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.Macro, "macroXSLT")) + // special trick to add the column without constraints and return the sql to add them later + AddColumn("macroType", out IEnumerable sqls1); + AddColumn("macroSource", out IEnumerable sqls2); + + // populate the new columns with legacy data + // when the macro type is PartialView, it corresponds to 7, else it is 4 for Unknown + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = 4").Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = 4 WHERE macroXSLT != '' AND macroXSLT IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = 4 WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = 4 WHERE macroScriptType != '' AND macroScriptType IS NOT NULL") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = 7 WHERE macroPython != '' AND macroPython IS NOT NULL") + .Do(); + + // now apply constraints (NOT NULL) to new table + foreach (var sql in sqls1) { - //special trick to add the column without constraints and return the sql to add them later - AddColumn("macroType", out var sqls1); - AddColumn("macroSource", out var sqls2); - - //populate the new columns with legacy data - //when the macro type is PartialView, it corresponds to 7, else it is 4 for Unknown - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = 4").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = 4 WHERE macroXSLT != '' AND macroXSLT IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = 4 WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = 4 WHERE macroScriptType != '' AND macroScriptType IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = 7 WHERE macroPython != '' AND macroPython IS NOT NULL").Do(); - - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls1) Execute.Sql(sql).Do(); - foreach (var sql in sqls2) Execute.Sql(sql).Do(); - - //now remove these old columns - Delete.Column("macroXSLT").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroScriptAssembly").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroScriptType").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); - Delete.Column("macroPython").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Macro).Do(); + Execute.Sql(sql).Do(); } + + foreach (var sql in sqls2) + { + Execute.Sql(sql).Do(); + } + + // now remove these old columns + Delete.Column("macroXSLT").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroScriptAssembly").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroScriptType").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); + Delete.Column("macroPython").FromTable(Constants.DatabaseSchema.Tables.Macro).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs index 1ff19e0698..500db8a4bc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs @@ -1,80 +1,99 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RefactorVariantsModel : MigrationBase { - public class RefactorVariantsModel : MigrationBase + public RefactorVariantsModel(IMigrationContext context) + : base(context) { - public RefactorVariantsModel(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "edited")) - Delete.Column("edited").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); - - - // add available column - AddColumn("available", out var sqls); - - // so far, only those cultures that were available had records in the table - Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do(); - - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - - // add published column - AddColumn("published", out sqls); - - // make it false by default - Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do(); - - // now figure out whether these available cultures are published, too - var getPublished = Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.LanguageId) - .From() - .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId) - .InnerJoin().On((cv, dv) => cv.Id == dv.Id && dv.Published) - .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); - - foreach (var dto in Database.Fetch(getPublished)) - Database.Execute(Sql() - .Update(u => u.Set(x => x.Published, true)) - .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); - - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - // so far, it was kinda impossible to make a culture unavailable again, - // so we *should* not have anything published but not available - ignore - - - // add name column - AddColumn("name"); - - // so far, every record in the table mapped to an available culture - var getNames = Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.LanguageId, x => x.Name) - .From() - .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId && cv.Current) - .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId); - - foreach (var dto in Database.Fetch(getNames)) - Database.Execute(Sql() - .Update(u => u.Set(x => x.Name, dto.Name)) - .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); - } - - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class TempDto - { - public int NodeId { get; set; } - public int LanguageId { get; set; } - public string? Name { get; set; } - } - // ReSharper restore UnusedAutoPropertyAccessor.Local } + + protected override void Migrate() + { + if (ColumnExists(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation, "edited")) + { + Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do(); + } + + // add available column + AddColumn("available", out IEnumerable sqls); + + // so far, only those cultures that were available had records in the table + Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do(); + + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // add published column + AddColumn("published", out sqls); + + // make it false by default + Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do(); + + // now figure out whether these available cultures are published, too + Sql getPublished = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId) + .From() + .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId) + .InnerJoin() + .On((cv, dv) => cv.Id == dv.Id && dv.Published) + .InnerJoin() + .On((cv, ccv) => cv.Id == ccv.VersionId); + + foreach (TempDto? dto in Database.Fetch(getPublished)) + { + Database.Execute(Sql() + .Update(u => u.Set(x => x.Published, true)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + } + + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // so far, it was kinda impossible to make a culture unavailable again, + // so we *should* not have anything published but not available - ignore + + // add name column + AddColumn("name"); + + // so far, every record in the table mapped to an available culture + Sql getNames = Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.LanguageId, x => x.Name) + .From() + .InnerJoin() + .On((node, cv) => node.NodeId == cv.NodeId && cv.Current) + .InnerJoin() + .On((cv, ccv) => cv.Id == ccv.VersionId); + + foreach (TempDto? dto in Database.Fetch(getNames)) + { + Database.Execute(Sql() + .Update(u => u.Set(x => x.Name, dto.Name)) + .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId)); + } + } + + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TempDto + { + public int NodeId { get; set; } + + public int LanguageId { get; set; } + + public string? Name { get; set; } + } + + // ReSharper restore UnusedAutoPropertyAccessor.Local } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs index c3fdf2d0fc..a638f17dc4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameLabelAndRichTextPropertyEditorAliases.cs @@ -1,41 +1,39 @@ -using System.Collections.Generic; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameLabelAndRichTextPropertyEditorAliases : MigrationBase { - public class RenameLabelAndRichTextPropertyEditorAliases : MigrationBase + public RenameLabelAndRichTextPropertyEditorAliases(IMigrationContext context) + : base(context) { - public RenameLabelAndRichTextPropertyEditorAliases(IMigrationContext context) - : base(context) + } + + protected override void Migrate() + { + MigratePropertyEditorAlias("Umbraco.TinyMCEv3", Constants.PropertyEditors.Aliases.TinyMce); + MigratePropertyEditorAlias("Umbraco.NoEdit", Constants.PropertyEditors.Aliases.Label); + } + + private void MigratePropertyEditorAlias(string oldAlias, string newAlias) + { + List dataTypes = GetDataTypes(oldAlias); + + foreach (DataTypeDto dataType in dataTypes) { + dataType.EditorAlias = newAlias; + Database.Update(dataType); } + } - protected override void Migrate() - { - MigratePropertyEditorAlias("Umbraco.TinyMCEv3", Cms.Core.Constants.PropertyEditors.Aliases.TinyMce); - MigratePropertyEditorAlias("Umbraco.NoEdit", Cms.Core.Constants.PropertyEditors.Aliases.Label); - } - - private void MigratePropertyEditorAlias(string oldAlias, string newAlias) - { - var dataTypes = GetDataTypes(oldAlias); - - foreach (var dataType in dataTypes) - { - dataType.EditorAlias = newAlias; - Database.Update(dataType); - } - } - - private List GetDataTypes(string editorAlias) - { - var dataTypes = Database.Fetch(Sql() - .Select() - .From() - .Where(x => x.EditorAlias == editorAlias)); - return dataTypes; - } - + private List GetDataTypes(string editorAlias) + { + List? dataTypes = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.EditorAlias == editorAlias)); + return dataTypes; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs index fa88f17422..a6fe5c895b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameMediaVersionTable.cs @@ -1,45 +1,48 @@ -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameMediaVersionTable : MigrationBase { - public class RenameMediaVersionTable : MigrationBase + public RenameMediaVersionTable(IMigrationContext context) + : base(context) { - public RenameMediaVersionTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Rename.Table("cmsMedia").To(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); + protected override void Migrate() + { + Rename.Table("cmsMedia").To(Constants.DatabaseSchema.Tables.MediaVersion).Do(); - // that is not supported on SqlCE - //Rename.Column("versionId").OnTable(Constants.DatabaseSchema.Tables.MediaVersion).To("id").Do(); + // that is not supported on SqlCE + // Rename.Column("versionId").OnTable(Constants.DatabaseSchema.Tables.MediaVersion).To("id").Do(); + AddColumn("id", out IEnumerable sqls); - AddColumn("id", out var sqls); - - Database.Execute($@"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id -FROM {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} m + Database.Execute($@"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET id=v.id +FROM {Constants.DatabaseSchema.Tables.MediaVersion} m JOIN cmsContentVersion v on m.versionId = v.versionId JOIN umbracoNode n on v.contentId=n.id -WHERE n.nodeObjectType='{Cms.Core.Constants.ObjectTypes.Media}'"); +WHERE n.nodeObjectType='{Constants.ObjectTypes.Media}'"); - foreach (var sql in sqls) - Execute.Sql(sql).Do(); - - AddColumn("path", out sqls); - - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion} SET path=mediaPath").Do(); - - foreach (var sql in sqls) - Execute.Sql(sql).Do(); - - // we had to run sqls to get the NULL constraints, but we need to get rid of most - Delete.KeysAndIndexes(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - - Delete.Column("mediaPath").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - Delete.Column("versionId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); - Delete.Column("nodeId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion).Do(); + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); } + + AddColumn("path", out sqls); + + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.MediaVersion} SET path=mediaPath").Do(); + + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // we had to run sqls to get the NULL constraints, but we need to get rid of most + Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + + Delete.Column("mediaPath").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("versionId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); + Delete.Column("nodeId").FromTable(Constants.DatabaseSchema.Tables.MediaVersion).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs index 8bb2a8c14c..8611128458 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/RenameUmbracoDomainsTable.cs @@ -1,14 +1,13 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class RenameUmbracoDomainsTable : MigrationBase - { - public RenameUmbracoDomainsTable(IMigrationContext context) - : base(context) - { } +using Umbraco.Cms.Core; - protected override void Migrate() - { - Rename.Table("umbracoDomains").To(Cms.Core.Constants.DatabaseSchema.Tables.Domain).Do(); - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class RenameUmbracoDomainsTable : MigrationBase +{ + public RenameUmbracoDomainsTable(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() => Rename.Table("umbracoDomains").To(Constants.DatabaseSchema.Tables.Domain).Do(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs index 4daab69962..135e562fde 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/SuperZero.cs @@ -1,20 +1,26 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class SuperZero : MigrationBase { - public class SuperZero : MigrationBase + public SuperZero(IMigrationContext context) + : base(context) { - public SuperZero(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + var exists = Database.Fetch("select id from umbracoUser where id=-1;").Count > 0; + if (exists) { - var exists = Database.Fetch("select id from umbracoUser where id=-1;").Count > 0; - if (exists) return; + return; + } - Database.Execute("update umbracoUser set userLogin = userLogin + '__' where id=0"); + Database.Execute("update umbracoUser set userLogin = userLogin + '__' where id=0"); - Database.Execute("set identity_insert umbracoUser on;"); - Database.Execute(@" + Database.Execute("set identity_insert umbracoUser on;"); + Database.Execute(@" insert into umbracoUser (id, userDisabled, userNoConsole, userName, userLogin, userPassword, passwordConfig, userEmail, userLanguage, securityStampToken, failedLoginAttempts, lastLockoutDate, @@ -27,14 +33,13 @@ lastPasswordChangeDate, lastLoginDate, emailConfirmedDate, invitedDate, createDate, updateDate, avatar, tourData from umbracoUser where id=0;"); - Database.Execute("set identity_insert umbracoUser off;"); + Database.Execute("set identity_insert umbracoUser off;"); - Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); - Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); - Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); - Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); - Database.Execute($"update {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); - Database.Execute("delete from umbracoUser where id=0;"); - } + Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); + Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); + Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); + Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); + Database.Execute($"update {Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); + Database.Execute("delete from umbracoUser where id=0;"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs index 531e7a06cc..013375352e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -1,58 +1,57 @@ -using System; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TablesForScheduledPublishing : MigrationBase { - public class TablesForScheduledPublishing : MigrationBase + public TablesForScheduledPublishing(IMigrationContext context) + : base(context) { - public TablesForScheduledPublishing(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // Get anything currently scheduled + Sql? releaseSql = new Sql() + .Select("nodeId", "releaseDate") + .From("umbracoDocument") + .Where("releaseDate IS NOT NULL"); + Dictionary? releases = Database.Dictionary(releaseSql); + + Sql? expireSql = new Sql() + .Select("nodeId", "expireDate") + .From("umbracoDocument") + .Where("expireDate IS NOT NULL"); + Dictionary? expires = Database.Dictionary(expireSql); + + // drop old cols + Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); + Delete.Column("expireDate").FromTable("umbracoDocument").Do(); + + // add new table + Create.Table(true).Do(); + + // migrate the schedule + foreach (KeyValuePair s in releases) { - //Get anything currently scheduled - var releaseSql = new Sql() - .Select("nodeId", "releaseDate") - .From("umbracoDocument") - .Where("releaseDate IS NOT NULL"); - var releases = Database.Dictionary (releaseSql); + DateTime date = s.Value; + var action = ContentScheduleAction.Release.ToString(); - var expireSql = new Sql() - .Select("nodeId", "expireDate") - .From("umbracoDocument") - .Where("expireDate IS NOT NULL"); - var expires = Database.Dictionary(expireSql); + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date, action }) + .Do(); + } + foreach (KeyValuePair s in expires) + { + DateTime date = s.Value; + var action = ContentScheduleAction.Expire.ToString(); - //drop old cols - Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); - Delete.Column("expireDate").FromTable("umbracoDocument").Do(); - //add new table - Create.Table(true).Do(); - - //migrate the schedule - foreach(var s in releases) - { - var date = s.Value; - var action = ContentScheduleAction.Release.ToString(); - - Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) - .Do(); - } - - foreach (var s in expires) - { - var date = s.Value; - var action = ContentScheduleAction.Expire.ToString(); - - Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) - .Do(); - } + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date, action }) + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs index 35c32bddb9..2f2ac746ab 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigration.cs @@ -1,21 +1,22 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TagsMigration : MigrationBase { - public class TagsMigration : MigrationBase + public TagsMigration(IMigrationContext context) + : base(context) { - public TagsMigration(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // alter columns => non-null - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "group"); - AlterColumn(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "tag"); + protected override void Migrate() + { + // alter columns => non-null + AlterColumn(Constants.DatabaseSchema.Tables.Tag, "group"); + AlterColumn(Constants.DatabaseSchema.Tables.Tag, "tag"); - // kill unused parentId column - Delete.Column("ParentId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Tag).Do(); - } + // kill unused parentId column + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs index 63ffd563a9..eaa745a780 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs @@ -1,16 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 -{ - public class TagsMigrationFix : MigrationBase - { - public TagsMigrationFix(IMigrationContext context) - : base(context) - { } +using Umbraco.Cms.Core; - protected override void Migrate() +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class TagsMigrationFix : MigrationBase +{ + public TagsMigrationFix(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + // kill unused parentId column, if it still exists + if (ColumnExists(Constants.DatabaseSchema.Tables.Tag, "ParentId")) { - // kill unused parentId column, if it still exists - if (ColumnExists(Cms.Core.Constants.DatabaseSchema.Tables.Tag, "ParentId")) - Delete.Column("ParentId").FromTable(Cms.Core.Constants.DatabaseSchema.Tables.Tag).Do(); + Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs index e3251dc6ed..557f658691 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdateDefaultMandatoryLanguage.cs @@ -1,48 +1,54 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class UpdateDefaultMandatoryLanguage : MigrationBase { - public class UpdateDefaultMandatoryLanguage : MigrationBase + public UpdateDefaultMandatoryLanguage(IMigrationContext context) + : base(context) { - public UpdateDefaultMandatoryLanguage(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // add the new languages lock object + AddLockObjects.EnsureLockObject(Database, Constants.Locks.Languages, "Languages"); + + // get all existing languages + Sql selectDtos = Sql() + .Select() + .From(); + + List? dtos = Database.Fetch(selectDtos); + + // get the id of the language which is already the default one, if any, + // else get the lowest language id, which will become the default language + var defaultId = int.MaxValue; + foreach (LanguageDto? dto in dtos) { - // add the new languages lock object - AddLockObjects.EnsureLockObject(Database, Cms.Core.Constants.Locks.Languages, "Languages"); - - // get all existing languages - var selectDtos = Sql() - .Select() - .From(); - - var dtos = Database.Fetch(selectDtos); - - // get the id of the language which is already the default one, if any, - // else get the lowest language id, which will become the default language - var defaultId = int.MaxValue; - foreach (var dto in dtos) + if (dto.IsDefault) { - if (dto.IsDefault) - { - defaultId = dto.Id; - break; - } - - if (dto.Id < defaultId) defaultId = dto.Id; + defaultId = dto.Id; + break; } - // update, so that language with that id is now default and mandatory - var updateDefault = Sql() - .Update(u => u - .Set(x => x.IsDefault, true) - .Set(x => x.IsMandatory, true)) - .Where(x => x.Id == defaultId); - - Database.Execute(updateDefault); + if (dto.Id < defaultId) + { + defaultId = dto.Id; + } } + + // update, so that language with that id is now default and mandatory + Sql updateDefault = Sql() + .Update(u => u + .Set(x => x.IsDefault, true) + .Set(x => x.IsMandatory, true)) + .Where(x => x.Id == defaultId); + + Database.Execute(updateDefault); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs index 7fe50b2159..18d55aa1e6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UpdatePickerIntegerValuesToUdi.cs @@ -1,110 +1,120 @@ -using System; using System.Globalization; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco; using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class UpdatePickerIntegerValuesToUdi : MigrationBase { - public class UpdatePickerIntegerValuesToUdi : MigrationBase + public UpdatePickerIntegerValuesToUdi(IMigrationContext context) + : base(context) { - public UpdatePickerIntegerValuesToUdi(IMigrationContext context) : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + Sql sqlDataTypes = Sql() + .Select() + .From() + .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.ContentPicker + || x.EditorAlias == Constants.PropertyEditors.Aliases.MediaPicker + || x.EditorAlias == Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + + var dataTypes = Database.Fetch(sqlDataTypes).ToList(); + + foreach (DataTypeDto? datatype in dataTypes.Where(x => !x.Configuration.IsNullOrWhiteSpace())) { - var sqlDataTypes = Sql() - .Select() - .From() - .Where(x => x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - || x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - - var dataTypes = Database.Fetch(sqlDataTypes).ToList(); - - foreach (var datatype in dataTypes.Where(x => !x.Configuration.IsNullOrWhiteSpace())) + switch (datatype.EditorAlias) { - switch (datatype.EditorAlias) + case Constants.PropertyEditors.Aliases.ContentPicker: + case Constants.PropertyEditors.Aliases.MediaPicker: { - case Cms.Core.Constants.PropertyEditors.Aliases.ContentPicker: - case Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker: + JObject? config = JsonConvert.DeserializeObject(datatype.Configuration!); + var startNodeId = config!.Value("startNodeId"); + if (!startNodeId.IsNullOrWhiteSpace() && int.TryParse(startNodeId, NumberStyles.Integer, + CultureInfo.InvariantCulture, out var intStartNode)) + { + Guid? guid = intStartNode <= 0 + ? null + : Context.Database.ExecuteScalar( + Sql().Select(x => x.UniqueId).From() + .Where(x => x.NodeId == intStartNode)); + if (guid.HasValue) { - var config = JsonConvert.DeserializeObject(datatype.Configuration!); - var startNodeId = config!.Value("startNodeId"); - if (!startNodeId.IsNullOrWhiteSpace() && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intStartNode)) - { - var guid = intStartNode <= 0 - ? null - : Context.Database.ExecuteScalar( - Sql().Select(x => x.UniqueId).From().Where(x => x.NodeId == intStartNode)); - if (guid.HasValue) - { - var udi = new GuidUdi(datatype.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker - ? Cms.Core.Constants.UdiEntityType.Media - : Cms.Core.Constants.UdiEntityType.Document, guid.Value); - config!["startNodeId"] = new JValue(udi.ToString()); - } - else - config!.Remove("startNodeId"); + var udi = new GuidUdi( + datatype.EditorAlias == Constants.PropertyEditors.Aliases.MediaPicker + ? Constants.UdiEntityType.Media + : Constants.UdiEntityType.Document, guid.Value); + config!["startNodeId"] = new JValue(udi.ToString()); + } + else + { + config!.Remove("startNodeId"); + } - datatype.Configuration = JsonConvert.SerializeObject(config); - Database.Update(datatype); + datatype.Configuration = JsonConvert.SerializeObject(config); + Database.Update(datatype); + } + + break; + } + + case Constants.PropertyEditors.Aliases.MultiNodeTreePicker: + { + JObject? config = JsonConvert.DeserializeObject(datatype.Configuration!); + JObject? startNodeConfig = config!.Value("startNode"); + if (startNodeConfig != null) + { + var startNodeId = startNodeConfig.Value("id"); + var objectType = startNodeConfig.Value("type"); + if (!objectType.IsNullOrWhiteSpace() + && !startNodeId.IsNullOrWhiteSpace() + && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var intStartNode)) + { + Guid? guid = intStartNode <= 0 + ? null + : Context.Database.ExecuteScalar( + Sql().Select(x => x.UniqueId).From() + .Where(x => x.NodeId == intStartNode)); + + string? entityType = null; + switch (objectType?.ToLowerInvariant()) + { + case "content": + entityType = Constants.UdiEntityType.Document; + break; + case "media": + entityType = Constants.UdiEntityType.Media; + break; + case "member": + entityType = Constants.UdiEntityType.Member; + break; } - break; - } - case Cms.Core.Constants.PropertyEditors.Aliases.MultiNodeTreePicker: - { - var config = JsonConvert.DeserializeObject(datatype.Configuration!); - var startNodeConfig = config!.Value("startNode"); - if (startNodeConfig != null) + if (entityType != null && guid.HasValue) { - var startNodeId = startNodeConfig.Value("id"); - var objectType = startNodeConfig.Value("type"); - if (!objectType.IsNullOrWhiteSpace() - && !startNodeId.IsNullOrWhiteSpace() - && int.TryParse(startNodeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intStartNode)) - { - var guid = intStartNode <= 0 - ? null - : Context.Database.ExecuteScalar( - Sql().Select(x => x.UniqueId).From().Where(x => x.NodeId == intStartNode)); - - string? entityType = null; - switch (objectType?.ToLowerInvariant()) - { - case "content": - entityType = Cms.Core.Constants.UdiEntityType.Document; - break; - case "media": - entityType = Cms.Core.Constants.UdiEntityType.Media; - break; - case "member": - entityType = Cms.Core.Constants.UdiEntityType.Member; - break; - } - - if (entityType != null && guid.HasValue) - { - var udi = new GuidUdi(entityType, guid.Value); - startNodeConfig["id"] = new JValue(udi.ToString()); - } - else - startNodeConfig.Remove("id"); - - datatype.Configuration = JsonConvert.SerializeObject(config); - Database.Update(datatype); - } + var udi = new GuidUdi(entityType, guid.Value); + startNodeConfig["id"] = new JValue(udi.ToString()); + } + else + { + startNodeConfig.Remove("id"); } - break; + datatype.Configuration = JsonConvert.SerializeObject(config); + Database.Update(datatype); } + } + + break; } } - } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs index 03c3529f59..fa19ac284a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/UserForeignKeys.cs @@ -1,31 +1,41 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +/// +/// Creates/Updates non mandatory FK columns to the user table +/// +public class UserForeignKeys : MigrationBase { - /// - /// Creates/Updates non mandatory FK columns to the user table - /// - public class UserForeignKeys : MigrationBase + public UserForeignKeys(IMigrationContext context) + : base(context) { - public UserForeignKeys(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - // first allow NULL-able - Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable().Do(); - Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().Do(); - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().Do(); - Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().Do(); + protected override void Migrate() + { + // first allow NULL-able + Alter.Table(ContentVersionCultureVariationDto.TableName).AlterColumn("availableUserId").AsInt32().Nullable() + .Do(); + Alter.Table(ContentVersionDto.TableName).AlterColumn("userId").AsInt32().Nullable().Do(); + Alter.Table(Constants.DatabaseSchema.Tables.Log).AlterColumn("userId").AsInt32().Nullable().Do(); + Alter.Table(NodeDto.TableName).AlterColumn("nodeUser").AsInt32().Nullable().Do(); - // then we can update any non existing users to NULL - Execute.Sql($"UPDATE {ContentVersionCultureVariationDto.TableName} SET availableUserId = NULL WHERE availableUserId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {ContentVersionDto.TableName} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {Cms.Core.Constants.DatabaseSchema.Tables.Log} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})").Do(); - Execute.Sql($"UPDATE {NodeDto.TableName} SET nodeUser = NULL WHERE nodeUser NOT IN (SELECT id FROM {UserDto.TableName})").Do(); + // then we can update any non existing users to NULL + Execute.Sql( + $"UPDATE {ContentVersionCultureVariationDto.TableName} SET availableUserId = NULL WHERE availableUserId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {ContentVersionDto.TableName} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.Log} SET userId = NULL WHERE userId NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); + Execute.Sql( + $"UPDATE {NodeDto.TableName} SET nodeUser = NULL WHERE nodeUser NOT IN (SELECT id FROM {UserDto.TableName})") + .Do(); - // FKs will be created after migrations - } + // FKs will be created after migrations } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs index f0fbb63729..db41f70711 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs @@ -1,215 +1,266 @@ -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; + +public class VariantsMigration : MigrationBase { - public class VariantsMigration : MigrationBase + public VariantsMigration(IMigrationContext context) + : base(context) { - public VariantsMigration(IMigrationContext context) - : base(context) - { } + } - // notes - // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead + // notes + // do NOT use Rename.Column as it's borked on SQLCE - use ReplaceColumn instead + protected override void Migrate() + { + MigratePropertyData(); + CreatePropertyDataIndexes(); + MigrateContentAndPropertyTypes(); + MigrateContent(); + MigrateVersions(); - protected override void Migrate() + if (Database.Fetch( + $@"SELECT {Constants.DatabaseSchema.Tables.ContentVersion}.nodeId, COUNT({Constants.DatabaseSchema.Tables.ContentVersion}.id) +FROM {Constants.DatabaseSchema.Tables.ContentVersion} +JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} ON {Constants.DatabaseSchema.Tables.ContentVersion}.id={Constants.DatabaseSchema.Tables.DocumentVersion}.id +WHERE {Constants.DatabaseSchema.Tables.DocumentVersion}.published=1 +GROUP BY {Constants.DatabaseSchema.Tables.ContentVersion}.nodeId +HAVING COUNT({Constants.DatabaseSchema.Tables.ContentVersion}.id) > 1").Any()) { - MigratePropertyData(); - CreatePropertyDataIndexes(); - MigrateContentAndPropertyTypes(); - MigrateContent(); - MigrateVersions(); - - if (Database.Fetch($@"SELECT {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.nodeId, COUNT({Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id) -FROM {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} ON {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id={Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion}.id -WHERE {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion}.published=1 -GROUP BY {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.nodeId -HAVING COUNT({Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion}.id) > 1").Any()) - { - Debugger.Break(); - throw new Exception("Migration failed: duplicate 'published' document versions."); - } - - if (Database.Fetch($@"SELECT v1.nodeId, v1.id, COUNT(v2.id) -FROM {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} v1 -LEFT JOIN {Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion} v2 ON v1.nodeId=v2.nodeId AND v2.[current]=1 -GROUP BY v1.nodeId, v1.id -HAVING COUNT(v2.id) <> 1").Any()) - { - Debugger.Break(); - throw new Exception("Migration failed: missing or duplicate 'current' content versions."); - } + Debugger.Break(); + throw new Exception("Migration failed: duplicate 'published' document versions."); } - private void MigratePropertyData() + if (Database.Fetch($@"SELECT v1.nodeId, v1.id, COUNT(v2.id) +FROM {Constants.DatabaseSchema.Tables.ContentVersion} v1 +LEFT JOIN {Constants.DatabaseSchema.Tables.ContentVersion} v2 ON v1.nodeId=v2.nodeId AND v2.[current]=1 +GROUP BY v1.nodeId, v1.id +HAVING COUNT(v2.id) <> 1").Any()) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData)) - return; + Debugger.Break(); + throw new Exception("Migration failed: missing or duplicate 'current' content versions."); + } + } - // add columns - if (!ColumnExists(PreTables.PropertyData, "languageId")) - AddColumn(PreTables.PropertyData, "languageId"); - if (!ColumnExists(PreTables.PropertyData, "segment")) - AddColumn(PreTables.PropertyData, "segment"); + private void MigratePropertyData() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.PropertyData)) + { + return; + } - // rename columns - if (ColumnExists(PreTables.PropertyData, "dataNtext")) - ReplaceColumn(PreTables.PropertyData, "dataNtext", "textValue"); - if (ColumnExists(PreTables.PropertyData, "dataNvarchar")) - ReplaceColumn(PreTables.PropertyData, "dataNvarchar", "varcharValue"); - if (ColumnExists(PreTables.PropertyData, "dataDecimal")) - ReplaceColumn(PreTables.PropertyData, "dataDecimal", "decimalValue"); - if (ColumnExists(PreTables.PropertyData, "dataInt")) - ReplaceColumn(PreTables.PropertyData, "dataInt", "intValue"); - if (ColumnExists(PreTables.PropertyData, "dataDate")) - ReplaceColumn(PreTables.PropertyData, "dataDate", "dateValue"); + // add columns + if (!ColumnExists(PreTables.PropertyData, "languageId")) + { + AddColumn(PreTables.PropertyData, "languageId"); + } - // transform column versionId from guid to integer (contentVersion.id) - if (ColumnType(PreTables.PropertyData, "versionId") == "uniqueidentifier") - { - Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); + if (!ColumnExists(PreTables.PropertyData, "segment")) + { + AddColumn(PreTables.PropertyData, "segment"); + } - Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id + // rename columns + if (ColumnExists(PreTables.PropertyData, "dataNtext")) + { + ReplaceColumn(PreTables.PropertyData, "dataNtext", "textValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataNvarchar")) + { + ReplaceColumn(PreTables.PropertyData, "dataNvarchar", "varcharValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataDecimal")) + { + ReplaceColumn(PreTables.PropertyData, "dataDecimal", "decimalValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataInt")) + { + ReplaceColumn(PreTables.PropertyData, "dataInt", "intValue"); + } + + if (ColumnExists(PreTables.PropertyData, "dataDate")) + { + ReplaceColumn(PreTables.PropertyData, "dataDate", "dateValue"); + } + + // transform column versionId from guid to integer (contentVersion.id) + if (ColumnType(PreTables.PropertyData, "versionId") == "uniqueidentifier") + { + Alter.Table(PreTables.PropertyData).AddColumn("versionId2").AsInt32().Nullable().Do(); + + Database.Execute($@"UPDATE {PreTables.PropertyData} SET versionId2={PreTables.ContentVersion}.id FROM {PreTables.ContentVersion} INNER JOIN {PreTables.PropertyData} ON {PreTables.ContentVersion}.versionId = {PreTables.PropertyData}.versionId"); - Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); - ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); - } - - // drop column - if (ColumnExists(PreTables.PropertyData, "contentNodeId")) - Delete.Column("contentNodeId").FromTable(PreTables.PropertyData).Do(); - - // rename table - Rename.Table(PreTables.PropertyData).To(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData).Do(); + Delete.Column("versionId").FromTable(PreTables.PropertyData).Do(); + ReplaceColumn(PreTables.PropertyData, "versionId2", "versionId"); } - private void CreatePropertyDataIndexes() + // drop column + if (ColumnExists(PreTables.PropertyData, "contentNodeId")) { - // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. - // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created - var tableDefinition = DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); - Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); - Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) - .WithOptions().Unique() - .WithOptions().NonClustered() - .OnColumn("versionId").Ascending() - .OnColumn("propertyTypeId").Ascending() - .OnColumn("languageId").Ascending() - .OnColumn("segment").Ascending() - .Do(); + Delete.Column("contentNodeId").FromTable(PreTables.PropertyData).Do(); } - private void MigrateContentAndPropertyTypes() + // rename table + Rename.Table(PreTables.PropertyData).To(Constants.DatabaseSchema.Tables.PropertyData).Do(); + } + + private void CreatePropertyDataIndexes() + { + // Creates a temporary index on umbracoPropertyData to speed up other migrations which update property values. + // It will be removed in CreateKeysAndIndexes before the normal indexes for the table are created + TableDefinition tableDefinition = DefinitionFactory.GetTableDefinition(typeof(PropertyDataDto), SqlSyntax); + Execute.Sql(SqlSyntax.FormatPrimaryKey(tableDefinition)).Do(); + Create.Index("IX_umbracoPropertyData_Temp").OnTable(PropertyDataDto.TableName) + .WithOptions().Unique() + .WithOptions().NonClustered() + .OnColumn("versionId").Ascending() + .OnColumn("propertyTypeId").Ascending() + .OnColumn("languageId").Ascending() + .OnColumn("segment").Ascending() + .Do(); + } + + private void MigrateContentAndPropertyTypes() + { + if (!ColumnExists(PreTables.ContentType, "variations")) { - if (!ColumnExists(PreTables.ContentType, "variations")) - AddColumn(PreTables.ContentType, "variations"); - if (!ColumnExists(PreTables.PropertyType, "variations")) - AddColumn(PreTables.PropertyType, "variations"); + AddColumn(PreTables.ContentType, "variations"); } - private void MigrateContent() + if (!ColumnExists(PreTables.PropertyType, "variations")) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.Content)) - return; + AddColumn(PreTables.PropertyType, "variations"); + } + } - // rename columns - if (ColumnExists(PreTables.Content, "contentType")) - ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); - - // drop columns - if (ColumnExists(PreTables.Content, "pk")) - Delete.Column("pk").FromTable(PreTables.Content).Do(); - - // rename table - Rename.Table(PreTables.Content).To(Cms.Core.Constants.DatabaseSchema.Tables.Content).Do(); + private void MigrateContent() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.Content)) + { + return; } - private void MigrateVersions() + // rename columns + if (ColumnExists(PreTables.Content, "contentType")) { - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion)) - return; + ReplaceColumn(PreTables.Content, "contentType", "contentTypeId"); + } - // if the table already exists, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)) - return; + // drop columns + if (ColumnExists(PreTables.Content, "pk")) + { + Delete.Column("pk").FromTable(PreTables.Content).Do(); + } - // if the table has already been renamed, we're done - if (TableExists(Cms.Core.Constants.DatabaseSchema.Tables.Document)) - return; + // rename table + Rename.Table(PreTables.Content).To(Constants.DatabaseSchema.Tables.Content).Do(); + } - // do it all at once + private void MigrateVersions() + { + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.ContentVersion)) + { + return; + } - // add contentVersion columns - if (!ColumnExists(PreTables.ContentVersion, "text")) - AddColumn(PreTables.ContentVersion, "text"); - if (!ColumnExists(PreTables.ContentVersion, "current")) + // if the table already exists, we're done + if (TableExists(Constants.DatabaseSchema.Tables.DocumentVersion)) + { + return; + } + + // if the table has already been renamed, we're done + if (TableExists(Constants.DatabaseSchema.Tables.Document)) + { + return; + } + + // do it all at once + + // add contentVersion columns + if (!ColumnExists(PreTables.ContentVersion, "text")) + { + AddColumn(PreTables.ContentVersion, "text"); + } + + if (!ColumnExists(PreTables.ContentVersion, "current")) + { + AddColumn(PreTables.ContentVersion, "current", out IEnumerable sqls); + Database.Execute( + $@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0"); + foreach (var sql in sqls) { - AddColumn(PreTables.ContentVersion, "current", out var sqls); - Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET {SqlSyntax.GetQuotedColumnName("current")}=0"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } - if (!ColumnExists(PreTables.ContentVersion, "userId")) + } + + if (!ColumnExists(PreTables.ContentVersion, "userId")) + { + AddColumn(PreTables.ContentVersion, "userId", out IEnumerable sqls); + Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET userId=0"); + foreach (var sql in sqls) { - AddColumn(PreTables.ContentVersion, "userId", out var sqls); - Database.Execute($@"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} SET userId=0"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } + } - // rename contentVersion contentId column - if (ColumnExists(PreTables.ContentVersion, "ContentId")) - ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); + // rename contentVersion contentId column + if (ColumnExists(PreTables.ContentVersion, "ContentId")) + { + ReplaceColumn(PreTables.ContentVersion, "ContentId", "nodeId"); + } - // populate contentVersion text, current and userId columns for documents - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser + // populate contentVersion text, current and userId columns for documents + Database.Execute( + $@"UPDATE {PreTables.ContentVersion} SET text=d.text, {SqlSyntax.GetQuotedColumnName("current")}=(d.newest & ~d.published), userId=d.documentUser FROM {PreTables.ContentVersion} v INNER JOIN {PreTables.Document} d ON d.versionId = v.versionId"); - - // populate contentVersion text and current columns for non-documents, userId is default - Database.Execute($@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 + // populate contentVersion text and current columns for non-documents, userId is default + Database.Execute( + $@"UPDATE {PreTables.ContentVersion} SET text=n.text, {SqlSyntax.GetQuotedColumnName("current")}=1, userId=0 FROM {PreTables.ContentVersion} cver -JOIN {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id +JOIN {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.Node)} n ON cver.nodeId=n.id WHERE cver.versionId NOT IN (SELECT versionId FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)})"); + // create table + Create.Table(true).Do(); - // create table - Create.Table(withoutKeysAndIndexes: true).Do(); - - // every document row becomes a document version - Database.Execute($@"INSERT INTO {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) + // every document row becomes a document version + Database.Execute( + $@"INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) SELECT cver.id, doc.templateId, doc.published FROM {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver JOIN {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId"); - - // need to add extra rows for where published=newest - // 'cos INSERT above has inserted the 'published' document version - // and v8 always has a 'edited' document version too - Database.Execute($@" + // need to add extra rows for where published=newest + // 'cos INSERT above has inserted the 'published' document version + // and v8 always has a 'edited' document version too + Database.Execute($@" INSERT INTO {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} (nodeId, versionId, versionDate, userId, {SqlSyntax.GetQuotedColumnName("current")}, text) SELECT doc.nodeId, NEWID(), doc.updateDate, doc.documentUser, 1, doc.text FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cver ON doc.nodeId=cver.nodeId AND doc.versionId=cver.versionId WHERE doc.newest=1 AND doc.published=1"); - Database.Execute($@" -INSERT INTO {SqlSyntax.GetQuotedTableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) + Database.Execute($@" +INSERT INTO {SqlSyntax.GetQuotedTableName(Constants.DatabaseSchema.Tables.DocumentVersion)} (id, templateId, published) SELECT cverNew.id, doc.templateId, 0 FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nodeId = cverNew.nodeId WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); - Database.Execute($@" + Database.Execute($@" INSERT INTO {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} (propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,versionId) SELECT propertytypeid,languageId,segment,textValue,varcharValue,decimalValue,intValue,dateValue,cverNew.id FROM {SqlSyntax.GetQuotedTableName(PreTables.Document)} doc @@ -218,127 +269,137 @@ JOIN {SqlSyntax.GetQuotedTableName(PreTables.ContentVersion)} cverNew ON doc.nod JOIN {SqlSyntax.GetQuotedTableName(PropertyDataDto.TableName)} pd ON pd.versionId=cver.id WHERE doc.newest=1 AND doc.published=1 AND cverNew.{SqlSyntax.GetQuotedColumnName("current")} = 1"); - - // reduce document to 1 row per content - Database.Execute($@"DELETE FROM {PreTables.Document} + // reduce document to 1 row per content + Database.Execute($@"DELETE FROM {PreTables.Document} WHERE versionId NOT IN (SELECT (versionId) FROM {PreTables.ContentVersion} WHERE {SqlSyntax.GetQuotedColumnName("current")} = 1) AND (published<>1 OR newest<>1)"); - // ensure that documents with a published version are marked as published - Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( -SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); + // ensure that documents with a published version are marked as published + Database.Execute($@"UPDATE {PreTables.Document} SET published=1 WHERE nodeId IN ( +SELECT nodeId FROM {PreTables.ContentVersion} cv INNER JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON dv.id = cv.id WHERE dv.published=1)"); - // drop some document columns - Delete.Column("text").FromTable(PreTables.Document).Do(); - Delete.Column("templateId").FromTable(PreTables.Document).Do(); - Delete.Column("documentUser").FromTable(PreTables.Document).Do(); - Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("updateDate").Do(); - Delete.Column("updateDate").FromTable(PreTables.Document).Do(); - Delete.Column("versionId").FromTable(PreTables.Document).Do(); - Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("newest").Do(); - Delete.Column("newest").FromTable(PreTables.Document).Do(); + // drop some document columns + Delete.Column("text").FromTable(PreTables.Document).Do(); + Delete.Column("templateId").FromTable(PreTables.Document).Do(); + Delete.Column("documentUser").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("updateDate").Do(); + Delete.Column("updateDate").FromTable(PreTables.Document).Do(); + Delete.Column("versionId").FromTable(PreTables.Document).Do(); + Delete.DefaultConstraint().OnTable(PreTables.Document).OnColumn("newest").Do(); + Delete.Column("newest").FromTable(PreTables.Document).Do(); - // add and populate edited column - if (!ColumnExists(PreTables.Document, "edited")) + // add and populate edited column + if (!ColumnExists(PreTables.Document, "edited")) + { + AddColumn(PreTables.Document, "edited", out IEnumerable sqls); + Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); + foreach (var sql in sqls) { - AddColumn(PreTables.Document, "edited", out var sqls); - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=~published"); - foreach (var sql in sqls) Database.Execute(sql); + Database.Execute(sql); } + } - // set 'edited' to true whenever a 'non-published' property data is != a published one - // cannot compare NTEXT values in TSQL - // cannot cast NTEXT to NVARCHAR(MAX) in SQLCE - // ... bah ... - var temp = Database.Fetch($@"SELECT n.id, + // set 'edited' to true whenever a 'non-published' property data is != a published one + // cannot compare NTEXT values in TSQL + // cannot cast NTEXT to NVARCHAR(MAX) in SQLCE + // ... bah ... + List? temp = Database.Fetch($@"SELECT n.id, v1.intValue intValue1, v1.decimalValue decimalValue1, v1.dateValue dateValue1, v1.varcharValue varcharValue1, v1.textValue textValue1, v2.intValue intValue2, v2.decimalValue decimalValue2, v2.dateValue dateValue2, v2.varcharValue varcharValue2, v2.textValue textValue2 -FROM {Cms.Core.Constants.DatabaseSchema.Tables.Node} n +FROM {Constants.DatabaseSchema.Tables.Node} n JOIN {PreTables.ContentVersion} cv1 ON n.id=cv1.nodeId AND cv1.{SqlSyntax.GetQuotedColumnName("current")}=1 -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v1 ON cv1.id=v1.versionId JOIN {PreTables.ContentVersion} cv2 ON n.id=cv2.nodeId -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 -JOIN {Cms.Core.Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId +JOIN {Constants.DatabaseSchema.Tables.DocumentVersion} dv ON cv2.id=dv.id AND dv.published=1 +JOIN {Constants.DatabaseSchema.Tables.PropertyData} v2 ON cv2.id=v2.versionId WHERE v1.propertyTypeId=v2.propertyTypeId AND (v1.languageId=v2.languageId OR (v1.languageId IS NULL AND v2.languageId IS NULL)) AND (v1.segment=v2.segment OR (v1.segment IS NULL AND v2.segment IS NULL))"); - var updatedIds = new HashSet(); - foreach (var t in temp) - if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) - if (updatedIds.Add((int)t.id)) - Database.Execute($"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", new { nodeId = t.id }); - - // drop more columns - Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); - - // rename tables - Rename.Table(PreTables.ContentVersion).To(Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion).Do(); - Rename.Table(PreTables.Document).To(Cms.Core.Constants.DatabaseSchema.Tables.Document).Do(); - } - - private static class PreTables + var updatedIds = new HashSet(); + foreach (dynamic t in temp) { - // ReSharper disable UnusedMember.Local - public const string Lock = "umbracoLock"; - public const string Log = "umbracoLog"; - - public const string Node = "umbracoNode"; - public const string NodeData = "cmsContentNu"; - public const string NodeXml = "cmsContentXml"; - public const string NodePreviewXml = "cmsPreviewXml"; - - public const string ContentType = "cmsContentType"; - public const string ContentChildType = "cmsContentTypeAllowedContentType"; - public const string DocumentType = "cmsDocumentType"; - public const string ElementTypeTree = "cmsContentType2ContentType"; - public const string DataType = "cmsDataType"; - public const string DataTypePreValue = "cmsDataTypePreValues"; - public const string Template = "cmsTemplate"; - - public const string Content = "cmsContent"; - public const string ContentVersion = "cmsContentVersion"; - public const string Document = "cmsDocument"; - - public const string PropertyType = "cmsPropertyType"; - public const string PropertyTypeGroup = "cmsPropertyTypeGroup"; - public const string PropertyData = "cmsPropertyData"; - - public const string RelationType = "umbracoRelationType"; - public const string Relation = "umbracoRelation"; - - public const string Domain = "umbracoDomains"; - public const string Language = "umbracoLanguage"; - public const string DictionaryEntry = "cmsDictionary"; - public const string DictionaryValue = "cmsLanguageText"; - - public const string User = "umbracoUser"; - public const string UserGroup = "umbracoUserGroup"; - public const string UserStartNode = "umbracoUserStartNode"; - public const string User2UserGroup = "umbracoUser2UserGroup"; - public const string User2NodeNotify = "umbracoUser2NodeNotify"; - public const string UserGroup2App = "umbracoUserGroup2App"; - public const string UserGroup2NodePermission = "umbracoUserGroup2NodePermission"; - public const string ExternalLogin = "umbracoExternalLogin"; - - public const string Macro = "cmsMacro"; - public const string MacroProperty = "cmsMacroProperty"; - - public const string Member = "cmsMember"; - public const string MemberType = "cmsMemberType"; - public const string Member2MemberGroup = "cmsMember2MemberGroup"; - - public const string Access = "umbracoAccess"; - public const string AccessRule = "umbracoAccessRule"; - public const string RedirectUrl = "umbracoRedirectUrl"; - - public const string CacheInstruction = "umbracoCacheInstruction"; - public const string Migration = "umbracoMigration"; - public const string Server = "umbracoServer"; - - public const string Tag = "cmsTags"; - public const string TagRelationship = "cmsTagRelationship"; - - // ReSharper restore UnusedMember.Local + if (t.intValue1 != t.intValue2 || t.decimalValue1 != t.decimalValue2 || t.dateValue1 != t.dateValue2 || + t.varcharValue1 != t.varcharValue2 || t.textValue1 != t.textValue2) + { + if (updatedIds.Add((int)t.id)) + { + Database.Execute( + $"UPDATE {SqlSyntax.GetQuotedTableName(PreTables.Document)} SET edited=1 WHERE nodeId=@nodeId", + new { nodeId = t.id }); + } + } } + + // drop more columns + Delete.Column("versionId").FromTable(PreTables.ContentVersion).Do(); + + // rename tables + Rename.Table(PreTables.ContentVersion).To(Constants.DatabaseSchema.Tables.ContentVersion).Do(); + Rename.Table(PreTables.Document).To(Constants.DatabaseSchema.Tables.Document).Do(); + } + + private static class PreTables + { + // ReSharper disable UnusedMember.Local + public const string Lock = "umbracoLock"; + public const string Log = "umbracoLog"; + + public const string Node = "umbracoNode"; + public const string NodeData = "cmsContentNu"; + public const string NodeXml = "cmsContentXml"; + public const string NodePreviewXml = "cmsPreviewXml"; + + public const string ContentType = "cmsContentType"; + public const string ContentChildType = "cmsContentTypeAllowedContentType"; + public const string DocumentType = "cmsDocumentType"; + public const string ElementTypeTree = "cmsContentType2ContentType"; + public const string DataType = "cmsDataType"; + public const string DataTypePreValue = "cmsDataTypePreValues"; + public const string Template = "cmsTemplate"; + + public const string Content = "cmsContent"; + public const string ContentVersion = "cmsContentVersion"; + public const string Document = "cmsDocument"; + + public const string PropertyType = "cmsPropertyType"; + public const string PropertyTypeGroup = "cmsPropertyTypeGroup"; + public const string PropertyData = "cmsPropertyData"; + + public const string RelationType = "umbracoRelationType"; + public const string Relation = "umbracoRelation"; + + public const string Domain = "umbracoDomains"; + public const string Language = "umbracoLanguage"; + public const string DictionaryEntry = "cmsDictionary"; + public const string DictionaryValue = "cmsLanguageText"; + + public const string User = "umbracoUser"; + public const string UserGroup = "umbracoUserGroup"; + public const string UserStartNode = "umbracoUserStartNode"; + public const string User2UserGroup = "umbracoUser2UserGroup"; + public const string User2NodeNotify = "umbracoUser2NodeNotify"; + public const string UserGroup2App = "umbracoUserGroup2App"; + public const string UserGroup2NodePermission = "umbracoUserGroup2NodePermission"; + public const string ExternalLogin = "umbracoExternalLogin"; + + public const string Macro = "cmsMacro"; + public const string MacroProperty = "cmsMacroProperty"; + + public const string Member = "cmsMember"; + public const string MemberType = "cmsMemberType"; + public const string Member2MemberGroup = "cmsMember2MemberGroup"; + + public const string Access = "umbracoAccess"; + public const string AccessRule = "umbracoAccessRule"; + public const string RedirectUrl = "umbracoRedirectUrl"; + + public const string CacheInstruction = "umbracoCacheInstruction"; + public const string Migration = "umbracoMigration"; + public const string Server = "umbracoServer"; + + public const string Tag = "cmsTags"; + public const string TagRelationship = "cmsTagRelationship"; + + // ReSharper restore UnusedMember.Local } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs index 74445d268d..60e38eca29 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_0_1/ChangeNuCacheJsonFormat.cs @@ -1,16 +1,16 @@ -using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; +using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1; + +public class ChangeNuCacheJsonFormat : MigrationBase { - public class ChangeNuCacheJsonFormat : MigrationBase + public ChangeNuCacheJsonFormat(IMigrationContext context) + : base(context) { - public ChangeNuCacheJsonFormat(IMigrationContext context) : base(context) - { } - - protected override void Migrate() - { - // nothing - just adding the post-migration - Context.AddPostMigration(); - } } + + protected override void Migrate() => + + // nothing - just adding the post-migration + Context.AddPostMigration(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs index 6c6fd6166c..4e57716c4e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs @@ -1,20 +1,18 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_10_0; + +public class AddPropertyTypeLabelOnTopColumn : MigrationBase { - - public class AddPropertyTypeLabelOnTopColumn : MigrationBase + public AddPropertyTypeLabelOnTopColumn(IMigrationContext context) + : base(context) { - public AddPropertyTypeLabelOnTopColumn(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "labelOnTop"); - } + AddColumnIfNotExists(columns, "labelOnTop"); } } 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 index 23bb979dd9..4a9a494b76 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/AddCmsContentNuByteColumn.cs @@ -1,47 +1,43 @@ using NPoco; -using System.Linq; using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; + +public class AddCmsContentNuByteColumn : MigrationBase { - public class AddCmsContentNuByteColumn : MigrationBase + private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + + public AddCmsContentNuByteColumn(IMigrationContext context) + : base(context) { - public AddCmsContentNuByteColumn(IMigrationContext context) - : base(context) - { + } - } + protected override void Migrate() + { + AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); - protected override void Migrate() - { - AlterColumn(Constants.DatabaseSchema.Tables.NodeData, "data"); + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + AddColumnIfNotExists(columns, "dataRaw"); + } - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "dataRaw"); - } + [TableName(TempTableName)] + [ExplicitColumns] + private class ContentNuDtoTemp + { + [Column("nodeId")] + public int NodeId { get; set; } - private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP"; + [Column("published")] + public bool Published { get; set; } - [TableName(TempTableName)] - [ExplicitColumns] - private class ContentNuDtoTemp - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Data { get; set; } - [Column("published")] - public bool Published { get; set; } - - [Column("data")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Data { get; set; } - - [Column("rv")] - public long Rv { get; set; } - } + [Column("rv")] + public long Rv { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs index 496e12a1fa..703cfc1474 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpdateCmsPropertyGroupIdSeed.cs @@ -1,16 +1,14 @@ -using Umbraco.Cms.Infrastructure.Persistence; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0 +public class UpdateCmsPropertyGroupIdSeed : MigrationBase { - public class UpdateCmsPropertyGroupIdSeed : MigrationBase + public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) + : base(context) { - public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() - { - // NOOP - was sql ce only - } + protected override void Migrate() + { + // NOOP - was sql ce only } } 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 index 9bdce9bfbf..114bb7becc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_15_0/UpgradedIncludeIndexes.cs @@ -1,66 +1,73 @@ -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 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0; + +public class UpgradedIncludeIndexes : MigrationBase { - public class UpgradedIncludeIndexes : MigrationBase + public UpgradedIncludeIndexes(IMigrationContext context) + : base(context) { - public UpgradedIncludeIndexes(IMigrationContext context) - : base(context) + } + + protected 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(); - protected override void Migrate() + var contentVersionIndexes = new[] { - // 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(); + $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current", + }; + DeleteIndexes(contentVersionIndexes); // delete existing ones + CreateIndexes(contentVersionIndexes); // update/add + } + private void DeleteIndexes(params string[] toDelete) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); - 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) + foreach (var i in 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) + if (IndexExists(i)) { - // get the definition by name - var index = tableDef.Indexes.First(x => x.Name == c); - new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); + Delete.Index(i).OnTable(tableDef.Name).Do(); } + } + } + private void CreateIndexes(params string[] toCreate) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax); + + foreach (var c in toCreate) + { + // get the definition by name + IndexDefinition index = tableDef.Indexes.First(x => x.Name == c); + new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) } + .Execute(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs index feedc56d9a..cef69d6bd3 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs @@ -1,65 +1,70 @@ -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0; + +public class AddPropertyTypeGroupColumns : MigrationBase { - public class AddPropertyTypeGroupColumns : MigrationBase + private readonly IShortStringHelper _shortStringHelper; + + public AddPropertyTypeGroupColumns(IMigrationContext context, IShortStringHelper shortStringHelper) + : base(context) => _shortStringHelper = shortStringHelper; + + internal IEnumerable PopulateAliases(IEnumerable dtos) { - private readonly IShortStringHelper _shortStringHelper; - - public AddPropertyTypeGroupColumns(IMigrationContext context, IShortStringHelper shortStringHelper) - : base(context) => _shortStringHelper = shortStringHelper; - - protected override void Migrate() + foreach (IGrouping dtosPerAlias in dtos.GroupBy(x => + x.Text?.ToSafeAlias(_shortStringHelper, true))) { - AddColumn("type"); - - // Add column without constraints - AddColumn("alias", out var sqls); - - // Populate non-null alias column - var dtos = Database.Fetch(); - foreach (var dto in PopulateAliases(dtos)) - Database.Update(dto, x => new { x.Alias }); - - // Finally add the constraints - foreach (var sql in sqls) - Database.Execute(sql); - } - - internal IEnumerable PopulateAliases(IEnumerable dtos) - { - foreach (var dtosPerAlias in dtos.GroupBy(x => x.Text?.ToSafeAlias(_shortStringHelper, true))) + IEnumerable> dtosPerAliasAndText = + dtosPerAlias.GroupBy(x => x.Text); + var numberSuffix = 1; + foreach (IGrouping dtosPerText in dtosPerAliasAndText) { - var dtosPerAliasAndText = dtosPerAlias.GroupBy(x => x.Text); - var numberSuffix = 1; - foreach (var dtosPerText in dtosPerAliasAndText) + foreach (PropertyTypeGroupDto dto in dtosPerText) { - foreach (var dto in dtosPerText) + dto.Alias = dtosPerAlias.Key ?? string.Empty; + + if (numberSuffix > 1) { - dto.Alias = dtosPerAlias.Key ?? string.Empty; - - if (numberSuffix > 1) - { - // More than 1 name found for the alias, so add a suffix - dto.Alias += numberSuffix; - } - - yield return dto; + // More than 1 name found for the alias, so add a suffix + dto.Alias += numberSuffix; } - numberSuffix++; + yield return dto; } - if (numberSuffix > 2) - { - Logger.LogError("Detected the same alias {Alias} for different property group names {Names}, the migration added suffixes, but this might break backwards compatibility.", dtosPerAlias.Key, dtosPerAliasAndText.Select(x => x.Key)); - } + numberSuffix++; + } + + if (numberSuffix > 2) + { + Logger.LogError( + "Detected the same alias {Alias} for different property group names {Names}, the migration added suffixes, but this might break backwards compatibility.", + dtosPerAlias.Key, dtosPerAliasAndText.Select(x => x.Key)); } } } + + protected override void Migrate() + { + AddColumn("type"); + + // Add column without constraints + AddColumn("alias", out IEnumerable sqls); + + // Populate non-null alias column + List? dtos = Database.Fetch(); + foreach (PropertyTypeGroupDto dto in PopulateAliases(dtos)) + { + Database.Update(dto, x => new { x.Alias }); + } + + // Finally add the constraints + foreach (var sql in sqls) + { + Database.Execute(sql); + } + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index 96d60a30e5..0f7fe97663 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -1,127 +1,132 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.Models; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class ConvertTinyMceAndGridMediaUrlsToLocalLink : MigrationBase { - public class ConvertTinyMceAndGridMediaUrlsToLocalLink : MigrationBase + private readonly IMediaService _mediaService; + + public ConvertTinyMceAndGridMediaUrlsToLocalLink(IMigrationContext context, IMediaService mediaService) + : base(context) => _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); + + protected override void Migrate() { - private readonly IMediaService _mediaService; + var mediaLinkPattern = new Regex( + @"(]*href="")(\/ media[^""\?]*)([^>]*>)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - public ConvertTinyMceAndGridMediaUrlsToLocalLink(IMigrationContext context, IMediaService mediaService) : base(context) + Sql sqlPropertyData = Sql() + .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x!.DataTypeDto))) + .From() + .InnerJoin() + .On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin() + .On((left, right) => left.DataTypeId == right.NodeId) + .Where(x => + x.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce || + x.EditorAlias == Constants.PropertyEditors.Aliases.Grid); + + List? properties = Database.Fetch(sqlPropertyData); + + var exceptions = new List(); + foreach (PropertyDataDto80? property in properties) { - _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); - } - - protected override void Migrate() - { - var mediaLinkPattern = new Regex( - @"(]*href="")(\/ media[^""\?]*)([^>]*>)", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - - var sqlPropertyData = Sql() - .Select(r => r.Select(x => x.PropertyTypeDto, r1 => r1.Select(x => x!.DataTypeDto))) - .From() - .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) - .Where(x => - x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.TinyMce || - x.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Grid); - - var properties = Database.Fetch(sqlPropertyData); - - var exceptions = new List(); - foreach (var property in properties) + var value = property.TextValue; + if (string.IsNullOrWhiteSpace(value)) { - var value = property.TextValue; - if (string.IsNullOrWhiteSpace(value)) continue; + continue; + } - - bool propertyChanged = false; - if (property.PropertyTypeDto?.DataTypeDto?.EditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Grid) + var propertyChanged = false; + if (property.PropertyTypeDto?.DataTypeDto?.EditorAlias == Constants.PropertyEditors.Aliases.Grid) + { + try { - try - { - var obj = JsonConvert.DeserializeObject(value); - var allControls = obj?.SelectTokens("$.sections..rows..areas..controls"); + JObject? obj = JsonConvert.DeserializeObject(value); + IEnumerable? allControls = obj?.SelectTokens("$.sections..rows..areas..controls"); - if (allControls is not null) + if (allControls is not null) + { + foreach (JObject control in allControls.SelectMany(c => c).OfType()) { - foreach (var control in allControls.SelectMany(c => c).OfType()) + JToken? controlValue = control["value"]; + if (controlValue?.Type == JTokenType.String) { - var controlValue = control["value"]; - if (controlValue?.Type == JTokenType.String) - { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()!, out var controlChanged); - propertyChanged |= controlChanged; - } + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()!, + out var controlChanged); + propertyChanged |= controlChanged; } } - - property.TextValue = JsonConvert.SerializeObject(obj); - } - catch (JsonException e) - { - exceptions.Add(new InvalidOperationException( - "Cannot deserialize the value as json. This can be because the property editor " + - "type is changed from another type into a grid. Old versions of the value in this " + - "property can have the structure from the old property editor type. This needs to be " + - "changed manually before updating the database.\n" + - $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" - , e)); - continue; } + property.TextValue = JsonConvert.SerializeObject(obj); } - else + catch (JsonException e) { - property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); + exceptions.Add(new InvalidOperationException( + "Cannot deserialize the value as json. This can be because the property editor " + + "type is changed from another type into a grid. Old versions of the value in this " + + "property can have the structure from the old property editor type. This needs to be " + + "changed manually before updating the database.\n" + + $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}", + e)); + continue; } - - if (propertyChanged) - Database.Update(property); } - - - if (exceptions.Any()) + else { - throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions); + property.TextValue = UpdateMediaUrls(mediaLinkPattern, value, out propertyChanged); } - Context.AddPostMigration(); + if (propertyChanged) + { + Database.Update(property); + } } - private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) + if (exceptions.Any()) { - bool matched = false; - - var result = mediaLinkPattern.Replace(value, match => - { - matched = true; - - // match groups: - // - 1 = from the beginning of the a tag until href attribute value begins - // - 2 = the href attribute value excluding the querystring (if present) - // - 3 = anything after group 2 until the a tag is closed - var href = match.Groups[2].Value; - - var media = _mediaService.GetMediaByPath(href); - return media == null - ? match.Value - : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; - }); - - changed = matched; - - return result; + throw new AggregateException( + "One or more errors related to unexpected data in grid values occurred.", + exceptions); } + + Context.AddPostMigration(); + } + + private string UpdateMediaUrls(Regex mediaLinkPattern, string value, out bool changed) + { + var matched = false; + + var result = mediaLinkPattern.Replace(value, match => + { + matched = true; + + // match groups: + // - 1 = from the beginning of the a tag until href attribute value begins + // - 2 = the href attribute value excluding the querystring (if present) + // - 3 = anything after group 2 until the a tag is closed + var href = match.Groups[2].Value; + + IMedia? media = _mediaService.GetMediaByPath(href); + return media == null + ? match.Value + : $"{match.Groups[1].Value}/{{localLink:{media.GetUdi()}}}{match.Groups[3].Value}"; + }); + + changed = matched; + + return result; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs index 00389c547e..abb4fbfea8 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/FixContentNuCascade.cs @@ -1,17 +1,17 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class FixContentNuCascade : MigrationBase { - public class FixContentNuCascade : MigrationBase + public FixContentNuCascade(IMigrationContext context) + : base(context) { - public FixContentNuCascade(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - Delete.KeysAndIndexes().Do(); - Create.KeysAndIndexes().Do(); - } + protected override void Migrate() + { + Delete.KeysAndIndexes().Do(); + Create.KeysAndIndexes().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs index f06477579a..ac2e27b2d6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_1_0/RenameUserLoginDtoDateIndex.cs @@ -1,36 +1,39 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; + +public class RenameUserLoginDtoDateIndex : MigrationBase { - public class RenameUserLoginDtoDateIndex : MigrationBase + public RenameUserLoginDtoDateIndex(IMigrationContext context) + : base(context) { - public RenameUserLoginDtoDateIndex(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + // there has been some confusion with an index name, resulting in + // different names depending on which migration path was followed, + // and discrepancies between an upgraded or an installed database. + // better normalize + if (IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) { - // there has been some confusion with an index name, resulting in - // different names depending on which migration path was followed, - // and discrepancies between an upgraded or an installed database. - // better normalize + return; + } - if (IndexExists("IX_umbracoUserLogin_lastValidatedUtc")) - return; - - if (IndexExists("IX_userLoginDto_lastValidatedUtc")) - Delete - .Index("IX_userLoginDto_lastValidatedUtc") - .OnTable(UserLoginDto.TableName) - .Do(); - - Create - .Index("IX_umbracoUserLogin_lastValidatedUtc") + if (IndexExists("IX_userLoginDto_lastValidatedUtc")) + { + Delete + .Index("IX_userLoginDto_lastValidatedUtc") .OnTable(UserLoginDto.TableName) - .OnColumn("lastValidatedUtc") - .Ascending() - .WithOptions().NonClustered() .Do(); } + + Create + .Index("IX_umbracoUserLogin_lastValidatedUtc") + .OnTable(UserLoginDto.TableName) + .OnColumn("lastValidatedUtc") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs index 9fe257fafe..bd76857ab7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs @@ -1,16 +1,15 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class AddMainDomLock : MigrationBase { - public class AddMainDomLock : MigrationBase + public AddMainDomLock(IMigrationContext context) + : base(context) { - public AddMainDomLock(IMigrationContext context) - : base(context) - { } - - protected override void Migrate() - { - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.MainDom, Name = "MainDom" }); - } } + + protected override void Migrate() => Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, + new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" }); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs index 9c770adf15..c2a447e778 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddNewRelationTypes.cs @@ -1,33 +1,38 @@ -using Umbraco.Cms.Infrastructure.Migrations.Install; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +/// +/// Ensures the new relation types are created +/// +public class AddNewRelationTypes : MigrationBase { - /// - /// Ensures the new relation types are created - /// - public class AddNewRelationTypes : MigrationBase + public AddNewRelationTypes(IMigrationContext context) + : base(context) { - public AddNewRelationTypes(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - CreateRelation( - Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, - Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName); + protected override void Migrate() + { + CreateRelation( + Constants.Conventions.RelationTypes.RelatedMediaAlias, + Constants.Conventions.RelationTypes.RelatedMediaName); - CreateRelation( - Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, - Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName); - } + CreateRelation( + Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedDocumentName); + } - private void CreateRelation(string alias, string name) - { - var uniqueId = DatabaseDataCreator.CreateUniqueRelationTypeId(alias ,name); //this is the same as how it installs so everything is consistent - Insert.IntoTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias }) - .Do(); - } + private void CreateRelation(string alias, string name) + { + Guid uniqueId = + DatabaseDataCreator + .CreateUniqueRelationTypeId( + alias, + name); // this is the same as how it installs so everything is consistent + Insert.IntoTable(Constants.DatabaseSchema.Tables.RelationType) + .Row(new { typeUniqueId = uniqueId, dual = 0, name, alias }) + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs index 9a9e2b5e77..a5aea97fc2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs @@ -1,21 +1,19 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class AddPropertyTypeValidationMessageColumns : MigrationBase { - - public class AddPropertyTypeValidationMessageColumns : MigrationBase + public AddPropertyTypeValidationMessageColumns(IMigrationContext context) + : base(context) { - public AddPropertyTypeValidationMessageColumns(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "mandatoryMessage"); - AddColumnIfNotExists(columns, "validationRegExpMessage"); - } + AddColumnIfNotExists(columns, "mandatoryMessage"); + AddColumnIfNotExists(columns, "validationRegExpMessage"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs index 2d4b227249..7e7b659401 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/MissingContentVersionsIndexes.cs @@ -1,33 +1,31 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class MissingContentVersionsIndexes : MigrationBase { - public class MissingContentVersionsIndexes : MigrationBase + private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + + public MissingContentVersionsIndexes(IMigrationContext context) + : base(context) { - private const string IndexName = "IX_" + ContentVersionDto.TableName + "_NodeId"; + } - public MissingContentVersionsIndexes(IMigrationContext context) : base(context) + protected override void Migrate() + { + // We must check before we create an index because if we are upgrading from v7 we force re-create all + // indexes in the whole DB and then this would throw + if (!IndexExists(IndexName)) { - } - - protected override void Migrate() - { - // We must check before we create an index because if we are upgrading from v7 we force re-create all - // indexes in the whole DB and then this would throw - - if (!IndexExists(IndexName)) - { - Create - .Index(IndexName) - .OnTable(ContentVersionDto.TableName) - .OnColumn("nodeId") - .Ascending() - .OnColumn("current") - .Ascending() - .WithOptions().NonClustered() - .Do(); - } - + Create + .Index(IndexName) + .OnTable(ContentVersionDto.TableName) + .OnColumn("nodeId") + .Ascending() + .OnColumn("current") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs index bc3757eaad..032359cfcc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/UpdateRelationTypeTable.cs @@ -1,36 +1,42 @@ -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0 +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_6_0; + +public class UpdateRelationTypeTable : MigrationBase { - - public class UpdateRelationTypeTable : MigrationBase + public UpdateRelationTypeTable(IMigrationContext context) + : base(context) { - public UpdateRelationTypeTable(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() + protected override void Migrate() + { + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable() + .Do(); + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable() + .Do(); + + // TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable + + // drop index before we can alter the column + if (IndexExists("IX_umbracoRelationType_alias")) { - - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("parentObjectType").AsGuid().Nullable().Do(); - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("childObjectType").AsGuid().Nullable().Do(); - - //TODO: We have to update this field to ensure it's not null, we can just copy across the name since that is not nullable - - //drop index before we can alter the column - if (IndexExists("IX_umbracoRelationType_alias")) - Delete - .Index("IX_umbracoRelationType_alias") - .OnTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .Do(); - //change the column to non nullable - Alter.Table(Cms.Core.Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do(); - //re-create the index - Create + Delete .Index("IX_umbracoRelationType_alias") - .OnTable(Cms.Core.Constants.DatabaseSchema.Tables.RelationType) - .OnColumn("alias") - .Ascending() - .WithOptions().Unique().WithOptions().NonClustered() + .OnTable(Constants.DatabaseSchema.Tables.RelationType) .Do(); } + + // change the column to non nullable + Alter.Table(Constants.DatabaseSchema.Tables.RelationType).AlterColumn("alias").AsString(100).NotNullable().Do(); + + // re-create the index + Create + .Index("IX_umbracoRelationType_alias") + .OnTable(Constants.DatabaseSchema.Tables.RelationType) + .OnColumn("alias") + .Ascending() + .WithOptions().Unique().WithOptions().NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs index 69e4a7423c..1ab43c2eb7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_7_0/MissingDictionaryIndex.cs @@ -1,33 +1,31 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_7_0; + +public class MissingDictionaryIndex : MigrationBase { - public class MissingDictionaryIndex : MigrationBase + public MissingDictionaryIndex(IMigrationContext context) + : base(context) { - public MissingDictionaryIndex(IMigrationContext context) - : base(context) + } + + /// + /// Adds an index to the foreign key column parent on DictionaryDto's table + /// if it doesn't already exist + /// + protected override void Migrate() + { + var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; + + if (!IndexExists(indexName)) { - - } - - /// - /// Adds an index to the foreign key column parent on DictionaryDto's table - /// if it doesn't already exist - /// - protected override void Migrate() - { - var indexName = "IX_" + DictionaryDto.TableName + "_Parent"; - - if (!IndexExists(indexName)) - { - Create - .Index(indexName) - .OnTable(DictionaryDto.TableName) - .OnColumn("parent") - .Ascending() - .WithOptions().NonClustered() - .Do(); - } + Create + .Index(indexName) + .OnTable(DictionaryDto.TableName) + .OnColumn("parent") + .Ascending() + .WithOptions().NonClustered() + .Do(); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs index 7f75bde572..bf21b6b928 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs @@ -1,20 +1,18 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0; + +public class ExternalLoginTableUserData : MigrationBase { - public class ExternalLoginTableUserData : MigrationBase + public ExternalLoginTableUserData(IMigrationContext context) + : base(context) { - public ExternalLoginTableUserData(IMigrationContext context) - : base(context) - { - } - - /// - /// Adds new column to the External Login table - /// - protected override void Migrate() - { - AddColumn(Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin, "userData"); - } } + + /// + /// Adds new column to the External Login table + /// + protected override void Migrate() => + AddColumn(Constants.DatabaseSchema.Tables.ExternalLogin, "userData"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs index 01ea1cf3b3..2bf01c55bb 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/AddPasswordConfigToMemberTable.cs @@ -1,23 +1,21 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class AddPasswordConfigToMemberTable : MigrationBase { - public class AddPasswordConfigToMemberTable : MigrationBase + public AddPasswordConfigToMemberTable(IMigrationContext context) + : base(context) { - public AddPasswordConfigToMemberTable(IMigrationContext context) - : base(context) - { - } + } - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + /// + /// Adds new columns to members table + /// + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "passwordConfig"); - } + AddColumnIfNotExists(columns, "passwordConfig"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs index 44034c5e45..3556213cd1 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/DictionaryTablesIndexes.cs @@ -1,129 +1,133 @@ -using System.Linq; using Microsoft.Extensions.Logging; +using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Execute.Expressions; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class DictionaryTablesIndexes : MigrationBase { - public class DictionaryTablesIndexes : MigrationBase + private const string IndexedDictionaryColumn = "key"; + private const string IndexedLanguageTextColumn = "languageId"; + + public DictionaryTablesIndexes(IMigrationContext context) + : base(context) { - private const string IndexedDictionaryColumn = "key"; - private const string IndexedLanguageTextColumn = "languageId"; - - public DictionaryTablesIndexes(IMigrationContext context) - : base(context) - { - } - - protected override void Migrate() - { - var indexDictionaryDto = $"IX_{DictionaryDto.TableName}_{IndexedDictionaryColumn}"; - var indexLanguageTextDto = $"IX_{LanguageTextDto.TableName}_{IndexedLanguageTextColumn}"; - var dictionaryColumnsToBeIndexed = new[] { IndexedDictionaryColumn }; - var langTextColumnsToBeIndexed = new[] { IndexedLanguageTextColumn, "UniqueId" }; - - var dictionaryTableHasDuplicates = ContainsDuplicates(dictionaryColumnsToBeIndexed); - var langTextTableHasDuplicates = ContainsDuplicates(langTextColumnsToBeIndexed); - - // Check if there are any duplicates before we delete and re-create the indexes since - // if there are duplicates we won't be able to create the new unique indexes - if (!dictionaryTableHasDuplicates) - { - // Delete existing - DeleteIndex(indexDictionaryDto); - } - - if (!langTextTableHasDuplicates) - { - // Delete existing - DeleteIndex(indexLanguageTextDto); - } - - // Try to re-create/add - TryAddUniqueConstraint(dictionaryColumnsToBeIndexed, indexDictionaryDto, dictionaryTableHasDuplicates); - TryAddUniqueConstraint(langTextColumnsToBeIndexed, indexLanguageTextDto, langTextTableHasDuplicates); - } - - private void DeleteIndex(string indexName) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - if (IndexExists(indexName)) - { - Delete.Index(indexName).OnTable(tableDef.Name).Do(); - } - } - - private void CreateIndex(string indexName) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - // get the definition by name - var index = tableDef.Indexes.First(x => x.Name == indexName); - new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute(); - } - - private void TryAddUniqueConstraint(string[] columns, string index, bool containsDuplicates) - { - var tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); - - // Check the existing data to ensure the constraint can be successfully applied. - // This seems to be better than relying on catching an exception as this leads to - // transaction errors: "This SqlTransaction has completed; it is no longer usable". - var columnsDescription = string.Join("], [", columns); - if (containsDuplicates) - { - var message = $"Could not create unique constraint on [{tableDef.Name}] due to existing " + - $"duplicate records across the column{(columns.Length > 1 ? "s" : string.Empty)}: [{columnsDescription}]."; - - LogIncompleteMigrationStep(message); - return; - } - - CreateIndex(index); - } - - private bool ContainsDuplicates(string[] columns) - { - // Check for duplicates by comparing the total count of all records with the count of records distinct by the - // provided column. If the former is greater than the latter, there's at least one duplicate record. - int recordCount = GetRecordCount(); - int distinctRecordCount = GetDistinctRecordCount(columns); - - return recordCount > distinctRecordCount; - } - - private int GetRecordCount() - { - var countQuery = Database.SqlContext.Sql() - .SelectCount() - .From(); - - return Database.ExecuteScalar(countQuery); - } - - private int GetDistinctRecordCount(string[] columns) - { - string columnSpecification; - - columnSpecification = columns.Length == 1 - ? QuoteColumnName(columns[0]) - : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; - - var distinctCountQuery = Database.SqlContext.Sql() - .Select($"COUNT(DISTINCT({columnSpecification}))") - .From(); - - return Database.ExecuteScalar(distinctCountQuery); - } - - private void LogIncompleteMigrationStep(string message) => Logger.LogError($"Database migration step failed: {message}"); - - private string StringConvertedAndQuotedColumnName(string column) => $"CONVERT(nvarchar(1000),{QuoteColumnName(column)})"; - - private string QuoteColumnName(string column) => $"[{column}]"; } + + protected override void Migrate() + { + var indexDictionaryDto = $"IX_{DictionaryDto.TableName}_{IndexedDictionaryColumn}"; + var indexLanguageTextDto = $"IX_{LanguageTextDto.TableName}_{IndexedLanguageTextColumn}"; + var dictionaryColumnsToBeIndexed = new[] { IndexedDictionaryColumn }; + var langTextColumnsToBeIndexed = new[] { IndexedLanguageTextColumn, "UniqueId" }; + + var dictionaryTableHasDuplicates = ContainsDuplicates(dictionaryColumnsToBeIndexed); + var langTextTableHasDuplicates = ContainsDuplicates(langTextColumnsToBeIndexed); + + // Check if there are any duplicates before we delete and re-create the indexes since + // if there are duplicates we won't be able to create the new unique indexes + if (!dictionaryTableHasDuplicates) + { + // Delete existing + DeleteIndex(indexDictionaryDto); + } + + if (!langTextTableHasDuplicates) + { + // Delete existing + DeleteIndex(indexLanguageTextDto); + } + + // Try to re-create/add + TryAddUniqueConstraint(dictionaryColumnsToBeIndexed, indexDictionaryDto, + dictionaryTableHasDuplicates); + TryAddUniqueConstraint(langTextColumnsToBeIndexed, indexLanguageTextDto, + langTextTableHasDuplicates); + } + + private void DeleteIndex(string indexName) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); + + if (IndexExists(indexName)) + { + Delete.Index(indexName).OnTable(tableDef.Name).Do(); + } + } + + private void CreateIndex(string indexName) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); + + // get the definition by name + IndexDefinition index = tableDef.Indexes.First(x => x.Name == indexName); + new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) } + .Execute(); + } + + private void TryAddUniqueConstraint(string[] columns, string index, bool containsDuplicates) + { + TableDefinition tableDef = DefinitionFactory.GetTableDefinition(typeof(TDto), Context.SqlContext.SqlSyntax); + + // Check the existing data to ensure the constraint can be successfully applied. + // This seems to be better than relying on catching an exception as this leads to + // transaction errors: "This SqlTransaction has completed; it is no longer usable". + var columnsDescription = string.Join("], [", columns); + if (containsDuplicates) + { + var message = $"Could not create unique constraint on [{tableDef.Name}] due to existing " + + $"duplicate records across the column{(columns.Length > 1 ? "s" : string.Empty)}: [{columnsDescription}]."; + + LogIncompleteMigrationStep(message); + return; + } + + CreateIndex(index); + } + + private bool ContainsDuplicates(string[] columns) + { + // Check for duplicates by comparing the total count of all records with the count of records distinct by the + // provided column. If the former is greater than the latter, there's at least one duplicate record. + var recordCount = GetRecordCount(); + var distinctRecordCount = GetDistinctRecordCount(columns); + + return recordCount > distinctRecordCount; + } + + private int GetRecordCount() + { + Sql countQuery = Database.SqlContext.Sql() + .SelectCount() + .From(); + + return Database.ExecuteScalar(countQuery); + } + + private int GetDistinctRecordCount(string[] columns) + { + string columnSpecification; + + columnSpecification = columns.Length == 1 + ? QuoteColumnName(columns[0]) + : $"CONCAT({string.Join(",", columns.Select(QuoteColumnName))})"; + + Sql distinctCountQuery = Database.SqlContext.Sql() + .Select($"COUNT(DISTINCT({columnSpecification}))") + .From(); + + return Database.ExecuteScalar(distinctCountQuery); + } + + private void LogIncompleteMigrationStep(string message) => + Logger.LogError($"Database migration step failed: {message}"); + + private string StringConvertedAndQuotedColumnName(string column) => + $"CONVERT(nvarchar(1000),{QuoteColumnName(column)})"; + + private string QuoteColumnName(string column) => $"[{column}]"; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs index db7f17eee3..8caa28de03 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexes.cs @@ -1,80 +1,70 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NPoco; -using Umbraco.Cms.Core; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +public class ExternalLoginTableIndexes : MigrationBase { - - public class ExternalLoginTableIndexes : MigrationBase + public ExternalLoginTableIndexes(IMigrationContext context) + : base(context) { - public ExternalLoginTableIndexes(IMigrationContext context) - : base(context) + } + + /// + /// Adds new indexes to the External Login table + /// + protected override void Migrate() + { + // Before adding these indexes we need to remove duplicate data. + // Get all logins by latest + var logins = Database.Fetch() + .OrderByDescending(x => x.CreateDate) + .ToList(); + + var toDelete = new List(); + + // used to track duplicates so they can be removed + var keys = new HashSet<(string, string)>(); + foreach (ExternalLoginTokenTable.LegacyExternalLoginDto login in logins) { - } - - /// - /// Adds new indexes to the External Login table - /// - protected override void Migrate() - { - // Before adding these indexes we need to remove duplicate data. - // Get all logins by latest - var logins = Database.Fetch() - .OrderByDescending(x => x.CreateDate) - .ToList(); - - var toDelete = new List(); - // used to track duplicates so they can be removed - var keys = new HashSet<(string, string)>(); - foreach(ExternalLoginTokenTable.LegacyExternalLoginDto login in logins) + if (!keys.Add((login.ProviderKey, login.LoginProvider))) { - if (!keys.Add((login.ProviderKey, login.LoginProvider))) - { - // if it already exists we need to remove this one - toDelete.Add(login.Id); - } - } - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } - - var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; - - if (!IndexExists(indexName1)) - { - Create - .Index(indexName1) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider") - .Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); - } - - var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; - - if (!IndexExists(indexName2)) - { - Create - .Index(indexName2) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("providerKey").Ascending() - .WithOptions() - .NonClustered() - .Do(); + // if it already exists we need to remove this one + toDelete.Add(login.Id); } } + if (toDelete.Count > 0) + { + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)) + .Execute(); + } + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; + + if (!IndexExists(indexName1)) + { + Create + .Index(indexName1) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider") + .Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + } + + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; + + if (!IndexExists(indexName2)) + { + Create + .Index(indexName2) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); + } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs index 2c77b301ce..8c508a3d04 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTableIndexesFixup.cs @@ -1,59 +1,58 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +/// +/// Fixes up the original for post RC release to ensure that +/// the correct indexes are applied. +/// +public class ExternalLoginTableIndexesFixup : MigrationBase { - /// - /// Fixes up the original for post RC release to ensure that - /// the correct indexes are applied. - /// - public class ExternalLoginTableIndexesFixup : MigrationBase + public ExternalLoginTableIndexesFixup(IMigrationContext context) + : base(context) { - public ExternalLoginTableIndexesFixup(IMigrationContext context) : base(context) + } + + protected override void Migrate() + { + var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; + var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; + + if (IndexExists(indexName1)) { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); } - protected override void Migrate() + if (IndexExists(indexName2)) { - var indexName1 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_LoginProvider"; - var indexName2 = "IX_" + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName + "_ProviderKey"; - - if (IndexExists(indexName1)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexName1).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); - } - - if (IndexExists(indexName2)) - { - // drop since it's using a column we're about to modify - Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); - } - - // then fixup the length of the loginProvider column - AlterColumn(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider"); - - // create it with the correct definition - Create - .Index(indexName1) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("userId").Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); - - // re-create the original - Create - .Index(indexName2) - .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("providerKey").Ascending() - .WithOptions() - .NonClustered() - .Do(); + // drop since it's using a column we're about to modify + Delete.Index(indexName2).OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName).Do(); } + + // then fixup the length of the loginProvider column + AlterColumn( + ExternalLoginTokenTable.LegacyExternalLoginDto.TableName, "loginProvider"); + + // create it with the correct definition + Create + .Index(indexName1) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userId").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + + // re-create the original + Create + .Index(indexName2) + .OnTable(ExternalLoginTokenTable.LegacyExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("providerKey").Ascending() + .WithOptions() + .NonClustered() + .Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs index ee089ad89c..288dbdf23f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/ExternalLoginTokenTable.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; @@ -7,75 +5,75 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class ExternalLoginTokenTable : MigrationBase { - public class ExternalLoginTokenTable : MigrationBase + public ExternalLoginTokenTable(IMigrationContext context) + : base(context) { - public ExternalLoginTokenTable(IMigrationContext context) - : base(context) + } + + /// + /// Adds new External Login token table + /// + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains(ExternalLoginTokenDto.TableName)) { + return; } + Create.Table().Do(); + } + + [TableName(TableName)] + [ExplicitColumns] + [PrimaryKey("Id")] + internal class LegacyExternalLoginDto + { + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Obsolete( + "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [Column("userId")] + public int? UserId { get; set; } + /// - /// Adds new External Login token table + /// Used to store the name of the provider (i.e. Facebook, Google) /// - protected override void Migrate() - { - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains(ExternalLoginTokenDto.TableName)) - { - return; - } + [Column("loginProvider")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", + Name = "IX_" + TableName + "_LoginProvider")] + public string LoginProvider { get; set; } = null!; - Create.Table().Do(); - } + /// + /// Stores the key the provider uses to lookup the login + /// + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", + Name = "IX_" + TableName + "_ProviderKey")] + public string ProviderKey { get; set; } = null!; - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class LegacyExternalLoginDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("id")] [PrimaryKeyColumn] public int Id { get; set; } - - [Obsolete( - "This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] - [Column("userId")] - public int? UserId { get; set; } - - - /// - /// Used to store the name of the provider (i.e. Facebook, Google) - /// - [Column("loginProvider")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", - Name = "IX_" + TableName + "_LoginProvider")] - public string LoginProvider { get; set; } = null!; - - /// - /// Stores the key the provider uses to lookup the login - /// - [Column("providerKey")] - [Length(4000)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", - Name = "IX_" + TableName + "_ProviderKey")] - public string ProviderKey { get; set; } = null!; - - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - - /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider - /// - [Column("userData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? UserData { get; set; } - } + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? UserData { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs index 5dd274ad05..50204e4432 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MemberTableColumns.cs @@ -1,24 +1,22 @@ -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class MemberTableColumns : MigrationBase { - public class MemberTableColumns : MigrationBase + public MemberTableColumns(IMigrationContext context) + : base(context) { - public MemberTableColumns(IMigrationContext context) - : base(context) - { - } + } - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + /// + /// Adds new columns to members table + /// + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - AddColumnIfNotExists(columns, "securityStampToken"); - AddColumnIfNotExists(columns, "emailConfirmedDate"); - } + AddColumnIfNotExists(columns, "securityStampToken"); + AddColumnIfNotExists(columns, "emailConfirmedDate"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs index 365c50b3f8..641433cee9 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/MigrateLogViewerQueriesFromFileToDb.cs @@ -1,105 +1,106 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Infrastructure.Migrations.PostMigrations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; + +public class MigrateLogViewerQueriesFromFileToDb : MigrationBase { - - public class MigrateLogViewerQueriesFromFileToDb : MigrationBase + internal static readonly IEnumerable _defaultLogQueries = new LogViewerQueryDto[] { - private readonly IHostingEnvironment _hostingEnvironment; - internal static readonly IEnumerable DefaultLogQueries = new LogViewerQueryDto[] + new() { - new (){ - Name = "Find all logs where the Level is NOT Verbose and NOT Debug", - Query = "Not(@Level='Verbose') and Not(@Level='Debug')" - }, - new (){ - Name = "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", - Query = "Has(@Exception)" - }, - new (){ - Name = "Find all logs that have the property 'Duration'", - Query = "Has(Duration)" - }, - new (){ - Name = "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", - Query = "Has(Duration) and Duration > 1000" - }, - new (){ - Name = "Find all logs that are from the namespace 'Umbraco.Core'", - Query = "StartsWith(SourceContext, 'Umbraco.Core')" - }, - new (){ - Name = "Find all logs that use a specific log message template", - Query = "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" - }, - new (){ - Name = "Find logs where one of the items in the SortedComponentTypes property array is equal to", - Query = "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'" - }, - new (){ - Name = "Find logs where one of the items in the SortedComponentTypes property array contains", - Query = "Contains(SortedComponentTypes[?], 'DatabaseServer')" - }, - new (){ - Name = "Find all logs that the message has localhost in it with SQL like", - Query = "@Message like '%localhost%'" - }, - new (){ - Name = "Find all logs that the message that starts with 'end' in it with SQL like", - Query = "@Message like 'end%'" - } - }; - - public MigrateLogViewerQueriesFromFileToDb(IMigrationContext context, IHostingEnvironment hostingEnvironment) - : base(context) + Name = "Find all logs where the Level is NOT Verbose and NOT Debug", + Query = "Not(@Level='Verbose') and Not(@Level='Debug')", + }, + new() { - _hostingEnvironment = hostingEnvironment; - } - - protected override void Migrate() + Name = "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", + Query = "Has(@Exception)", + }, + new() { Name = "Find all logs that have the property 'Duration'", Query = "Has(Duration)" }, + new() { - CreateDatabaseTable(); - MigrateFileContentToDB(); - } - private void CreateDatabaseTable() + Name = "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + Query = "Has(Duration) and Duration > 1000", + }, + new() { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(Core.Constants.DatabaseSchema.Tables.LogViewerQuery)) - { - Create.Table().Do(); - } - } - - internal static string GetLogViewerQueryFile(IHostingEnvironment hostingEnvironment) + Name = "Find all logs that are from the namespace 'Umbraco.Core'", + Query = "StartsWith(SourceContext, 'Umbraco.Core')", + }, + new() { - return hostingEnvironment.MapPathContentRoot( - Path.Combine(Cms.Core.Constants.SystemDirectories.Config, "logviewer.searches.config.js")); - } - private void MigrateFileContentToDB() + Name = "Find all logs that use a specific log message template", + Query = "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'", + }, + new() { - var logViewerQueryFile = GetLogViewerQueryFile(_hostingEnvironment); + Name = "Find logs where one of the items in the SortedComponentTypes property array is equal to", + Query = "SortedComponentTypes[?] = 'Umbraco.Web.Search.ExamineComponent'", + }, + new() + { + Name = "Find logs where one of the items in the SortedComponentTypes property array contains", + Query = "Contains(SortedComponentTypes[?], 'DatabaseServer')", + }, + new() + { + Name = "Find all logs that the message has localhost in it with SQL like", + Query = "@Message like '%localhost%'", + }, + new() + { + Name = "Find all logs that the message that starts with 'end' in it with SQL like", + Query = "@Message like 'end%'" + }, + }; - var logQueriesInFile = File.Exists(logViewerQueryFile) ? - JsonConvert.DeserializeObject(File.ReadAllText(logViewerQueryFile)) - : DefaultLogQueries; + private readonly IHostingEnvironment _hostingEnvironment; - var logQueriesInDb = Database.Query().ToArray(); + public MigrateLogViewerQueriesFromFileToDb(IMigrationContext context, IHostingEnvironment hostingEnvironment) + : base(context) => + _hostingEnvironment = hostingEnvironment; - if (logQueriesInDb.Any()) - { - return; - } + internal static string GetLogViewerQueryFile(IHostingEnvironment hostingEnvironment) => + hostingEnvironment.MapPathContentRoot( + Path.Combine(Constants.SystemDirectories.Config, "logviewer.searches.config.js")); - Database.InsertBulk(logQueriesInFile!); + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateFileContentToDB(); + } - Context.AddPostMigration(); + private void CreateDatabaseTable() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(Constants.DatabaseSchema.Tables.LogViewerQuery)) + { + Create.Table().Do(); } } + + private void MigrateFileContentToDB() + { + var logViewerQueryFile = GetLogViewerQueryFile(_hostingEnvironment); + + IEnumerable? logQueriesInFile = File.Exists(logViewerQueryFile) + ? JsonConvert.DeserializeObject(File.ReadAllText(logViewerQueryFile)) + : _defaultLogQueries; + + LogViewerQueryDto[]? logQueriesInDb = Database.Query().ToArray(); + + if (logQueriesInDb.Any()) + { + return; + } + + Database.InsertBulk(logQueriesInFile!); + + Context.AddPostMigration(); + } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs index 601f2bd966..c844606ab2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_0_0/UmbracoServerColumn.cs @@ -1,20 +1,19 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0 -{ - public class UmbracoServerColumn : MigrationBase - { - public UmbracoServerColumn(IMigrationContext context) - : base(context) - { - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; - /// - /// Adds new columns to members table - /// - protected override void Migrate() - { - ReplaceColumn(Cms.Core.Constants.DatabaseSchema.Tables.Server, "isMaster", "isSchedulingPublisher"); - } +public class UmbracoServerColumn : MigrationBase +{ + public UmbracoServerColumn(IMigrationContext context) + : base(context) + { } + + /// + /// Adds new columns to members table + /// + protected override void Migrate() => ReplaceColumn( + Constants.DatabaseSchema.Tables.Server, + "isMaster", "isSchedulingPublisher"); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs index aa0d4472e8..7a885eca07 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_1_0/AddContentVersionCleanupFeature.cs @@ -1,28 +1,29 @@ -using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0; + +internal class AddContentVersionCleanupFeature : MigrationBase { - class AddContentVersionCleanupFeature : MigrationBase + public AddContentVersionCleanupFeature(IMigrationContext context) + : base(context) { - public AddContentVersionCleanupFeature(IMigrationContext context) - : base(context) { } + } - /// - /// The conditionals are useful to enable the same migration to be used in multiple - /// migration paths x.x -> 8.18 and x.x -> 9.x - /// - protected override void Migrate() + /// + /// The conditionals are useful to enable the same migration to be used in multiple + /// migration paths x.x -> 8.18 and x.x -> 9.x + /// + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(ContentVersionCleanupPolicyDto.TableName)) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(ContentVersionCleanupPolicyDto.TableName)) - { - Create.Table().Do(); - } - - var columns = SqlSyntax.GetColumnsInSchema(Context.Database); - AddColumnIfNotExists(columns, "preventCleanup"); + Create.Table().Do(); } + + IEnumerable columns = SqlSyntax.GetColumnsInSchema(Context.Database); + AddColumnIfNotExists(columns, "preventCleanup"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs index 3bc62ab42e..9b91c0a372 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs @@ -1,17 +1,21 @@ -using Umbraco.Cms.Core; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; + +public class AddDefaultForNotificationsToggle : MigrationBase { - public class AddDefaultForNotificationsToggle : MigrationBase + public AddDefaultForNotificationsToggle(IMigrationContext context) + : base(context) { - public AddDefaultForNotificationsToggle(IMigrationContext context) - : base(context) - { } + } - protected override void Migrate() - { - var updateSQL = Sql($"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); - Execute.Sql(updateSQL.SQL).Do(); - } + protected override void Migrate() + { + Sql updateSQL = + Sql( + $"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); + Execute.Sql(updateSQL.SQL).Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs index 1bb7b71c89..41abd07b23 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddUserGroup2NodeTable.cs @@ -1,30 +1,31 @@ -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; + +internal class AddUserGroup2NodeTable : MigrationBase { - class AddUserGroup2NodeTable : MigrationBase + public AddUserGroup2NodeTable(IMigrationContext context) + : base(context) { - public AddUserGroup2NodeTable(IMigrationContext context) - : base(context) { } + } - protected override void Migrate() + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(UserGroup2NodeDto.TableName)) { - var tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(UserGroup2NodeDto.TableName)) - { - Create.Table().Do(); - } - - // Insert if there exists specific permissions today. Can't do it directly in db in any nice way. - var allData = Database.Fetch(); - var toInsert = allData.Select(x => new UserGroup2NodeDto() { NodeId = x.NodeId, UserGroupId = x.UserGroupId }).Distinct( - new DelegateEqualityComparer( - (x, y) => x?.NodeId == y?.NodeId && x?.UserGroupId == y?.UserGroupId, - x => x.NodeId.GetHashCode() + x.UserGroupId.GetHashCode())).ToArray(); - Database.InsertBulk(toInsert); + Create.Table().Do(); } + + // Insert if there exists specific permissions today. Can't do it directly in db in any nice way. + List? allData = Database.Fetch(); + UserGroup2NodeDto[] toInsert = allData + .Select(x => new UserGroup2NodeDto { NodeId = x.NodeId, UserGroupId = x.UserGroupId }).Distinct( + new DelegateEqualityComparer( + (x, y) => x?.NodeId == y?.NodeId && x?.UserGroupId == y?.UserGroupId, + x => x.NodeId.GetHashCode() + x.UserGroupId.GetHashCode())).ToArray(); + Database.InsertBulk(toInsert); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs index c5e569282a..5e781406ae 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs @@ -1,24 +1,23 @@ -using System.Collections.Generic; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class AddTwoFactorLoginTable : MigrationBase { - public class AddTwoFactorLoginTable : MigrationBase + public AddTwoFactorLoginTable(IMigrationContext context) + : base(context) { - public AddTwoFactorLoginTable(IMigrationContext context) : base(context) + } + + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains(TwoFactorLoginDto.TableName)) { + return; } - protected override void Migrate() - { - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (tables.InvariantContains(TwoFactorLoginDto.TableName)) - { - return; - } - - Create.Table().Do(); - } + Create.Table().Do(); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs index 3173e739a9..a2b2b40238 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs @@ -1,72 +1,64 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Linq; using Umbraco.Cms.Core.Packaging; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class MovePackageXMLToDb : MigrationBase { - public class MovePackageXMLToDb : MigrationBase + private readonly PackagesRepository _packagesRepository; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) + : base(context) { - private readonly PackagesRepository _packagesRepository; - private readonly PackageDefinitionXmlParser _xmlParser; + _packagesRepository = packagesRepository; + _xmlParser = new PackageDefinitionXmlParser(); + } - /// - /// Initializes a new instance of the class. - /// - public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) - : base(context) + /// + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateCreatedPackageFilesToDb(); + } + + private void CreateDatabaseTable() + { + // Add CreatedPackage table in database if it doesn't exist + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) { - _packagesRepository = packagesRepository; - _xmlParser = new PackageDefinitionXmlParser(); + Create.Table().Do(); + } + } + + private void MigrateCreatedPackageFilesToDb() + { + // Load data from file + IEnumerable packages = _packagesRepository.GetAll().WhereNotNull(); + var createdPackageDtos = new List(); + foreach (PackageDefinition package in packages) + { + // Create dto from xmlDocument + var dto = new CreatedPackageSchemaDto + { + Name = package.Name, + Value = _xmlParser.ToXml(package).ToString(), + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid(), + }; + createdPackageDtos.Add(dto); } - private void CreateDatabaseTable() + _packagesRepository.DeleteLocalRepositoryFiles(); + if (createdPackageDtos.Any()) { - // Add CreatedPackage table in database if it doesn't exist - IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); - if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) - { - Create.Table().Do(); - } - } - - private void MigrateCreatedPackageFilesToDb() - { - // Load data from file - IEnumerable packages = _packagesRepository.GetAll().WhereNotNull(); - var createdPackageDtos = new List(); - foreach (PackageDefinition package in packages) - { - // Create dto from xmlDocument - var dto = new CreatedPackageSchemaDto() - { - Name = package.Name, - Value = _xmlParser.ToXml(package).ToString(), - UpdateDate = DateTime.Now, - PackageId = Guid.NewGuid() - }; - createdPackageDtos.Add(dto); - } - - _packagesRepository.DeleteLocalRepositoryFiles(); - if (createdPackageDtos.Any()) - { - // Insert dto into CreatedPackage table - Database.InsertBulk(createdPackageDtos); - } - } - - /// - protected override void Migrate() - { - CreateDatabaseTable(); - MigrateCreatedPackageFilesToDb(); + // Insert dto into CreatedPackage table + Database.InsertBulk(createdPackageDtos); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs index 6b74c49f67..c3f42a90ab 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs @@ -1,63 +1,63 @@ -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; + +public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase { - public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase + public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) + : base(context) { - public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) : base(context) - { - } + } - protected override void Migrate() + protected override void Migrate() + { + if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey")) { - if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey")) + var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; + var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId"; + + if (IndexExists(indexNameToRecreate)) { - var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; - var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId"; - - if (IndexExists(indexNameToRecreate)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do(); - } - - if (IndexExists(indexNameToDelete)) - { - // drop it since the previous migration index was wrong, and we - // need to modify a column that belons to it - Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do(); - } - - //special trick to add the column without constraints and return the sql to add them later - AddColumn("userOrMemberKey", out var sqls); - - //populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. - Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)").Do(); - - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls) Execute.Sql(sql).Do(); - - //now remove these old columns - Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do(); - - // create index with the correct definition - Create - .Index(indexNameToRecreate) - .OnTable(ExternalLoginDto.TableName) - .OnColumn("loginProvider").Ascending() - .OnColumn("userOrMemberKey").Ascending() - .WithOptions() - .Unique() - .WithOptions() - .NonClustered() - .Do(); + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do(); } + + if (IndexExists(indexNameToDelete)) + { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do(); + } + + // special trick to add the column without constraints and return the sql to add them later + AddColumn("userOrMemberKey", out IEnumerable sqls); + + // populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. + Execute.Sql( + $"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)") + .Do(); + + // now apply constraints (NOT NULL) to new table + foreach (var sql in sqls) + { + Execute.Sql(sql).Do(); + } + + // now remove these old columns + Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do(); + + // create index with the correct definition + Create + .Index(indexNameToRecreate) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userOrMemberKey").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); } - - } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs index 01cfb22a3d..550e67879a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/AddScheduledPublishingLock.cs @@ -1,15 +1,15 @@ +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 -{ - internal class AddScheduledPublishingLock : MigrationBase - { - public AddScheduledPublishingLock(IMigrationContext context) - : base(context) - { - } +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; - protected override void Migrate() => - Database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Cms.Core.Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); +internal class AddScheduledPublishingLock : MigrationBase +{ + public AddScheduledPublishingLock(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() => + Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.ScheduledPublishing, Name = "ScheduledPublishing" }); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs index 1c8fe7ed72..44144b93fe 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs @@ -1,34 +1,31 @@ -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 +internal class UpdateRelationTypesToHandleDependencies : MigrationBase { - internal class UpdateRelationTypesToHandleDependencies : MigrationBase + public UpdateRelationTypesToHandleDependencies(IMigrationContext context) + : base(context) { - public UpdateRelationTypesToHandleDependencies(IMigrationContext context) - : base(context) + } + + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "isDependency"); + + var aliasesWithDependencies = new[] { - } + Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Constants.Conventions.RelationTypes.RelatedMediaAlias, + }; - protected override void Migrate() - { - var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - - AddColumnIfNotExists(columns, "isDependency"); - - var aliasesWithDependencies = new[] - { - Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, - Core.Constants.Conventions.RelationTypes.RelatedMediaAlias - }; - - Database.Execute( - Sql() - .Update(u => u.Set(x => x.IsDependency, true)) - .WhereIn(x => x.Alias, aliasesWithDependencies)); - - } + Database.Execute( + Sql() + .Update(u => u.Set(x => x.IsDependency, true)) + .WhereIn(x => x.Alias, aliasesWithDependencies)); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs index e2eece8313..04a9a700b1 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorData.cs @@ -1,45 +1,48 @@ -using System; -using System.Collections.Generic; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Convertable block data from json +/// +public class BlockEditorData { - /// - /// Convertable block data from json - /// - public class BlockEditorData + private readonly string _propertyEditorAlias; + + public BlockEditorData( + string propertyEditorAlias, + IEnumerable references, + BlockValue blockValue) { - private readonly string _propertyEditorAlias; - - public static BlockEditorData Empty { get; } = new BlockEditorData(); - - private BlockEditorData() + if (string.IsNullOrWhiteSpace(propertyEditorAlias)) { - _propertyEditorAlias = string.Empty; - BlockValue = new BlockValue(); + throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias)); } - public BlockEditorData(string propertyEditorAlias, - IEnumerable references, - BlockValue blockValue) - { - if (string.IsNullOrWhiteSpace(propertyEditorAlias)) - throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias)); - _propertyEditorAlias = propertyEditorAlias; - BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue)); - References = references != null ? new List(references) : throw new ArgumentNullException(nameof(references)); - } - - /// - /// Returns the layout for this specific property editor - /// - public JToken? Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null; - - /// - /// Returns the reference to the original BlockValue - /// - public BlockValue BlockValue { get; } - - public List References { get; } = new List(); + _propertyEditorAlias = propertyEditorAlias; + BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue)); + References = references != null + ? new List(references) + : throw new ArgumentNullException(nameof(references)); } + + private BlockEditorData() + { + _propertyEditorAlias = string.Empty; + BlockValue = new BlockValue(); + } + + public static BlockEditorData Empty { get; } = new(); + + /// + /// Returns the layout for this specific property editor + /// + public JToken? Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out JToken? layout) ? layout : null; + + /// + /// Returns the reference to the original BlockValue + /// + public BlockValue BlockValue { get; } + + public List References { get; } = new(); } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs index 0389603ac2..5b125ce855 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockEditorDataConverter.cs @@ -1,68 +1,65 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Converts the block json data into objects +/// +public abstract class BlockEditorDataConverter { - /// - /// Converts the block json data into objects - /// - public abstract class BlockEditorDataConverter + private readonly string _propertyEditorAlias; + + protected BlockEditorDataConverter(string propertyEditorAlias) => _propertyEditorAlias = propertyEditorAlias; + + public BlockEditorData ConvertFrom(JToken json) { - private readonly string _propertyEditorAlias; + BlockValue? value = json.ToObject(); + return Convert(value); + } - protected BlockEditorDataConverter(string propertyEditorAlias) + public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) + { + try { - _propertyEditorAlias = propertyEditorAlias; + BlockValue? value = JsonConvert.DeserializeObject(json); + blockEditorData = Convert(value); + return true; + } + catch (Exception) + { + blockEditorData = null; + return false; + } + } + + public BlockEditorData Deserialize(string json) + { + BlockValue? value = JsonConvert.DeserializeObject(json); + return Convert(value); + } + + /// + /// Return the collection of from the block editor's Layout (which could be an array or + /// an object depending on the editor) + /// + /// + /// + protected abstract IEnumerable? GetBlockReferences(JToken jsonLayout); + + private BlockEditorData Convert(BlockValue? value) + { + if (value?.Layout == null) + { + return BlockEditorData.Empty; } - public BlockEditorData ConvertFrom(JToken json) - { - var value = json.ToObject(); - return Convert(value); - } - - public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) - { - try - { - var value = JsonConvert.DeserializeObject(json); - blockEditorData = Convert(value); - return true; - } - catch (System.Exception) - { - blockEditorData = null; - return false; - } - } - - public BlockEditorData Deserialize(string json) - { - var value = JsonConvert.DeserializeObject(json); - return Convert(value); - } - - private BlockEditorData Convert(BlockValue? value) - { - if (value?.Layout == null) - return BlockEditorData.Empty; - - var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout) + IEnumerable? references = + value.Layout.TryGetValue(_propertyEditorAlias, out JToken? layout) ? GetBlockReferences(layout) : Enumerable.Empty(); - return new BlockEditorData(_propertyEditorAlias, references!, value); - } - - /// - /// Return the collection of from the block editor's Layout (which could be an array or an object depending on the editor) - /// - /// - /// - protected abstract IEnumerable? GetBlockReferences(JToken jsonLayout); - + return new BlockEditorData(_propertyEditorAlias, references!, value); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs index a459a055ce..a99a62fea4 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockItemData.cs @@ -1,62 +1,61 @@ -using System; -using System.Collections.Generic; using Newtonsoft.Json; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +/// +/// Represents a single block's data in raw form +/// +public class BlockItemData { + [JsonProperty("contentTypeKey")] + public Guid ContentTypeKey { get; set; } + /// - /// Represents a single block's data in raw form + /// not serialized, manually set and used during internally /// - public class BlockItemData + [JsonIgnore] + public string ContentTypeAlias { get; set; } = string.Empty; + + [JsonProperty("udi")] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? Udi { get; set; } + + [JsonIgnore] + public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); + + /// + /// The remaining properties will be serialized to a dictionary + /// + /// + /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket + /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm + /// NestedContent serializes to string, int, whatever eg + /// "stringValue":"Some String","numericValue":125,"otherNumeric":null + /// + [JsonExtensionData] + public Dictionary RawPropertyValues { get; set; } = new(); + + /// + /// Used during deserialization to convert the raw property data into data with a property type context + /// + [JsonIgnore] + public IDictionary PropertyValues { get; set; } = + new Dictionary(); + + /// + /// Used during deserialization to populate the property value/property type of a block item content property + /// + public class BlockPropertyValue { - [JsonProperty("contentTypeKey")] - public Guid ContentTypeKey { get; set; } - - /// - /// not serialized, manually set and used during internally - /// - [JsonIgnore] - public string ContentTypeAlias { get; set; } = string.Empty; - - [JsonProperty("udi")] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? Udi { get; set; } - - [JsonIgnore] - public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); - - /// - /// The remaining properties will be serialized to a dictionary - /// - /// - /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket - /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm - /// NestedContent serializes to string, int, whatever eg - /// "stringValue":"Some String","numericValue":125,"otherNumeric":null - /// - [JsonExtensionData] - public Dictionary RawPropertyValues { get; set; } = new Dictionary(); - - /// - /// Used during deserialization to convert the raw property data into data with a property type context - /// - [JsonIgnore] - public IDictionary PropertyValues { get; set; } = new Dictionary(); - - /// - /// Used during deserialization to populate the property value/property type of a block item content property - /// - public class BlockPropertyValue + public BlockPropertyValue(object? value, IPropertyType propertyType) { - public BlockPropertyValue(object? value, IPropertyType propertyType) - { - Value = value; - PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - } - - public object? Value { get; } - public IPropertyType PropertyType { get; } + Value = value; + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); } + + public object? Value { get; } + + public IPropertyType PropertyType { get; } } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs index 3d6c49c2e9..289a4245ec 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListEditorDataConverter.cs @@ -1,22 +1,20 @@ -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks -{ - /// - /// Data converter for the block list property editor - /// - public class BlockListEditorDataConverter : BlockEditorDataConverter - { - public BlockListEditorDataConverter() : base(Cms.Core.Constants.PropertyEditors.Aliases.BlockList) - { - } +namespace Umbraco.Cms.Core.Models.Blocks; - protected override IEnumerable? GetBlockReferences(JToken jsonLayout) - { - var blockListLayout = jsonLayout.ToObject>(); - return blockListLayout?.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); - } +/// +/// Data converter for the block list property editor +/// +public class BlockListEditorDataConverter : BlockEditorDataConverter +{ + public BlockListEditorDataConverter() + : base(Constants.PropertyEditors.Aliases.BlockList) + { + } + + protected override IEnumerable? GetBlockReferences(JToken jsonLayout) + { + IEnumerable? blockListLayout = jsonLayout.ToObject>(); + return blockListLayout?.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs index 6df34079f4..f98372be59 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockListLayoutItem.cs @@ -1,19 +1,18 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Umbraco.Cms.Infrastructure.Serialization; -namespace Umbraco.Cms.Core.Models.Blocks -{ - /// - /// Used for deserializing the block list layout - /// - public class BlockListLayoutItem - { - [JsonProperty("contentUdi", Required = Required.Always)] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? ContentUdi { get; set; } +namespace Umbraco.Cms.Core.Models.Blocks; - [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi? SettingsUdi { get; set; } - } +/// +/// Used for deserializing the block list layout +/// +public class BlockListLayoutItem +{ + [JsonProperty("contentUdi", Required = Required.Always)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? ContentUdi { get; set; } + + [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi? SettingsUdi { get; set; } } diff --git a/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs b/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs index 3b6df71422..aa7498f9f5 100644 --- a/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs +++ b/src/Umbraco.Infrastructure/Models/Blocks/BlockValue.cs @@ -1,18 +1,16 @@ -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models.Blocks +namespace Umbraco.Cms.Core.Models.Blocks; + +public class BlockValue { - public class BlockValue - { - [JsonProperty("layout")] - public IDictionary Layout { get; set; } = null!; + [JsonProperty("layout")] + public IDictionary Layout { get; set; } = null!; - [JsonProperty("contentData")] - public List ContentData { get; set; } = new List(); + [JsonProperty("contentData")] + public List ContentData { get; set; } = new(); - [JsonProperty("settingsData")] - public List SettingsData { get; set; } = new List(); - } + [JsonProperty("settingsData")] + public List SettingsData { get; set; } = new(); } diff --git a/src/Umbraco.Infrastructure/Models/GridValue.cs b/src/Umbraco.Infrastructure/Models/GridValue.cs index a83d235b55..29e88ea564 100644 --- a/src/Umbraco.Infrastructure/Models/GridValue.cs +++ b/src/Umbraco.Infrastructure/Models/GridValue.cs @@ -1,87 +1,84 @@ -using System; -using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Umbraco.Cms.Core.Models -{ - // TODO: Make a property value converter for this! +namespace Umbraco.Cms.Core.Models; - /// - /// A model representing the value saved for the grid - /// - public class GridValue +// TODO: Make a property value converter for this! + +/// +/// A model representing the value saved for the grid +/// +public class GridValue +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("sections")] + public IEnumerable Sections { get; set; } = null!; + + public class GridSection + { + [JsonProperty("grid")] + public string? Grid { get; set; } // TODO: what is this? + + [JsonProperty("rows")] + public IEnumerable Rows { get; set; } = null!; + } + + public class GridRow { [JsonProperty("name")] public string? Name { get; set; } - [JsonProperty("sections")] - public IEnumerable Sections { get; set; } = null!; + [JsonProperty("id")] + public Guid Id { get; set; } - public class GridSection - { - [JsonProperty("grid")] - public string? Grid { get; set; } // TODO: what is this? + [JsonProperty("areas")] + public IEnumerable Areas { get; set; } = null!; - [JsonProperty("rows")] - public IEnumerable Rows { get; set; } = null!; - } + [JsonProperty("styles")] + public JToken? Styles { get; set; } - public class GridRow - { - [JsonProperty("name")] - public string? Name { get; set; } + [JsonProperty("config")] + public JToken? Config { get; set; } + } - [JsonProperty("id")] - public Guid Id { get; set; } + public class GridArea + { + [JsonProperty("grid")] + public string? Grid { get; set; } // TODO: what is this? - [JsonProperty("areas")] - public IEnumerable Areas { get; set; } = null!; + [JsonProperty("controls")] + public IEnumerable Controls { get; set; } = null!; - [JsonProperty("styles")] - public JToken? Styles { get; set; } + [JsonProperty("styles")] + public JToken? Styles { get; set; } - [JsonProperty("config")] - public JToken? Config { get; set; } - } + [JsonProperty("config")] + public JToken? Config { get; set; } + } - public class GridArea - { - [JsonProperty("grid")] - public string? Grid { get; set; } // TODO: what is this? + public class GridControl + { + [JsonProperty("value")] + public JToken? Value { get; set; } - [JsonProperty("controls")] - public IEnumerable Controls { get; set; } = null!; + [JsonProperty("editor")] + public GridEditor Editor { get; set; } = null!; - [JsonProperty("styles")] - public JToken? Styles { get; set; } + [JsonProperty("styles")] + public JToken? Styles { get; set; } - [JsonProperty("config")] - public JToken? Config { get; set; } - } + [JsonProperty("config")] + public JToken? Config { get; set; } + } - public class GridControl - { - [JsonProperty("value")] - public JToken? Value { get; set; } + public class GridEditor + { + [JsonProperty("alias")] + public string Alias { get; set; } = null!; - [JsonProperty("editor")] - public GridEditor Editor { get; set; } = null!; - - [JsonProperty("styles")] - public JToken? Styles { get; set; } - - [JsonProperty("config")] - public JToken? Config { get; set; } - } - - public class GridEditor - { - [JsonProperty("alias")] - public string Alias { get; set; } = null!; - - [JsonProperty("view")] - public string? View { get; set; } - } + [JsonProperty("view")] + public string? View { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs index abb987c119..c133277dca 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Globalization; using Examine; using Umbraco.Cms.Core.Mapping; @@ -9,283 +7,319 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models.Mapping +namespace Umbraco.Cms.Core.Models.Mapping; + +public class EntityMapDefinition : IMapDefinition { - public class EntityMapDefinition : IMapDefinition + public void DefineMaps(IUmbracoMapper mapper) { - public void DefineMaps(IUmbracoMapper mapper) + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new ContentTypeSort(), Map); + mapper.Define((source, context) => new EntityBasic(), Map); + mapper.Define((source, context) => new SearchResultEntity(), Map); + mapper.Define((source, context) => new SearchResultEntity(), Map); + mapper.Define>((source, context) => + context.MapEnumerable(source).WhereNotNull()); + mapper.Define, IEnumerable>((source, context) => + context.MapEnumerable(source).WhereNotNull()); + } + + // Umbraco.Code.MapAll -Alias + private static void Map(IEntitySlim source, EntityBasic target, MapperContext context) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = MapName(source, context); + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Trashed = source.Trashed; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (source is IContentEntitySlim contentSlim) { - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new ContentTypeSort(), Map); - mapper.Define((source, context) => new EntityBasic(), Map); - mapper.Define((source, context) => new SearchResultEntity(), Map); - mapper.Define((source, context) => new SearchResultEntity(), Map); - mapper.Define>((source, context) => context.MapEnumerable(source).WhereNotNull()); - mapper.Define, IEnumerable>((source, context) => context.MapEnumerable(source).WhereNotNull()); + source.AdditionalData!["ContentTypeAlias"] = contentSlim.ContentTypeAlias; } - // Umbraco.Code.MapAll -Alias - private static void Map(IEntitySlim source, EntityBasic target, MapperContext context) + if (source is IDocumentEntitySlim documentSlim) { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = MapName(source, context); - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Trashed = source.Trashed; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - - if (source is IContentEntitySlim contentSlim) - { - source.AdditionalData!["ContentTypeAlias"] = contentSlim.ContentTypeAlias; - } - - if (source is IDocumentEntitySlim documentSlim) - { - source.AdditionalData!["IsPublished"] = documentSlim.Published; - } - - if (source is IMediaEntitySlim mediaSlim) - { - if (source.AdditionalData is not null) - { - //pass UpdateDate for MediaPicker ListView ordering - source.AdditionalData["UpdateDate"] = mediaSlim.UpdateDate; - source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; - } - } + source.AdditionalData!["IsPublished"] = documentSlim.Published; + } + if (source is IMediaEntitySlim mediaSlim) + { if (source.AdditionalData is not null) { - // NOTE: we're mapping the objects in AdditionalData by object reference here. - // it works fine for now, but it's something to keep in mind in the future - foreach(var kvp in source.AdditionalData) + // pass UpdateDate for MediaPicker ListView ordering + source.AdditionalData["UpdateDate"] = mediaSlim.UpdateDate; + source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; + } + } + + if (source.AdditionalData is not null) + { + // NOTE: we're mapping the objects in AdditionalData by object reference here. + // it works fine for now, but it's something to keep in mind in the future + foreach (KeyValuePair kvp in source.AdditionalData) + { + if (kvp.Value is not null) { - if (kvp.Value is not null) - { - target.AdditionalData[kvp.Key] = kvp.Value; - } + target.AdditionalData[kvp.Key] = kvp.Value; } } - - target.AdditionalData.Add("IsContainer", source.IsContainer); } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(PropertyType source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = "icon-box"; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + target.AdditionalData.Add("IsContainer", source.IsContainer); + } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(PropertyGroup source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = "icon-tab"; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(PropertyType source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = "icon-box"; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = string.Empty; + } - // Umbraco.Code.MapAll -Udi -Trashed - private static void Map(IUser source, EntityBasic target, MapperContext context) - { - target.Alias = source.Username; - target.Icon = Constants.Icons.User; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = ""; - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(PropertyGroup source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = "icon-tab"; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = string.Empty; + } - // Umbraco.Code.MapAll -Trashed - private static void Map(ITemplate source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = Constants.Icons.Template; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = -1; - target.Path = source.Path; - target.Udi = Udi.Create(Constants.UdiEntityType.Template, source.Key); - } + // Umbraco.Code.MapAll -Udi -Trashed + private static void Map(IUser source, EntityBasic target, MapperContext context) + { + target.Alias = source.Username; + target.Icon = Constants.Icons.User; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = string.Empty; + } - // Umbraco.Code.MapAll -SortOrder - private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context) - { - target.Alias = source.Alias; - target.Id = new Lazy(() => Convert.ToInt32(source.Id)); - } + // Umbraco.Code.MapAll -Trashed + private static void Map(ITemplate source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = Constants.Icons.Template; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = -1; + target.Path = source.Path; + target.Udi = Udi.Create(Constants.UdiEntityType.Template, source.Key); + } - // Umbraco.Code.MapAll -Trashed - private static void Map(IContentTypeComposition source, EntityBasic target, MapperContext context) - { - target.Alias = source.Alias; - target.Icon = source.Icon; - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = ContentTypeMapDefinition.MapContentTypeUdi(source); - } + // Umbraco.Code.MapAll -SortOrder + private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context) + { + target.Alias = source.Alias; + target.Id = new Lazy(() => Convert.ToInt32(source.Id)); + } - // Umbraco.Code.MapAll -Trashed -Alias -Score - private static void Map(EntitySlim source, SearchResultEntity target, MapperContext context) - { - target.Icon = MapContentTypeIcon(source); - target.Id = source.Id; - target.Key = source.Key; - target.Name = source.Name; - target.ParentId = source.ParentId; - target.Path = source.Path; - target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + // Umbraco.Code.MapAll -Trashed + private static void Map(IContentTypeComposition source, EntityBasic target, MapperContext context) + { + target.Alias = source.Alias; + target.Icon = source.Icon; + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = ContentTypeMapDefinition.MapContentTypeUdi(source); + } - if (target.Icon.IsNullOrWhiteSpace()) + // Umbraco.Code.MapAll -Trashed -Alias -Score + private static void Map(EntitySlim source, SearchResultEntity target, MapperContext context) + { + target.Icon = MapContentTypeIcon(source); + target.Id = source.Id; + target.Key = source.Key; + target.Name = source.Name; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); + + if (target.Icon.IsNullOrWhiteSpace()) + { + if (source.NodeObjectType == Constants.ObjectTypes.Document) { - if (source.NodeObjectType == Constants.ObjectTypes.Document) - target.Icon = Constants.Icons.Content; - if (source.NodeObjectType == Constants.ObjectTypes.Media) - target.Icon = Constants.Icons.Content; - if (source.NodeObjectType == Constants.ObjectTypes.Member) - target.Icon = Constants.Icons.Member; - else if (source.NodeObjectType == Constants.ObjectTypes.DataType) - target.Icon = Constants.Icons.DataType; - else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) - target.Icon = Constants.Icons.ContentType; - else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) - target.Icon = Constants.Icons.MediaType; - else if (source.NodeObjectType == Constants.ObjectTypes.MemberType) - target.Icon = Constants.Icons.MemberType; - else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) - target.Icon = Constants.Icons.Template; - } - } - - // Umbraco.Code.MapAll -Alias -Trashed - private static void Map(ISearchResult source, SearchResultEntity target, MapperContext context) - { - target.Id = source.Id; - target.Score = source.Score; - - // TODO: Properly map this (not aftermap) - - //get the icon if there is one - target.Icon = source.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) - ? source.Values[UmbracoExamineFieldNames.IconFieldName] - : Constants.Icons.DefaultIcon; - - target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] : "[no name]"; - - var culture = context.GetCulture()?.ToLowerInvariant(); - if(culture.IsNullOrWhiteSpace() == false) - { - target.Name = source.Values.ContainsKey($"nodeName_{culture}") ? source.Values[$"nodeName_{culture}"] : target.Name; + target.Icon = Constants.Icons.Content; } - if (source.Values.TryGetValue(UmbracoExamineFieldNames.UmbracoFileFieldName, out var umbracoFile) && - umbracoFile.IsNullOrWhiteSpace() == false) + if (source.NodeObjectType == Constants.ObjectTypes.Media) { - if (umbracoFile != null) - { - target.Name = $"{target.Name} ({umbracoFile})"; - } + target.Icon = Constants.Icons.Content; } - if (source.Values.ContainsKey(UmbracoExamineFieldNames.NodeKeyFieldName)) + if (source.NodeObjectType == Constants.ObjectTypes.Member) { - if (Guid.TryParse(source.Values[UmbracoExamineFieldNames.NodeKeyFieldName], out var key)) - { - target.Key = key; - - //need to set the UDI - if (source.Values.ContainsKey(ExamineFieldNames.CategoryFieldName)) - { - switch (source.Values[ExamineFieldNames.CategoryFieldName]) - { - case IndexTypes.Member: - target.Udi = new GuidUdi(Constants.UdiEntityType.Member, target.Key); - break; - case IndexTypes.Content: - target.Udi = new GuidUdi(Constants.UdiEntityType.Document, target.Key); - break; - case IndexTypes.Media: - target.Udi = new GuidUdi(Constants.UdiEntityType.Media, target.Key); - break; - } - } - } + target.Icon = Constants.Icons.Member; } - - if (source.Values.ContainsKey("parentID")) + else if (source.NodeObjectType == Constants.ObjectTypes.DataType) { - if (int.TryParse(source.Values["parentID"], NumberStyles.Integer, CultureInfo.InvariantCulture,out var parentId)) - { - target.ParentId = parentId; - } - else - { - target.ParentId = -1; - } + target.Icon = Constants.Icons.DataType; } - - target.Path = source.Values.ContainsKey(UmbracoExamineFieldNames.IndexPathFieldName) ? source.Values[UmbracoExamineFieldNames.IndexPathFieldName] : ""; - - if (source.Values.ContainsKey(ExamineFieldNames.ItemTypeFieldName)) + else if (source.NodeObjectType == Constants.ObjectTypes.DocumentType) { - target.AdditionalData.Add("contentType", source.Values[ExamineFieldNames.ItemTypeFieldName]); + target.Icon = Constants.Icons.ContentType; } - } - - private static string? MapContentTypeIcon(IEntitySlim entity) - { - switch (entity) + else if (source.NodeObjectType == Constants.ObjectTypes.MediaType) { - case IMemberEntitySlim memberEntity: - return memberEntity.ContentTypeIcon; - case IContentEntitySlim contentEntity: - // NOTE: this case covers both content and media entities - return contentEntity.ContentTypeIcon; + target.Icon = Constants.Icons.MediaType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.MemberType) + { + target.Icon = Constants.Icons.MemberType; + } + else if (source.NodeObjectType == Constants.ObjectTypes.TemplateType) + { + target.Icon = Constants.Icons.Template; } - - return null; - } - - private static string MapName(IEntitySlim source, MapperContext context) - { - if (!(source is DocumentEntitySlim doc)) - return source.Name!; - - // invariant = only 1 name - if (!doc.Variations.VariesByCulture()) return source.Name!; - - // variant = depends on culture - var culture = context.GetCulture(); - - // if there's no culture here, the issue is somewhere else (UI, whatever) - throw! - if (culture == null) - //throw new InvalidOperationException("Missing culture in mapping options."); - // TODO: we should throw, but this is used in various places that won't set a culture yet - return source.Name!; - - // if we don't have a name for a culture, it means the culture is not available, and - // hey we should probably not be mapping it, but it's too late, return a fallback name - return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() ? name : $"({source.Name})"; } } + + // Umbraco.Code.MapAll -Alias -Trashed + private static void Map(ISearchResult source, SearchResultEntity target, MapperContext context) + { + target.Id = source.Id; + target.Score = source.Score; + + // TODO: Properly map this (not aftermap) + + // get the icon if there is one + target.Icon = source.Values.ContainsKey(UmbracoExamineFieldNames.IconFieldName) + ? source.Values[UmbracoExamineFieldNames.IconFieldName] + : Constants.Icons.DefaultIcon; + + target.Name = source.Values.ContainsKey(UmbracoExamineFieldNames.NodeNameFieldName) + ? source.Values[UmbracoExamineFieldNames.NodeNameFieldName] + : "[no name]"; + + var culture = context.GetCulture()?.ToLowerInvariant(); + if (culture.IsNullOrWhiteSpace() == false) + { + target.Name = source.Values.ContainsKey($"nodeName_{culture}") + ? source.Values[$"nodeName_{culture}"] + : target.Name; + } + + if (source.Values.TryGetValue(UmbracoExamineFieldNames.UmbracoFileFieldName, out var umbracoFile) && + umbracoFile.IsNullOrWhiteSpace() == false) + { + if (umbracoFile != null) + { + target.Name = $"{target.Name} ({umbracoFile})"; + } + } + + if (source.Values.ContainsKey(UmbracoExamineFieldNames.NodeKeyFieldName)) + { + if (Guid.TryParse(source.Values[UmbracoExamineFieldNames.NodeKeyFieldName], out Guid key)) + { + target.Key = key; + + // need to set the UDI + if (source.Values.ContainsKey(ExamineFieldNames.CategoryFieldName)) + { + switch (source.Values[ExamineFieldNames.CategoryFieldName]) + { + case IndexTypes.Member: + target.Udi = new GuidUdi(Constants.UdiEntityType.Member, target.Key); + break; + case IndexTypes.Content: + target.Udi = new GuidUdi(Constants.UdiEntityType.Document, target.Key); + break; + case IndexTypes.Media: + target.Udi = new GuidUdi(Constants.UdiEntityType.Media, target.Key); + break; + } + } + } + } + + if (source.Values.ContainsKey("parentID")) + { + if (int.TryParse(source.Values["parentID"], NumberStyles.Integer, CultureInfo.InvariantCulture, + out var parentId)) + { + target.ParentId = parentId; + } + else + { + target.ParentId = -1; + } + } + + target.Path = source.Values.ContainsKey(UmbracoExamineFieldNames.IndexPathFieldName) + ? source.Values[UmbracoExamineFieldNames.IndexPathFieldName] + : string.Empty; + + if (source.Values.ContainsKey(ExamineFieldNames.ItemTypeFieldName)) + { + target.AdditionalData.Add("contentType", source.Values[ExamineFieldNames.ItemTypeFieldName]); + } + } + + private static string? MapContentTypeIcon(IEntitySlim entity) + { + switch (entity) + { + case IMemberEntitySlim memberEntity: + return memberEntity.ContentTypeIcon; + case IContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; + } + + return null; + } + + private static string MapName(IEntitySlim source, MapperContext context) + { + if (!(source is DocumentEntitySlim doc)) + { + return source.Name!; + } + + // invariant = only 1 name + if (!doc.Variations.VariesByCulture()) + { + return source.Name!; + } + + // variant = depends on culture + var culture = context.GetCulture(); + + // if there's no culture here, the issue is somewhere else (UI, whatever) - throw! + if (culture == null) + + // throw new InvalidOperationException("Missing culture in mapping options."); + // TODO: we should throw, but this is used in various places that won't set a culture yet + { + return source.Name!; + } + + // if we don't have a name for a culture, it means the culture is not available, and + // hey we should probably not be mapping it, but it's too late, return a fallback name + return doc.CultureNames.TryGetValue(culture, out var name) && !name.IsNullOrWhiteSpace() + ? name + : $"({source.Name})"; + } } diff --git a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs index 9e5101550a..04e1a6825d 100644 --- a/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs +++ b/src/Umbraco.Infrastructure/Models/MediaWithCrops.cs @@ -1,79 +1,73 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Represents a media item with local crops. +/// +/// +public class MediaWithCrops : PublishedContentWrapped { /// - /// Represents a media item with local crops. + /// Initializes a new instance of the class. /// - /// - public class MediaWithCrops : PublishedContentWrapped - { - - /// - /// Gets the content/media item. - /// - /// - /// The content/media item. - /// - public IPublishedContent Content => Unwrap(); - - /// - /// Gets the local crops. - /// - /// - /// The local crops. - /// - public ImageCropperValue LocalCrops { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The published value fallback. - /// The local crops. - public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) - : base(content, publishedValueFallback) - { - LocalCrops = localCrops; - } - } + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(IPublishedContent content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) + : base(content, publishedValueFallback) => + LocalCrops = localCrops; /// - /// Represents a media item with local crops. + /// Gets the content/media item. /// - /// The type of the media item. - /// - public class MediaWithCrops : MediaWithCrops - where T : IPublishedContent - { - /// - /// Gets the media item. - /// - /// - /// The media item. - /// - public new T Content { get; } + /// + /// The content/media item. + /// + public IPublishedContent Content => Unwrap(); - /// - /// Initializes a new instance of the class. - /// - /// The content. - /// The published value fallback. - /// The local crops. - public MediaWithCrops(T content,IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) - : base(content, publishedValueFallback, localCrops) - { - Content = content; - } - - /// - /// Performs an implicit conversion from to . - /// - /// The media with crops. - /// - /// The result of the conversion. - /// - public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; - } + /// + /// Gets the local crops. + /// + /// + /// The local crops. + /// + public ImageCropperValue LocalCrops { get; } +} + +/// +/// Represents a media item with local crops. +/// +/// The type of the media item. +/// +public class MediaWithCrops : MediaWithCrops + where T : IPublishedContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The content. + /// The published value fallback. + /// The local crops. + public MediaWithCrops(T content, IPublishedValueFallback publishedValueFallback, ImageCropperValue localCrops) + : base(content, publishedValueFallback, localCrops) => + Content = content; + + /// + /// Gets the media item. + /// + /// + /// The media item. + /// + public new T Content { get; } + + /// + /// Performs an implicit conversion from to . + /// + /// The media with crops. + /// + /// The result of the conversion. + /// + public static implicit operator T(MediaWithCrops mediaWithCrops) => mediaWithCrops.Content; } diff --git a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs index e7286d683f..a14137338d 100644 --- a/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs +++ b/src/Umbraco.Infrastructure/Models/PathValidationExtensions.cs @@ -1,116 +1,134 @@ -using System; -using System.IO; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Models +namespace Umbraco.Cms.Core.Models; + +/// +/// Provides extension methods for path validation. +/// +internal static class PathValidationExtensions { /// - /// Provides extension methods for path validation. + /// Does a quick check on the entity's set path to ensure that it's valid and consistent /// - internal static class PathValidationExtensions + /// + /// + public static void ValidatePathWithException(this NodeDto entity) { - /// - /// Does a quick check on the entity's set path to ensure that it's valid and consistent - /// - /// - /// - public static void ValidatePathWithException(this NodeDto entity) + // don't validate if it's empty and it has no id + if (entity.NodeId == default && entity.Path.IsNullOrWhiteSpace()) { - //don't validate if it's empty and it has no id - if (entity.NodeId == default(int) && entity.Path.IsNullOrWhiteSpace()) - return; - - if (entity.Path.IsNullOrWhiteSpace()) - throw new InvalidDataException($"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}"); - - var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (pathParts.Length < 2) - { - //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id - throw new InvalidDataException($"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); - } - - if (entity.ParentId != default(int) && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) - { - //the 2nd last id in the path must be it's parent id - throw new InvalidDataException($"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); - } + return; } - /// - /// Does a quick check on the entity's set path to ensure that it's valid and consistent - /// - /// - /// - public static bool ValidatePath(this IUmbracoEntity entity) + if (entity.Path.IsNullOrWhiteSpace()) { - //don't validate if it's empty and it has no id - if (entity.HasIdentity == false && entity.Path.IsNullOrWhiteSpace()) - return true; + throw new InvalidDataException( + $"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}"); + } - if (entity.Path.IsNullOrWhiteSpace()) - return false; + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + { + // a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id + throw new InvalidDataException( + $"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); + } - var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); - if (pathParts.Length < 2) - { - //a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id - return false; - } - - if (entity.ParentId != default(int) && pathParts[pathParts.Length - 2] != entity.ParentId.ToInvariantString()) - { - //the 2nd last id in the path must be it's parent id - return false; - } + if (entity.ParentId != default && pathParts[^2] != entity.ParentId.ToInvariantString()) + { + // the 2nd last id in the path must be it's parent id + throw new InvalidDataException( + $"The content item {entity.NodeId} has an invalid path: {entity.Path} with parentID: {entity.ParentId}"); + } + } + /// + /// Does a quick check on the entity's set path to ensure that it's valid and consistent + /// + /// + /// + public static bool ValidatePath(this IUmbracoEntity entity) + { + // don't validate if it's empty and it has no id + if (entity.HasIdentity == false && entity.Path.IsNullOrWhiteSpace()) + { return true; } - /// - /// This will validate the entity's path and if it's invalid it will fix it, if fixing is required it will recursively - /// check and fix all ancestors if required. - /// - /// - /// - /// A callback specified to retrieve the parent entity of the entity - /// A callback specified to update a fixed entity - public static void EnsureValidPath(this T entity, - ILogger logger, - Func getParent, - Action update) - where T: IUmbracoEntity + if (entity.Path.IsNullOrWhiteSpace()) { - if (entity.HasIdentity == false) - throw new InvalidOperationException("Could not ensure the entity path, the entity has not been assigned an identity"); - - if (entity.ValidatePath() == false) - { - logger.LogWarning("The content item {EntityId} has an invalid path: {EntityPath} with parentID: {EntityParentId}", entity.Id, entity.Path, entity.ParentId); - if (entity.ParentId == -1) - { - entity.Path = string.Concat("-1,", entity.Id); - //path changed, update it - update(entity); - } - else - { - var parent = getParent(entity); - if (parent == null) - throw new NullReferenceException("Could not ensure path for entity " + entity.Id + " could not resolve it's parent " + entity.ParentId); - - //the parent must also be valid! - parent.EnsureValidPath(logger, getParent, update); - - entity.Path = string.Concat(parent.Path, ",", entity.Id); - //path changed, update it - update(entity); - } - } + return false; } + var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + { + // a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id + return false; + } + + if (entity.ParentId != default && pathParts[^2] != entity.ParentId.ToInvariantString()) + { + // the 2nd last id in the path must be it's parent id + return false; + } + + return true; + } + + /// + /// This will validate the entity's path and if it's invalid it will fix it, if fixing is required it will recursively + /// check and fix all ancestors if required. + /// + /// + /// + /// A callback specified to retrieve the parent entity of the entity + /// A callback specified to update a fixed entity + public static void EnsureValidPath( + this T entity, + ILogger logger, + Func getParent, + Action update) + where T : IUmbracoEntity + { + if (entity.HasIdentity == false) + { + throw new InvalidOperationException( + "Could not ensure the entity path, the entity has not been assigned an identity"); + } + + if (entity.ValidatePath() == false) + { + logger.LogWarning( + "The content item {EntityId} has an invalid path: {EntityPath} with parentID: {EntityParentId}", + entity.Id, entity.Path, entity.ParentId); + if (entity.ParentId == -1) + { + entity.Path = string.Concat("-1,", entity.Id); + + // path changed, update it + update(entity); + } + else + { + T? parent = getParent(entity); + if (parent == null) + { + throw new NullReferenceException("Could not ensure path for entity " + entity.Id + + " could not resolve it's parent " + entity.ParentId); + } + + // the parent must also be valid! + parent.EnsureValidPath(logger, getParent, update); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + + // path changed, update it + update(entity); + } + } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs index fc123d485c..aad8417af9 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ApiVersion.cs @@ -1,33 +1,33 @@ -using System; using System.Reflection; using Umbraco.Cms.Core.Semver; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Manages API version handshake between client and server. +/// +public class ApiVersion { /// - /// Manages API version handshake between client and server. + /// Initializes a new instance of the class. /// - public class ApiVersion - { - /// - /// Initializes a new instance of the class. - /// - /// The currently executing version. - /// - internal ApiVersion(SemVersion executingVersion) => Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); + /// The currently executing version. + /// + internal ApiVersion(SemVersion executingVersion) => + Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); - private static SemVersion CurrentAssemblyVersion - => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute()!.InformationalVersion); + /// + /// Gets the currently executing API version. + /// + public static ApiVersion Current { get; } + = new(CurrentAssemblyVersion); - /// - /// Gets the currently executing API version. - /// - public static ApiVersion Current { get; } - = new ApiVersion(CurrentAssemblyVersion); + private static SemVersion CurrentAssemblyVersion + => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute()! + .InformationalVersion); - /// - /// Gets the executing version of the API. - /// - public SemVersion Version { get; } - } + /// + /// Gets the executing version of the API. + /// + public SemVersion Version { get; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs index c61de4ada4..5f7d018b67 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/AutoModelsNotificationHandler.cs @@ -1,136 +1,131 @@ -using System; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Notification handlers used by . +/// +/// +/// supports mode but not mode. +/// +public sealed class AutoModelsNotificationHandler : INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { + private static int _req; + private readonly ModelsBuilderSettings _config; + private readonly ILogger _logger; + private readonly IMainDom _mainDom; + private readonly ModelsGenerationError _mbErrors; + private readonly ModelsGenerator _modelGenerator; + /// - /// Notification handlers used by . + /// Initializes a new instance of the class. /// - /// - /// supports mode but not mode. - /// - public sealed class AutoModelsNotificationHandler : INotificationHandler, - INotificationHandler, - INotificationHandler, - INotificationHandler + public AutoModelsNotificationHandler( + ILogger logger, + IOptionsMonitor config, + ModelsGenerator modelGenerator, + ModelsGenerationError mbErrors, + IMainDom mainDom) { - private static int s_req; - private readonly ILogger _logger; - private readonly ModelsBuilderSettings _config; - private readonly ModelsGenerator _modelGenerator; - private readonly ModelsGenerationError _mbErrors; - private readonly IMainDom _mainDom; + _logger = logger; - /// - /// Initializes a new instance of the class. - /// - - public AutoModelsNotificationHandler( - ILogger logger, - IOptionsMonitor config, - ModelsGenerator modelGenerator, - ModelsGenerationError mbErrors, - IMainDom mainDom) + // We cant use IOptionsSnapshot here, cause this is used in the Core runtime, and that cannot use a scoped service as it has no scope + _config = config.CurrentValue ?? throw new ArgumentNullException(nameof(config)); + _modelGenerator = modelGenerator; + _mbErrors = mbErrors; + _mainDom = mainDom; + } + + // we do not manage InMemory models here + internal bool IsEnabled => _config.ModelsMode.IsAutoNotInMemory(); + + public void Handle(ContentTypeCacheRefresherNotification notification) => RequestModelsGeneration(); + + public void Handle(DataTypeCacheRefresherNotification notification) => RequestModelsGeneration(); + + /// + /// Handles the notification + /// + public void Handle(UmbracoApplicationStartingNotification notification) => Install(); + + public void Handle(UmbracoRequestEndNotification notification) + { + if (IsEnabled && _mainDom.IsMainDom) { - _logger = logger; - //We cant use IOptionsSnapshot here, cause this is used in the Core runtime, and that cannot use a scoped service as it has no scope - _config = config.CurrentValue ?? throw new ArgumentNullException(nameof(config)); - _modelGenerator = modelGenerator; - _mbErrors = mbErrors; - _mainDom = mainDom; + GenerateModelsIfRequested(); + } + } + + private void Install() + { + // don't run if not enabled + if (!IsEnabled) + { + } + } + + // NOTE + // CacheUpdated triggers within some asynchronous backend task where + // we have no HttpContext. So we use a static (non request-bound) + // var to register that models + // need to be generated. Could be by another request. Anyway. We could + // have collisions but... you know the risk. + private void RequestModelsGeneration() + { + if (!_mainDom.IsMainDom) + { + return; } - // we do not manage InMemory models here - internal bool IsEnabled => _config.ModelsMode.IsAutoNotInMemory(); + _logger.LogDebug("Requested to generate models."); - /// - /// Handles the notification - /// - public void Handle(UmbracoApplicationStartingNotification notification) => Install(); + Interlocked.Exchange(ref _req, 1); + } - private void Install() + private void GenerateModelsIfRequested() + { + if (Interlocked.Exchange(ref _req, 0) == 0) { - // don't run if not enabled - if (!IsEnabled) - { - return; - } + return; } - // NOTE - // CacheUpdated triggers within some asynchronous backend task where - // we have no HttpContext. So we use a static (non request-bound) - // var to register that models - // need to be generated. Could be by another request. Anyway. We could - // have collisions but... you know the risk. - - private void RequestModelsGeneration() + // cannot proceed unless we are MainDom + if (_mainDom.IsMainDom) { - if (!_mainDom.IsMainDom) + try { - return; + _logger.LogDebug("Generate models..."); + _logger.LogInformation("Generate models now."); + _modelGenerator.GenerateModels(); + _mbErrors.Clear(); + _logger.LogInformation("Generated."); } - - _logger.LogDebug("Requested to generate models."); - - Interlocked.Exchange(ref s_req, 1); - } - - private void GenerateModelsIfRequested() - { - if (Interlocked.Exchange(ref s_req, 0) == 0) + catch (TimeoutException) { - return; + _logger.LogWarning("Timeout, models were NOT generated."); } - - // cannot proceed unless we are MainDom - if (_mainDom.IsMainDom) + catch (Exception e) { - try - { - _logger.LogDebug("Generate models..."); - _logger.LogInformation("Generate models now."); - _modelGenerator.GenerateModels(); - _mbErrors.Clear(); - _logger.LogInformation("Generated."); - } - catch (TimeoutException) - { - _logger.LogWarning("Timeout, models were NOT generated."); - } - catch (Exception e) - { - _mbErrors.Report("Failed to build Live models.", e); - _logger.LogError("Failed to generate models.", e); - } - } - else - { - // this will only occur if this appdomain was MainDom and it has - // been released while trying to regenerate models. - _logger.LogWarning("Cannot generate models while app is shutting down"); + _mbErrors.Report("Failed to build Live models.", e); + _logger.LogError("Failed to generate models.", e); } } - - public void Handle(UmbracoRequestEndNotification notification) + else { - if (IsEnabled && _mainDom.IsMainDom) - { - GenerateModelsIfRequested(); - } + // this will only occur if this appdomain was MainDom and it has + // been released while trying to regenerate models. + _logger.LogWarning("Cannot generate models while app is shutting down"); } - - public void Handle(ContentTypeCacheRefresherNotification notification) => RequestModelsGeneration(); - - public void Handle(DataTypeCacheRefresherNotification notification) => RequestModelsGeneration(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs index 4bfd6ff348..2f9d4ff4cc 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/Builder.cs @@ -1,219 +1,239 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +// NOTE +// The idea was to have different types of builder, because I wanted to experiment with +// building code with CodeDom. Turns out more complicated than I thought and maybe not +// worth it at the moment, to we're using TextBuilder and its Generate method is specific. +// +// Keeping the code as-is for the time being... + +/// +/// Provides a base class for all builders. +/// +public abstract class Builder { - // NOTE - // The idea was to have different types of builder, because I wanted to experiment with - // building code with CodeDom. Turns out more complicated than I thought and maybe not - // worth it at the moment, to we're using TextBuilder and its Generate method is specific. - // - // Keeping the code as-is for the time being... + /// + /// Initializes a new instance of the class with a list of models to generate, + /// the result of code parsing, and a models namespace. + /// + /// The list of models to generate. + /// Configuration for modelsbuilder settings + protected Builder(ModelsBuilderSettings config, IList typeModels) + { + TypeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); + + Config = config ?? throw new ArgumentNullException(nameof(config)); + + // can be null or empty, we'll manage + ModelsNamespace = Config.ModelsNamespace; + + // but we want it to prepare + Prepare(); + } + + // for unit tests only +#pragma warning disable CS8618 + protected Builder() +#pragma warning restore CS8618 + { + } /// - /// Provides a base class for all builders. + /// Gets or sets a value indicating the namespace to use for the models. /// - public abstract class Builder + /// May be overriden by code attributes. + public string ModelsNamespace { get; set; } + + protected Dictionary ModelsMap { get; } = new(); + + // the list of assemblies that will be 'using' by default + protected IList TypesUsing { get; } = new List { - protected Dictionary ModelsMap { get; } = new Dictionary(); + "System", + "System.Linq.Expressions", + "Umbraco.Cms.Core.Models.PublishedContent", + "Umbraco.Cms.Core.PublishedCache", + "Umbraco.Cms.Infrastructure.ModelsBuilder", + "Umbraco.Cms.Core", + "Umbraco.Extensions", + }; - // the list of assemblies that will be 'using' by default - protected IList TypesUsing { get; } = new List + /// + /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. + /// + public IList Using => TypesUsing; + + /// + /// Gets the list of all models. + /// + /// Includes those that are ignored. + public IList TypeModels { get; } + + public string? ModelsNamespaceForTests { get; set; } + + protected ModelsBuilderSettings Config { get; } + + /// + /// Gets the list of models to generate. + /// + /// The models to generate + public IEnumerable GetModelsToGenerate() => TypeModels; + + public string GetModelsNamespace() + { + if (ModelsNamespaceForTests != null) { - "System", - "System.Linq.Expressions", - "Umbraco.Cms.Core.Models.PublishedContent", - "Umbraco.Cms.Core.PublishedCache", - "Umbraco.Cms.Infrastructure.ModelsBuilder", - "Umbraco.Cms.Core", - "Umbraco.Extensions" - }; - - /// - /// Gets or sets a value indicating the namespace to use for the models. - /// - /// May be overriden by code attributes. - public string ModelsNamespace { get; set; } - - /// - /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. - /// - public IList Using => TypesUsing; - - /// - /// Gets the list of models to generate. - /// - /// The models to generate - public IEnumerable GetModelsToGenerate() => TypeModels; - - /// - /// Gets the list of all models. - /// - /// Includes those that are ignored. - public IList TypeModels { get; } - - /// - /// Initializes a new instance of the class with a list of models to generate, - /// the result of code parsing, and a models namespace. - /// - /// The list of models to generate. - /// The models namespace. - protected Builder(ModelsBuilderSettings config, IList typeModels) - { - TypeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); - - Config = config ?? throw new ArgumentNullException(nameof(config)); - - // can be null or empty, we'll manage - ModelsNamespace = Config.ModelsNamespace; - - // but we want it to prepare - Prepare(); + return ModelsNamespaceForTests; } - // for unit tests only -#pragma warning disable CS8618 - protected Builder() -#pragma warning restore CS8618 - { } - - protected ModelsBuilderSettings Config { get; } - - /// - /// Prepares generation by processing the result of code parsing. - /// - private void Prepare() + // if builder was initialized with a namespace, use that one + if (!string.IsNullOrWhiteSpace(ModelsNamespace)) { - TypeModel.MapModelTypes(TypeModels, ModelsNamespace); + return ModelsNamespace; + } - var isInMemoryMode = Config.ModelsMode == ModelsMode.InMemoryAuto; + // use configured else fallback to default + return string.IsNullOrWhiteSpace(Config.ModelsNamespace) + ? Constants.ModelsBuilder.DefaultModelsNamespace + : Config.ModelsNamespace; + } - // for the first two of these two tests, - // always throw, even in InMemory mode: cannot happen unless ppl start fidling with attributes to rename - // things, and then they should pay attention to the generation error log - there's no magic here - // for the last one, don't throw in InMemory mode, see comment + // looking for a simple symbol eg 'Umbraco' or 'String' + // expecting to match eg 'Umbraco' or 'System.String' + // returns true if either + // - more than 1 symbol is found (explicitely ambiguous) + // - 1 symbol is found BUT not matching (implicitely ambiguous) + protected bool IsAmbiguousSymbol(string symbol, string match) => - // ensure we have no duplicates type names - foreach (var xx in TypeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" - + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting types."); + // cannot figure out is a symbol is ambiguous without Roslyn + // so... let's say everything is ambiguous - code won't be + // pretty but it'll work + // Essentially this means that a `global::` syntax will be output for the generated models + true; - // ensure we have no duplicates property names - foreach (var typeModel in TypeModels) - foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" - + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting properties."); + /// + /// Prepares generation by processing the result of code parsing. + /// + private void Prepare() + { + TypeModel.MapModelTypes(TypeModels, ModelsNamespace); - // ensure content & property type don't have identical name (csharp hates it) - foreach (var typeModel in TypeModels) + var isInMemoryMode = Config.ModelsMode == ModelsMode.InMemoryAuto; + + // for the first two of these two tests, + // always throw, even in InMemory mode: cannot happen unless ppl start fidling with attributes to rename + // things, and then they should pay attention to the generation error log - there's no magic here + // for the last one, don't throw in InMemory mode, see comment + + // ensure we have no duplicates type names + foreach (IGrouping xx in TypeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + { + throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" + + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting types."); + } + + // ensure we have no duplicates property names + foreach (TypeModel typeModel in TypeModels) + { + foreach (IGrouping xx in typeModel.Properties.GroupBy(x => x.ClrName) + .Where(x => x.Count() > 1)) { - foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) + throw new InvalidOperationException( + $"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" + + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting properties."); + } + } + + // ensure content & property type don't have identical name (csharp hates it) + foreach (TypeModel typeModel in TypeModels) + { + foreach (PropertyModel xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) + { + if (!isInMemoryMode) { - if (!isInMemoryMode) - throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." - + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." + throw new InvalidOperationException( + $"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." + + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." + + " Consider using an attribute to assign a different name to the property."); + } + + // in InMemory mode we generate commented out properties with an error message, + // instead of throwing, because then it kills the sites and ppl don't understand why + xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" + + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." + " Consider using an attribute to assign a different name to the property."); - // in InMemory mode we generate commented out properties with an error message, - // instead of throwing, because then it kills the sites and ppl don't understand why - xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" - + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." - + " Consider using an attribute to assign a different name to the property."); - - // will not be implemented on interface nor class - // note: we will still create the static getter, and implement the property on other classes... - } + // will not be implemented on interface nor class + // note: we will still create the static getter, and implement the property on other classes... } + } - // ensure we have no collision between base types - // NO: we may want to define a base class in a partial, on a model that has a parent - // we are NOT checking that the defined base type does maintain the inheritance chain - //foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) - // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", - // xx.Alias)); + // ensure we have no collision between base types + // NO: we may want to define a base class in a partial, on a model that has a parent + // we are NOT checking that the defined base type does maintain the inheritance chain + // foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) + // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", + // xx.Alias)); - // discover interfaces that need to be declared / implemented - foreach (var typeModel in TypeModels) + // discover interfaces that need to be declared / implemented + foreach (TypeModel typeModel in TypeModels) + { + // collect all the (non-removed) types implemented at parent level + // ie the parent content types and the mixins content types, recursively + var parentImplems = new List(); + if (typeModel.BaseType != null) { - // collect all the (non-removed) types implemented at parent level - // ie the parent content types and the mixins content types, recursively - var parentImplems = new List(); - if (typeModel.BaseType != null) - TypeModel.CollectImplems(parentImplems, typeModel.BaseType); - - // interfaces we must declare we implement (initially empty) - // ie this type's mixins, except those that have been removed, - // and except those that are already declared at the parent level - // in other words, DeclaringInterfaces is "local mixins" - var declaring = typeModel.MixinTypes - .Except(parentImplems); - typeModel.DeclaringInterfaces.AddRange(declaring); - - // interfaces we must actually implement (initially empty) - // if we declare we implement a mixin interface, we must actually implement - // its properties, all recursively (ie if the mixin interface implements...) - // so, starting with local mixins, we collect all the (non-removed) types above them - var mixinImplems = new List(); - foreach (var i in typeModel.DeclaringInterfaces) - TypeModel.CollectImplems(mixinImplems, i); - // and then we remove from that list anything that is already declared at the parent level - typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); + TypeModel.CollectImplems(parentImplems, typeModel.BaseType); } - // ensure elements don't inherit from non-elements - foreach (var typeModel in TypeModels.Where(x => x.IsElement)) + // interfaces we must declare we implement (initially empty) + // ie this type's mixins, except those that have been removed, + // and except those that are already declared at the parent level + // in other words, DeclaringInterfaces is "local mixins" + IEnumerable declaring = typeModel.MixinTypes + .Except(parentImplems); + typeModel.DeclaringInterfaces.AddRange(declaring); + + // interfaces we must actually implement (initially empty) + // if we declare we implement a mixin interface, we must actually implement + // its properties, all recursively (ie if the mixin interface implements...) + // so, starting with local mixins, we collect all the (non-removed) types above them + var mixinImplems = new List(); + foreach (TypeModel i in typeModel.DeclaringInterfaces) { - if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) - throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); - - var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); - if (errs.Count > 0) - throw new InvalidOperationException($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); + TypeModel.CollectImplems(mixinImplems, i); } + + // and then we remove from that list anything that is already declared at the parent level + typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); } - // looking for a simple symbol eg 'Umbraco' or 'String' - // expecting to match eg 'Umbraco' or 'System.String' - // returns true if either - // - more than 1 symbol is found (explicitely ambiguous) - // - 1 symbol is found BUT not matching (implicitely ambiguous) - protected bool IsAmbiguousSymbol(string symbol, string match) + // ensure elements don't inherit from non-elements + foreach (TypeModel typeModel in TypeModels.Where(x => x.IsElement)) { - // cannot figure out is a symbol is ambiguous without Roslyn - // so... let's say everything is ambiguous - code won't be - // pretty but it'll work + if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) + { + throw new InvalidOperationException( + $"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); + } - // Essentially this means that a `global::` syntax will be output for the generated models - return true; - } - - public string? ModelsNamespaceForTests { get; set; } - - public string GetModelsNamespace() - { - if (ModelsNamespaceForTests != null) - return ModelsNamespaceForTests; - - // if builder was initialized with a namespace, use that one - if (!string.IsNullOrWhiteSpace(ModelsNamespace)) - return ModelsNamespace; - - // use configured else fallback to default - return string.IsNullOrWhiteSpace(Config.ModelsNamespace) - ? Constants.ModelsBuilder.DefaultModelsNamespace - : Config.ModelsNamespace; - } - - protected string GetModelsBaseClassName(TypeModel type) - { - // default - return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; + var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); + if (errs.Count > 0) + { + throw new InvalidOperationException( + $"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); + } } } + + protected string GetModelsBaseClassName(TypeModel type) => + + // default + type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs index 930bc163f0..0b2997e994 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/ModelsGenerator.cs @@ -1,65 +1,64 @@ -using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building -{ - public class ModelsGenerator - { - private readonly UmbracoServices _umbracoService; - private ModelsBuilderSettings _config; - private readonly OutOfDateModelsStatus _outOfDateModels; - private readonly IHostingEnvironment _hostingEnvironment; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; - public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) +public class ModelsGenerator +{ + private readonly IHostingEnvironment _hostingEnvironment; + private readonly OutOfDateModelsStatus _outOfDateModels; + private readonly UmbracoServices _umbracoService; + private ModelsBuilderSettings _config; + + public ModelsGenerator(UmbracoServices umbracoService, IOptionsMonitor config, + OutOfDateModelsStatus outOfDateModels, IHostingEnvironment hostingEnvironment) + { + _umbracoService = umbracoService; + _config = config.CurrentValue; + _outOfDateModels = outOfDateModels; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } + + public void GenerateModels() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) { - _umbracoService = umbracoService; - _config = config.CurrentValue; - _outOfDateModels = outOfDateModels; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); + Directory.CreateDirectory(modelsDirectory); } - public void GenerateModels() + foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - Directory.CreateDirectory(modelsDirectory); - } + File.Delete(file); + } - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - { - File.Delete(file); - } + IList typeModels = _umbracoService.GetAllTypes(); - System.Collections.Generic.IList typeModels = _umbracoService.GetAllTypes(); + var builder = new TextBuilder(_config, typeModels); - var builder = new TextBuilder(_config, typeModels); + foreach (TypeModel typeModel in builder.GetModelsToGenerate()) + { + var sb = new StringBuilder(); + builder.Generate(sb, typeModel); + var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); + File.WriteAllText(filename, sb.ToString()); + } - foreach (TypeModel typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); - File.WriteAllText(filename, sb.ToString()); - } - - // the idea was to calculate the current hash and to add it as an extra file to the compilation, - // in order to be able to detect whether a DLL is consistent with an environment - however the - // environment *might not* contain the local partial files, and thus it could be impossible to - // calculate the hash. So... maybe that's not a good idea after all? - /* - var currentHash = HashHelper.Hash(ourFiles, typeModels); - ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; + // the idea was to calculate the current hash and to add it as an extra file to the compilation, + // in order to be able to detect whether a DLL is consistent with an environment - however the + // environment *might not* contain the local partial files, and thus it could be impossible to + // calculate the hash. So... maybe that's not a good idea after all? + /* + var currentHash = HashHelper.Hash(ourFiles, typeModels); + ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; [assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] "; - */ + */ - _outOfDateModels.Clear(); - } + _outOfDateModels.Clear(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs index 6738308735..2475aebc3f 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/PropertyModel.cs @@ -1,62 +1,67 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Represents a model property. +/// +public class PropertyModel { /// - /// Represents a model property. + /// Gets the alias of the property. /// - public class PropertyModel + public string Alias = string.Empty; + + /// + /// Gets the clr name of the property. + /// + /// This is just the local name eg "Price". + public string ClrName = string.Empty; + + /// + /// Gets the CLR type name of the property values. + /// + public string ClrTypeName = string.Empty; + + /// + /// Gets the description of the property. + /// + public string? Description; + + /// + /// Gets the generation errors for the property. + /// + /// + /// This should be null, unless something prevents the property from being + /// generated, and then the value should explain what. This can be used to generate + /// commented out code eg in mode. + /// + public List? Errors; + + /// + /// Gets the Model Clr type of the property values. + /// + /// + /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter + /// if any, else object. May include some ModelType that will need to be mapped. + /// + public Type ModelClrType = null!; + + /// + /// Gets the name of the property. + /// + public string Name = string.Empty; + + /// + /// Adds an error. + /// + public void AddError(string error) { - /// - /// Gets the alias of the property. - /// - public string Alias = string.Empty; - - /// - /// Gets the name of the property. - /// - public string Name = string.Empty; - - /// - /// Gets the description of the property. - /// - public string? Description; - - /// - /// Gets the clr name of the property. - /// - /// This is just the local name eg "Price". - public string ClrName = string.Empty; - - /// - /// Gets the Model Clr type of the property values. - /// - /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter - /// if any, else object. May include some ModelType that will need to be mapped. - public Type ModelClrType = null!; - - /// - /// Gets the CLR type name of the property values. - /// - public string ClrTypeName = string.Empty; - - /// - /// Gets the generation errors for the property. - /// - /// This should be null, unless something prevents the property from being - /// generated, and then the value should explain what. This can be used to generate - /// commented out code eg in mode. - public List? Errors; - - /// - /// Adds an error. - /// - public void AddError(string error) + if (Errors == null) { - if (Errors == null) Errors = new List(); - Errors.Add(error); + Errors = new List(); } + + Errors.Add(error); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs index 8bb65eb543..0fa866ec23 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextBuilder.cs @@ -1,570 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Implements a builder that works by writing text. +/// +public class TextBuilder : Builder { - /// - /// Implements a builder that works by writing text. - /// - public class TextBuilder : Builder - { - /// - /// Initializes a new instance of the class with a list of models to generate - /// and the result of code parsing. - /// - /// The list of models to generate. - public TextBuilder(ModelsBuilderSettings config, IList typeModels) - : base(config, typeModels) - { } - - // internal for unit tests only - public TextBuilder() - { } - - /// - /// Outputs a generated model to a string builder. - /// - /// The string builder. - /// The model to generate. - public void Generate(StringBuilder sb, TypeModel typeModel) - { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); - - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); - - WriteContentType(sb, typeModel); - - sb.Append("}\n"); - } - - /// - /// Outputs generated models to a string builder. - /// - /// The string builder. - /// The models to generate. - public void Generate(StringBuilder sb, IEnumerable typeModels) - { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); - - // assembly attributes marker - sb.Append("\n//ASSATTR\n"); - - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); - - foreach (var typeModel in typeModels) - { - WriteContentType(sb, typeModel); - sb.Append("\n"); - } - - sb.Append("}\n"); - } - - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) - { - TextHeaderWriter.WriteHeader(sb); - } - - // writes an attribute that identifies code generated by a tool - // (helps reduce warnings, tools such as FxCop use it) - // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 - // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute - // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ - // - // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." - // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. - // - private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) - { - sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); - } - - // writes an attribute that specifies that an output may be null. - // (useful for consuming projects with nullable reference types enabled) - private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) - { - sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, isReturn ? "return: " : ""); - } - - private void WriteContentType(StringBuilder sb, TypeModel type) - { - string sep; - - if (type.IsMixin) - { - // write the interface declaration - sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); - if (!string.IsNullOrWhiteSpace(type.Name)) - sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); - var implements = type.BaseType == null - ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) - : type.BaseType.ClrName; - if (implements != null) - sb.AppendFormat(" : I{0}", implements); - - // write the mixins - sep = implements == null ? ":" : ","; - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } - - sb.Append("\n\t{\n"); - - // write the properties - only the local (non-ignored) ones, we're an interface - var more = false; - foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) - { - if (more) sb.Append("\n"); - more = true; - WriteInterfaceProperty(sb, prop); - } - - sb.Append("\t}\n\n"); - } - - // write the class declaration - if (!string.IsNullOrWhiteSpace(type.Name)) - sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - // cannot do it now. see note in ImplementContentTypeAttribute - //if (!type.HasImplement) - // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\tpublic partial class {0}", type.ClrName); - var inherits = type.HasBase - ? null // has its own base already - : (type.BaseType == null - ? GetModelsBaseClassName(type) - : type.BaseType.ClrName); - if (inherits != null) - sb.AppendFormat(" : {0}", inherits); - - sep = inherits == null ? ":" : ","; - if (type.IsMixin) - { - // if it's a mixin it implements its own interface - sb.AppendFormat("{0} I{1}", sep, type.ClrName); - } - else - { - // write the mixins, if any, as interfaces - // only if not a mixin because otherwise the interface already has them already - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } - } - - // begin class body - sb.Append("\n\t{\n"); - - // write the constants & static methods - // as 'new' since parent has its own - or maybe not - disable warning - sb.Append("\t\t// helpers\n"); - sb.Append("#pragma warning disable 0109 // new is redundant\n"); - WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", - type.Alias); - var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme - WriteGeneratedCodeAttribute(sb, "\t\t"); - sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", - itemType); - WriteGeneratedCodeAttribute(sb, "\t\t"); - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.Append("\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); - WriteGeneratedCodeAttribute(sb, "\t\t"); - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.AppendFormat("\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", - type.ClrName); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); - sb.Append("#pragma warning restore 0109\n\n"); - sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); - - // write the ctor - sb.AppendFormat("\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\n\t\t}}\n\n", - type.ClrName, type.IsElement ? "Element" : "Content"); - - // write the properties - sb.Append("\t\t// properties\n"); - WriteContentTypeProperties(sb, type); - - // close the class declaration - sb.Append("\t}\n"); - } - - private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) - { - var staticMixinGetters = true; - - // write the properties - foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) - WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); - - // no need to write the parent properties since we inherit from the parent - // and the parent defines its own properties. need to write the mixins properties - // since the mixins are only interfaces and we have to provide an implementation. - - // write the mixins properties - foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) - foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName)) - if (staticMixinGetters) - WriteMixinProperty(sb, prop, mixinType.ClrName); - else - WriteProperty(sb, mixinType, prop); - } - - private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) - { - sb.Append("\n"); - - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) - { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); - else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\t///\n"); - } - - WriteGeneratedCodeAttribute(sb, "\t\t"); - - if (!property.ModelClrType.IsValueType) - { - WriteMaybeNullAttribute(sb, "\t\t", false); - } - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - - sb.AppendFormat(" {0} => ", - property.ClrName); - WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); - sb.AppendFormat(".{0}(this, _publishedValueFallback);\n", - MixinStaticGetterName(property.ClrName)); - } - - private static string MixinStaticGetterName(string clrName) - { - return string.Format("Get{0}", clrName); - } - - private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string? mixinClrName = null) - { - var mixinStatic = mixinClrName != null; - - sb.Append("\n"); - - if (property.Errors != null) - { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) - { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } - } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } - - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) - { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); - else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\t///\n"); - } - - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t"); - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - - if (mixinStatic) - { - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => {1}(this, _publishedValueFallback);\n", - property.ClrName, MixinStaticGetterName(property.ClrName)); - } - else - { - sb.Append("\t\tpublic virtual "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => this.Value", - property.ClrName); - if (property.ModelClrType != typeof(object)) - { - sb.Append("<"); - WriteClrType(sb, property.ClrTypeName); - sb.Append(">"); - } - sb.AppendFormat("(_publishedValueFallback, \"{0}\");\n", - property.Alias); - } - - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); - } - - if (!mixinStatic) return; - - var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); - - //if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; - - sb.Append("\n"); - - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); - - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t", true); - sb.Append("\t\tpublic static "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value", - mixinStaticGetterName, mixinClrName); - if (property.ModelClrType != typeof(object)) - { - sb.Append("<"); - WriteClrType(sb, property.ClrTypeName); - sb.Append(">"); - } - sb.AppendFormat("(publishedValueFallback, \"{0}\");\n", - property.Alias); - } - - private static IEnumerable SplitError(string error) - { - var p = 0; - while (p < error.Length) - { - var n = p + 50; - while (n < error.Length && error[n] != ' ') n++; - if (n >= error.Length) break; - yield return error.Substring(p, n - p); - p = n + 1; - } - if (p < error.Length) - yield return error.Substring(p); - } - - private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) - { - if (property.Errors != null) - { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) - { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } - } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } - - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - WriteGeneratedCodeAttribute(sb, "\t\t"); - if (!property.ModelClrType.IsValueType) - WriteMaybeNullAttribute(sb, "\t\t"); - - sb.Append("\t\t"); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} {{ get; }}\n", - property.ClrName); - - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); - } - } - - // internal for unit tests - public void WriteClrType(StringBuilder sb, Type type) - { - var s = type.ToString(); - - if (type.IsGenericType) - { - var p = s.IndexOf('`'); - WriteNonGenericClrType(sb, s.Substring(0, p)); - sb.Append("<"); - var args = type.GetGenericArguments(); - for (var i = 0; i < args.Length; i++) - { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); - } - sb.Append(">"); - } - else - { - WriteNonGenericClrType(sb, s); - } - } - - internal void WriteClrType(StringBuilder sb, string type) - { - var p = type.IndexOf('<'); - if (type.Contains('<')) - { - WriteNonGenericClrType(sb, type.Substring(0, p)); - sb.Append("<"); - var args = type.Substring(p + 1).TrimEnd(Constants.CharArrays.GreaterThan).Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types - for (var i = 0; i < args.Length; i++) - { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); - } - sb.Append(">"); - } - else - { - WriteNonGenericClrType(sb, type); - } - } - - private void WriteNonGenericClrType(StringBuilder sb, string s) - { - // map model types - s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); - - // takes care eg of "System.Int32" vs. "int" - if (TypesMap.TryGetValue(s, out string? typeName)) - { - sb.Append(typeName); - return; - } - - // if full type name matches a using clause, strip - // so if we want Umbraco.Core.Models.IPublishedContent - // and using Umbraco.Core.Models, then we just need IPublishedContent - typeName = s; - string? typeUsing = null; - var p = typeName.LastIndexOf('.'); - if (p > 0) - { - var x = typeName.Substring(0, p); - if (Using.Contains(x)) - { - typeName = typeName.Substring(p + 1); - typeUsing = x; - } - else if (x == ModelsNamespace) // that one is used by default - { - typeName = typeName.Substring(p + 1); - typeUsing = ModelsNamespace; - } - } - - // nested types *after* using - typeName = typeName.Replace("+", "."); - - // symbol to test is the first part of the name - // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous - p = typeName.IndexOf('.'); - var symbol = p > 0 ? typeName.Substring(0, p) : typeName; - - // what we should find - WITHOUT any generic thing - just the type - // no 'using' = the exact symbol - // a 'using' = using.symbol - var match = typeUsing == null ? symbol : (typeUsing + "." + symbol); - - // if not ambiguous, be happy - if (!IsAmbiguousSymbol(symbol, match)) - { - sb.Append(typeName); - return; - } - - // symbol is ambiguous - // if no 'using', must prepend global:: - if (typeUsing == null) - { - sb.Append("global::"); - sb.Append(s.Replace("+", ".")); - return; - } - - // could fullname be non-ambiguous? - // note: all-or-nothing, not trying to segment the using clause - typeName = s.Replace("+", "."); - p = typeName.IndexOf('.'); - symbol = typeName.Substring(0, p); - match = symbol; - - // still ambiguous, must prepend global:: - if (IsAmbiguousSymbol(symbol, match)) - sb.Append("global::"); - - sb.Append(typeName); - } - - private static string XmlCommentString(string s) - { - return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); - } - - private static readonly IDictionary TypesMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + private static readonly IDictionary _typesMap = + new Dictionary(StringComparer.OrdinalIgnoreCase) { { "System.Int16", "short" }, { "System.Int32", "int" }, @@ -581,7 +28,659 @@ namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building { "System.SByte", "sbyte" }, { "System.Single", "float" }, { "System.Double", "double" }, - { "System.Decimal", "decimal" } + { "System.Decimal", "decimal" }, }; + + /// + /// Initializes a new instance of the class with a list of models to generate + /// and the result of code parsing. + /// + /// The list of models to generate. + public TextBuilder(ModelsBuilderSettings config, IList typeModels) + : base(config, typeModels) + { + } + + // internal for unit tests only + public TextBuilder() + { + } + + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) => TextHeaderWriter.WriteHeader(sb); + + /// + /// Outputs a generated model to a string builder. + /// + /// The string builder. + /// The model to generate. + public void Generate(StringBuilder sb, TypeModel typeModel) + { + WriteHeader(sb); + + foreach (var t in TypesUsing) + { + sb.AppendFormat("using {0};\n", t); + } + + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); + + WriteContentType(sb, typeModel); + + sb.Append("}\n"); + } + + /// + /// Outputs generated models to a string builder. + /// + /// The string builder. + /// The models to generate. + public void Generate(StringBuilder sb, IEnumerable typeModels) + { + WriteHeader(sb); + + foreach (var t in TypesUsing) + { + sb.AppendFormat("using {0};\n", t); + } + + // assembly attributes marker + sb.Append("\n//ASSATTR\n"); + + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); + + foreach (TypeModel typeModel in typeModels) + { + WriteContentType(sb, typeModel); + sb.Append("\n"); + } + + sb.Append("}\n"); + } + + // internal for unit tests + public void WriteClrType(StringBuilder sb, Type type) + { + var s = type.ToString(); + + if (type.IsGenericType) + { + var p = s.IndexOf('`'); + WriteNonGenericClrType(sb, s.Substring(0, p)); + sb.Append("<"); + Type[] args = type.GetGenericArguments(); + for (var i = 0; i < args.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + WriteClrType(sb, args[i]); + } + + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, s); + } + } + + // writes an attribute that identifies code generated by a tool + // (helps reduce warnings, tools such as FxCop use it) + // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 + // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute + // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ + // + // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." + // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. + private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat( + "{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n", + tabs, ApiVersion.Current.Version); + + // writes an attribute that specifies that an output may be null. + // (useful for consuming projects with nullable reference types enabled) + private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) => + sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs, + isReturn ? "return: " : string.Empty); + + private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName); + + private static IEnumerable SplitError(string error) + { + var p = 0; + while (p < error.Length) + { + var n = p + 50; + while (n < error.Length && error[n] != ' ') + { + n++; + } + + if (n >= error.Length) + { + break; + } + + yield return error.Substring(p, n - p); + p = n + 1; + } + + if (p < error.Length) + { + yield return error[p..]; + } + } + + private void WriteContentType(StringBuilder sb, TypeModel type) + { + string sep; + + if (type.IsMixin) + { + // write the interface declaration + sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); + if (!string.IsNullOrWhiteSpace(type.Name)) + { + sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); + } + + sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); + var implements = type.BaseType == null + ? type.HasBase ? null : type.IsElement ? "PublishedElement" : "PublishedContent" + : type.BaseType.ClrName; + if (implements != null) + { + sb.AppendFormat(" : I{0}", implements); + } + + // write the mixins + sep = implements == null ? ":" : ","; + foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) + { + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; + } + + sb.Append("\n\t{\n"); + + // write the properties - only the local (non-ignored) ones, we're an interface + var more = false; + foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName)) + { + if (more) + { + sb.Append("\n"); + } + + more = true; + WriteInterfaceProperty(sb, prop); + } + + sb.Append("\t}\n\n"); + } + + // write the class declaration + if (!string.IsNullOrWhiteSpace(type.Name)) + { + sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); + } + + // cannot do it now. see note in ImplementContentTypeAttribute + // if (!type.HasImplement) + // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\tpublic partial class {0}", type.ClrName); + var inherits = type.HasBase + ? null // has its own base already + : type.BaseType == null + ? GetModelsBaseClassName(type) + : type.BaseType.ClrName; + if (inherits != null) + { + sb.AppendFormat(" : {0}", inherits); + } + + sep = inherits == null ? ":" : ","; + if (type.IsMixin) + { + // if it's a mixin it implements its own interface + sb.AppendFormat("{0} I{1}", sep, type.ClrName); + } + else + { + // write the mixins, if any, as interfaces + // only if not a mixin because otherwise the interface already has them already + foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) + { + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; + } + } + + // begin class body + sb.Append("\n\t{\n"); + + // write the constants & static methods + // as 'new' since parent has its own - or maybe not - disable warning + sb.Append("\t\t// helpers\n"); + sb.Append("#pragma warning disable 0109 // new is redundant\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat( + "\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", + type.Alias); + TypeModel.ItemTypes itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat( + "\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", + itemType); + WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); + sb.Append( + "\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n"); + sb.Append( + "\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + WriteMaybeNullAttribute(sb, "\t\t", true); + sb.AppendFormat( + "\t\tpublic static IPublishedPropertyType GetModelPropertyType(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression> selector)\n", + type.ClrName); + sb.Append( + "\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n"); + sb.Append("#pragma warning restore 0109\n\n"); + sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;"); + + // write the ctor + sb.AppendFormat( + "\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\n\t\t}}\n\n", + type.ClrName, type.IsElement ? "Element" : "Content"); + + // write the properties + sb.Append("\t\t// properties\n"); + WriteContentTypeProperties(sb, type); + + // close the class declaration + sb.Append("\t}\n"); + } + + private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) + { + var staticMixinGetters = true; + + // write the properties + foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName)) + { + WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); + } + + // no need to write the parent properties since we inherit from the parent + // and the parent defines its own properties. need to write the mixins properties + // since the mixins are only interfaces and we have to provide an implementation. + + // write the mixins properties + foreach (TypeModel mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) + { + foreach (PropertyModel prop in mixinType.Properties.OrderBy(x => x.ClrName)) + { + if (staticMixinGetters) + { + WriteMixinProperty(sb, prop, mixinType.ClrName); + } + else + { + WriteProperty(sb, mixinType, prop); + } + } + } + } + + private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) + { + sb.Append("\n"); + + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) + { + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), + XmlCommentString(property.Description)); + } + else + { + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + } + + sb.Append("\t\t///\n"); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } + + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); + + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + + sb.AppendFormat( + " {0} => ", + property.ClrName); + WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); + sb.AppendFormat( + ".{0}(this, _publishedValueFallback);\n", + MixinStaticGetterName(property.ClrName)); + } + + private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string? mixinClrName = null) + { + var mixinStatic = mixinClrName != null; + + sb.Append("\n"); + + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) + { + if (first) + { + first = false; + } + else + { + sb.Append("\t\t *\n"); + } + + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); + } + } + + sb.Append("\t\t *\n"); + sb.Append("\n"); + } + + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) + { + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), + XmlCommentString(property.Description)); + } + else + { + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + } + + sb.Append("\t\t///\n"); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } + + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); + + if (mixinStatic) + { + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0} => {1}(this, _publishedValueFallback);\n", + property.ClrName, MixinStaticGetterName(property.ClrName)); + } + else + { + sb.Append("\t\tpublic virtual "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0} => this.Value", + property.ClrName); + if (property.ModelClrType != typeof(object)) + { + sb.Append("<"); + WriteClrType(sb, property.ClrTypeName); + sb.Append(">"); + } + + sb.AppendFormat( + "(_publishedValueFallback, \"{0}\");\n", + property.Alias); + } + + if (property.Errors != null) + { + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); + } + + if (!mixinStatic) + { + return; + } + + var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); + + // if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; + sb.Append("\n"); + + if (!string.IsNullOrWhiteSpace(property.Name)) + { + sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t", true); + } + + sb.Append("\t\tpublic static "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value", + mixinStaticGetterName, mixinClrName); + if (property.ModelClrType != typeof(object)) + { + sb.Append("<"); + WriteClrType(sb, property.ClrTypeName); + sb.Append(">"); + } + + sb.AppendFormat( + "(publishedValueFallback, \"{0}\");\n", + property.Alias); + } + + private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) + { + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) + { + if (first) + { + first = false; + } + else + { + sb.Append("\t\t *\n"); + } + + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); + } + } + + sb.Append("\t\t *\n"); + sb.Append("\n"); + } + + if (!string.IsNullOrWhiteSpace(property.Name)) + { + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + if (!property.ModelClrType.IsValueType) + { + WriteMaybeNullAttribute(sb, "\t\t"); + } + + sb.Append("\t\t"); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat( + " {0} {{ get; }}\n", + property.ClrName); + + if (property.Errors != null) + { + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); + } + } + + internal void WriteClrType(StringBuilder sb, string type) + { + var p = type.IndexOf('<'); + if (type.Contains('<')) + { + WriteNonGenericClrType(sb, type[..p]); + sb.Append("<"); + var args = type[(p + 1)..].TrimEnd(Constants.CharArrays.GreaterThan) + .Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types + for (var i = 0; i < args.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + WriteClrType(sb, args[i]); + } + + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, type); + } + } + + private static string XmlCommentString(string s) => + s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); + + private void WriteNonGenericClrType(StringBuilder sb, string s) + { + // map model types + s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); + + // takes care eg of "System.Int32" vs. "int" + if (_typesMap.TryGetValue(s, out var typeName)) + { + sb.Append(typeName); + return; + } + + // if full type name matches a using clause, strip + // so if we want Umbraco.Core.Models.IPublishedContent + // and using Umbraco.Core.Models, then we just need IPublishedContent + typeName = s; + string? typeUsing = null; + var p = typeName.LastIndexOf('.'); + if (p > 0) + { + var x = typeName.Substring(0, p); + if (Using.Contains(x)) + { + typeName = typeName.Substring(p + 1); + typeUsing = x; + } + else if (x == ModelsNamespace) // that one is used by default + { + typeName = typeName.Substring(p + 1); + typeUsing = ModelsNamespace; + } + } + + // nested types *after* using + typeName = typeName.Replace("+", "."); + + // symbol to test is the first part of the name + // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous + p = typeName.IndexOf('.'); + var symbol = p > 0 ? typeName.Substring(0, p) : typeName; + + // what we should find - WITHOUT any generic thing - just the type + // no 'using' = the exact symbol + // a 'using' = using.symbol + var match = typeUsing == null ? symbol : typeUsing + "." + symbol; + + // if not ambiguous, be happy + if (!IsAmbiguousSymbol(symbol, match)) + { + sb.Append(typeName); + return; + } + + // symbol is ambiguous + // if no 'using', must prepend global:: + if (typeUsing == null) + { + sb.Append("global::"); + sb.Append(s.Replace("+", ".")); + return; + } + + // could fullname be non-ambiguous? + // note: all-or-nothing, not trying to segment the using clause + typeName = s.Replace("+", "."); + p = typeName.IndexOf('.'); + symbol = typeName.Substring(0, p); + match = symbol; + + // still ambiguous, must prepend global:: + if (IsAmbiguousSymbol(symbol, match)) + { + sb.Append("global::"); + } + + sb.Append(typeName); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs index a192560f1d..5a532cbdba 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TextHeaderWriter.cs @@ -1,25 +1,24 @@ using System.Text; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +internal static class TextHeaderWriter { - internal static class TextHeaderWriter + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) { - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) - { - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("// \n"); - sb.Append("// This code was generated by a tool.\n"); - sb.Append("//\n"); - sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version); - sb.Append("//\n"); - sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); - sb.Append("// \n"); - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("\n"); - } + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("// \n"); + sb.Append("// This code was generated by a tool.\n"); + sb.Append("//\n"); + sb.AppendFormat("// Umbraco.ModelsBuilder.Embedded v{0}\n", ApiVersion.Current.Version); + sb.Append("//\n"); + sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); + sb.Append("// \n"); + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("\n"); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs index 00da2e06fc..cc6d3be7c8 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModel.cs @@ -1,204 +1,218 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models.PublishedContent; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +/// +/// Represents a model. +/// +public class TypeModel { /// - /// Represents a model. + /// Gets the list of interfaces that this model needs to declare it implements. /// - public class TypeModel + /// + /// Some of these interfaces may actually be implemented by a base model + /// that this model inherits from. + /// + public readonly List DeclaringInterfaces = new(); + + /// + /// Represents the different model item types. + /// + public enum ItemTypes { /// - /// Gets the unique identifier of the corresponding content type. + /// Element. /// - public int Id; + Element, /// - /// Gets the alias of the model. + /// Content. /// - public string Alias = string.Empty; + Content, /// - /// Gets the name of the content type. + /// Media. /// - public string? Name; + Media, /// - /// Gets the description of the content type. + /// Member. /// - public string? Description; + Member, + } - /// - /// Gets the clr name of the model. - /// - /// This is the complete name eg "Foo.Bar.MyContent". - public string ClrName = string.Empty; + /// + /// Gets the list of interfaces that this model needs to actually implement. + /// + public readonly List ImplementingInterfaces = new(); - /// - /// Gets the unique identifier of the parent. - /// - /// The parent can either be a base content type, or a content types container. If the content - /// type does not have a base content type, then returns -1. - public int ParentId; + /// + /// Gets the mixin models. + /// + /// The current model implements mixins. + public readonly List MixinTypes = new(); - /// - /// Gets the base model. - /// - /// - /// If the content type does not have a base content type, then returns null. - /// The current model inherits from its base model. - /// - public TypeModel? BaseType; // the parent type in Umbraco (type inherits its properties) + /// + /// Gets the list of properties that are defined by this model. + /// + /// + /// These are only those property that are defined locally by this model, + /// and the list does not contain properties inherited from base models or from mixins. + /// + public readonly List Properties = new(); - /// - /// Gets the list of properties that are defined by this model. - /// - /// These are only those property that are defined locally by this model, - /// and the list does not contain properties inherited from base models or from mixins. - public readonly List Properties = new List(); + /// + /// Gets the alias of the model. + /// + public string Alias = string.Empty; - /// - /// Gets the mixin models. - /// - /// The current model implements mixins. - public readonly List MixinTypes = new List(); + private ItemTypes _itemType; - /// - /// Gets the list of interfaces that this model needs to declare it implements. - /// - /// Some of these interfaces may actually be implemented by a base model - /// that this model inherits from. - public readonly List DeclaringInterfaces = new List(); + /// + /// Gets the base model. + /// + /// + /// If the content type does not have a base content type, then returns null. + /// The current model inherits from its base model. + /// + public TypeModel? BaseType; // the parent type in Umbraco (type inherits its properties) - /// - /// Gets the list of interfaces that this model needs to actually implement. - /// - public readonly List ImplementingInterfaces = new List(); + /// + /// Gets the clr name of the model. + /// + /// This is the complete name eg "Foo.Bar.MyContent". + public string ClrName = string.Empty; - ///// - ///// Gets the list of existing static mixin method candidates. - ///// - //public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used + /// + /// Gets the description of the content type. + /// + public string? Description; - /// - /// Gets a value indicating whether this model has a base class. - /// - /// Can be either because the content type has a base content type declared in Umbraco, - /// or because the existing user's code declares a base class for this model. - public bool HasBase; + ///// + ///// Gets the list of existing static mixin method candidates. + ///// + // public readonly List StaticMixinMethods = new List(); //TODO: Do we need this? it isn't used - /// - /// Gets a value indicating whether this model is used as a mixin by another model. - /// - public bool IsMixin; + /// + /// Gets a value indicating whether this model has a base class. + /// + /// + /// Can be either because the content type has a base content type declared in Umbraco, + /// or because the existing user's code declares a base class for this model. + /// + public bool HasBase; - /// - /// Gets a value indicating whether this model is the base model of another model. - /// - public bool IsParent; + /// + /// Gets the unique identifier of the corresponding content type. + /// + public int Id; - /// - /// Gets a value indicating whether the type is an element. - /// - public bool IsElement => ItemType == ItemTypes.Element; + /// + /// Gets a value indicating whether this model is used as a mixin by another model. + /// + public bool IsMixin; - /// - /// Represents the different model item types. - /// - public enum ItemTypes + /// + /// Gets a value indicating whether this model is the base model of another model. + /// + public bool IsParent; + + /// + /// Gets the name of the content type. + /// + public string? Name; + + /// + /// Gets the unique identifier of the parent. + /// + /// + /// The parent can either be a base content type, or a content types container. If the content + /// type does not have a base content type, then returns -1. + /// + public int ParentId; + + /// + /// Gets a value indicating whether the type is an element. + /// + public bool IsElement => ItemType == ItemTypes.Element; + + /// + /// Gets or sets the model item type. + /// + public ItemTypes ItemType + { + get => _itemType; + set { - /// - /// Element. - /// - Element, - - /// - /// Content. - /// - Content, - - /// - /// Media. - /// - Media, - - /// - /// Member. - /// - Member - } - - private ItemTypes _itemType; - - /// - /// Gets or sets the model item type. - /// - public ItemTypes ItemType - { - get { return _itemType; } - set + switch (value) { - switch (value) - { - case ItemTypes.Element: - case ItemTypes.Content: - case ItemTypes.Media: - case ItemTypes.Member: - _itemType = value; - break; - default: - throw new ArgumentException("value"); - } + case ItemTypes.Element: + case ItemTypes.Content: + case ItemTypes.Media: + case ItemTypes.Member: + _itemType = value; + break; + default: + throw new ArgumentException("value"); } } + } - /// - /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. - /// - /// The collection. - /// The type. - /// Includes the specified type. - internal static void CollectImplems(ICollection types, TypeModel type) + /// + /// Enumerates the base models starting from the current model up. + /// + /// + /// Indicates whether the enumeration should start with the current model + /// or from its base model. + /// + /// The base models. + public IEnumerable EnumerateBaseTypes(bool andSelf = false) + { + TypeModel? typeModel = andSelf ? this : BaseType; + while (typeModel != null) { - if (types.Contains(type) == false) - types.Add(type); - if (type.BaseType != null) - CollectImplems(types, type.BaseType); - foreach (var mixin in type.MixinTypes) - CollectImplems(types, mixin); + yield return typeModel; + typeModel = typeModel.BaseType; + } + } + + /// + /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. + /// + /// The collection. + /// The type. + /// Includes the specified type. + internal static void CollectImplems(ICollection types, TypeModel type) + { + if (types.Contains(type) == false) + { + types.Add(type); } - /// - /// Enumerates the base models starting from the current model up. - /// - /// Indicates whether the enumeration should start with the current model - /// or from its base model. - /// The base models. - public IEnumerable EnumerateBaseTypes(bool andSelf = false) + if (type.BaseType != null) { - var typeModel = andSelf ? this : BaseType; - while (typeModel != null) - { - yield return typeModel; - typeModel = typeModel.BaseType; - } + CollectImplems(types, type.BaseType); } - /// - /// Maps ModelType. - /// - public static void MapModelTypes(IList typeModels, string ns) + foreach (TypeModel mixin in type.MixinTypes) { - var hasNs = !string.IsNullOrWhiteSpace(ns); - var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName); - foreach (var typeModel in typeModels) + CollectImplems(types, mixin); + } + } + + /// + /// Maps ModelType. + /// + public static void MapModelTypes(IList typeModels, string ns) + { + var hasNs = !string.IsNullOrWhiteSpace(ns); + var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? ns + "." + x.ClrName : x.ClrName); + foreach (TypeModel typeModel in typeModels) + { + foreach (PropertyModel propertyModel in typeModel.Properties) { - foreach (var propertyModel in typeModel.Properties) - { - propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); - } + propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs index 46af457299..0e53b9f04b 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/Building/TypeModelHasher.cs @@ -1,46 +1,42 @@ -using System.Collections.Generic; -using System.Linq; using System.Text; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building +namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building; + +public class TypeModelHasher { - public class TypeModelHasher + public static string Hash(IEnumerable typeModels) { - public static string Hash(IEnumerable typeModels) + var builder = new StringBuilder(); + + // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash + // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference + foreach (TypeModel typeModel in typeModels.OrderBy(x => x.Alias)) { - var builder = new StringBuilder(); + builder.AppendLine("--- CONTENT TYPE MODEL ---"); + builder.AppendLine(typeModel.Id.ToString()); + builder.AppendLine(typeModel.Alias); + builder.AppendLine(typeModel.ClrName); + builder.AppendLine(typeModel.ParentId.ToString()); + builder.AppendLine(typeModel.Name); + builder.AppendLine(typeModel.Description); + builder.AppendLine(typeModel.ItemType.ToString()); + builder.AppendLine("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); - // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash - // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference - - foreach (var typeModel in typeModels.OrderBy(x => x.Alias)) + foreach (PropertyModel prop in typeModel.Properties.OrderBy(x => x.Alias)) { - builder.AppendLine("--- CONTENT TYPE MODEL ---"); - builder.AppendLine(typeModel.Id.ToString()); - builder.AppendLine(typeModel.Alias); - builder.AppendLine(typeModel.ClrName); - builder.AppendLine(typeModel.ParentId.ToString()); - builder.AppendLine(typeModel.Name); - builder.AppendLine(typeModel.Description); - builder.AppendLine(typeModel.ItemType.ToString()); - builder.AppendLine("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); - - foreach (var prop in typeModel.Properties.OrderBy(x => x.Alias)) - { - builder.AppendLine("--- PROPERTY ---"); - builder.AppendLine(prop.Alias); - builder.AppendLine(prop.ClrName); - builder.AppendLine(prop.Name); - builder.AppendLine(prop.Description); - builder.AppendLine(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName - } + builder.AppendLine("--- PROPERTY ---"); + builder.AppendLine(prop.Alias); + builder.AppendLine(prop.ClrName); + builder.AppendLine(prop.Name); + builder.AppendLine(prop.Description); + builder.AppendLine(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName } - - // Include the MB version in the hash so that if the MB version changes, models are rebuilt - builder.AppendLine(ApiVersion.Current.Version.ToString()); - - return builder.ToString().GenerateHash(); } + + // Include the MB version in the hash so that if the MB version changes, models are rebuilt + builder.AppendLine(ApiVersion.Current.Version.ToString()); + + return builder.ToString().GenerateHash(); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs index 474bea9251..53c70ef8ac 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ImplementPropertyTypeAttribute.cs @@ -1,16 +1,13 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +/// +/// Indicates that a property implements a given property alias. +/// +/// And therefore it should not be generated. +[AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] +public class ImplementPropertyTypeAttribute : Attribute { - /// - /// Indicates that a property implements a given property alias. - /// - /// And therefore it should not be generated. - [AttributeUsage(AttributeTargets.Property /*, AllowMultiple = false, Inherited = false*/)] - public class ImplementPropertyTypeAttribute : Attribute - { - public ImplementPropertyTypeAttribute(string alias) => Alias = alias; + public ImplementPropertyTypeAttribute(string alias) => Alias = alias; - public string Alias { get; } - } + public string Alias { get; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs index f016a3ecd2..073f72c6ad 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsBuilderAssemblyAttribute.cs @@ -1,23 +1,20 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +/// +/// Indicates that an Assembly is a Models Builder assembly. +/// +[AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] +public sealed class ModelsBuilderAssemblyAttribute : Attribute { /// - /// Indicates that an Assembly is a Models Builder assembly. + /// Gets or sets a value indicating whether the assembly is a InMemory assembly. /// - [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] - public sealed class ModelsBuilderAssemblyAttribute : Attribute - { - /// - /// Gets or sets a value indicating whether the assembly is a InMemory assembly. - /// - /// A Models Builder assembly can be either InMemory or a normal Dll. - public bool IsInMemory { get; set; } + /// A Models Builder assembly can be either InMemory or a normal Dll. + public bool IsInMemory { get; set; } - /// - /// Gets or sets a hash value representing the state of the custom source code files - /// and the Umbraco content types that were used to generate and compile the assembly. - /// - public string? SourceHash { get; set; } - } + /// + /// Gets or sets a hash value representing the state of the custom source code files + /// and the Umbraco content types that were used to generate and compile the assembly. + /// + public string? SourceHash { get; set; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs index b421042928..02db02afda 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/ModelsGenerationError.cs @@ -1,87 +1,84 @@ -using System; -using System.IO; using System.Text; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public sealed class ModelsGenerationError { - public sealed class ModelsGenerationError + private readonly IHostingEnvironment _hostingEnvironment; + private ModelsBuilderSettings _config; + + /// + /// Initializes a new instance of the class. + /// + public ModelsGenerationError(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) { - private ModelsBuilderSettings _config; - private readonly IHostingEnvironment _hostingEnvironment; + _config = config.CurrentValue; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } - /// - /// Initializes a new instance of the class. - /// - public ModelsGenerationError(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) + public void Clear() + { + var errFile = GetErrFile(); + if (errFile == null) { - _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); + return; } - public void Clear() - { - var errFile = GetErrFile(); - if (errFile == null) - { - return; - } + // "If the file to be deleted does not exist, no exception is thrown." + File.Delete(errFile); + } - // "If the file to be deleted does not exist, no exception is thrown." - File.Delete(errFile); + public void Report(string message, Exception e) + { + var errFile = GetErrFile(); + if (errFile == null) + { + return; } - public void Report(string message, Exception e) + var sb = new StringBuilder(); + sb.Append(message); + sb.Append("\r\n"); + sb.Append(e.Message); + sb.Append("\r\n\r\n"); + sb.Append(e.StackTrace); + sb.Append("\r\n"); + + File.WriteAllText(errFile, sb.ToString()); + } + + public string? GetLastError() + { + var errFile = GetErrFile(); + if (errFile == null) { - var errFile = GetErrFile(); - if (errFile == null) - { - return; - } - - var sb = new StringBuilder(); - sb.Append(message); - sb.Append("\r\n"); - sb.Append(e.Message); - sb.Append("\r\n\r\n"); - sb.Append(e.StackTrace); - sb.Append("\r\n"); - - File.WriteAllText(errFile, sb.ToString()); + return null; } - public string? GetLastError() + try { - var errFile = GetErrFile(); - if (errFile == null) - { - return null; - } - - try - { - return File.ReadAllText(errFile); - } - catch - { - // accepted - return null; - } + return File.ReadAllText(errFile); } - - private string? GetErrFile() + catch { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - return null; - } - - return Path.Combine(modelsDirectory, "models.err"); + // accepted + return null; } } + + private string? GetErrFile() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) + { + return null; + } + + return Path.Combine(modelsDirectory, "models.err"); + } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs index 1d9ea7d499..4336f8ec71 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/OutOfDateModelsStatus.cs @@ -1,102 +1,98 @@ -using System.IO; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Notifications; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// Used to track if ModelsBuilder models are out of date/stale +/// +public sealed class OutOfDateModelsStatus : INotificationHandler, + INotificationHandler { + private readonly IHostingEnvironment _hostingEnvironment; + private ModelsBuilderSettings _config; + /// - /// Used to track if ModelsBuilder models are out of date/stale + /// Initializes a new instance of the class. /// - public sealed class OutOfDateModelsStatus : INotificationHandler, - INotificationHandler + public OutOfDateModelsStatus(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) { - private ModelsBuilderSettings _config; - private readonly IHostingEnvironment _hostingEnvironment; + _config = config.CurrentValue; + _hostingEnvironment = hostingEnvironment; + config.OnChange(x => _config = x); + } - /// - /// Initializes a new instance of the class. - /// - public OutOfDateModelsStatus(IOptionsMonitor config, IHostingEnvironment hostingEnvironment) - { - _config = config.CurrentValue; - _hostingEnvironment = hostingEnvironment; - config.OnChange(x => _config = x); - } + /// + /// Gets a value indicating whether flagging out of date models is enabled + /// + public bool IsEnabled => _config.FlagOutOfDateModels; - /// - /// Gets a value indicating whether flagging out of date models is enabled - /// - public bool IsEnabled => _config.FlagOutOfDateModels; - - /// - /// Gets a value indicating whether models are out of date - /// - public bool IsOutOfDate - { - get - { - if (_config.FlagOutOfDateModels == false) - { - return false; - } - - var path = GetFlagPath(); - return path != null && File.Exists(path); - } - } - - - private string GetFlagPath() - { - var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); - if (!Directory.Exists(modelsDirectory)) - { - Directory.CreateDirectory(modelsDirectory); - } - - return Path.Combine(modelsDirectory, "ood.flag"); - } - - private void Write() - { - // don't run if not configured - if (!IsEnabled) - { - return; - } - - var path = GetFlagPath(); - if (path == null || File.Exists(path)) - { - return; - } - - File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); - } - - public void Clear() + /// + /// Gets a value indicating whether models are out of date + /// + public bool IsOutOfDate + { + get { if (_config.FlagOutOfDateModels == false) { - return; + return false; } var path = GetFlagPath(); - if (path == null || !File.Exists(path)) - { - return; - } + return path != null && File.Exists(path); + } + } - File.Delete(path); + public void Handle(ContentTypeCacheRefresherNotification notification) => Write(); + + public void Handle(DataTypeCacheRefresherNotification notification) => Write(); + + public void Clear() + { + if (_config.FlagOutOfDateModels == false) + { + return; } - public void Handle(ContentTypeCacheRefresherNotification notification) => Write(); + var path = GetFlagPath(); + if (!File.Exists(path)) + { + return; + } - public void Handle(DataTypeCacheRefresherNotification notification) => Write(); + File.Delete(path); + } + + private string GetFlagPath() + { + var modelsDirectory = _config.ModelsDirectoryAbsolute(_hostingEnvironment); + if (!Directory.Exists(modelsDirectory)) + { + Directory.CreateDirectory(modelsDirectory); + } + + return Path.Combine(modelsDirectory, "ood.flag"); + } + + private void Write() + { + // don't run if not configured + if (!IsEnabled) + { + return; + } + + var path = GetFlagPath(); + if (path == null || File.Exists(path)) + { + return; + } + + File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs index 85d953da3a..5da139b147 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedElementExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Linq.Expressions; using System.Reflection; using Umbraco.Cms.Core.Models.PublishedContent; @@ -6,46 +5,60 @@ using Umbraco.Cms.Infrastructure.ModelsBuilder; // same namespace as original Umbraco.Web PublishedElementExtensions // ReSharper disable once CheckNamespace -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to models. +/// +public static class PublishedElementExtensions { /// - /// Provides extension methods to models. + /// Gets the value of a property. /// - public static class PublishedElementExtensions + public static TValue? ValueFor( + this TModel model, + IPublishedValueFallback publishedValueFallback, + Expression> property, + string? culture = null, + string? segment = null, + Fallback fallback = default, + TValue? defaultValue = default) + where TModel : IPublishedElement { - /// - /// Gets the value of a property. - /// - public static TValue? ValueFor(this TModel model, IPublishedValueFallback publishedValueFallback, Expression> property, string? culture = null, string? segment = null, Fallback fallback = default, TValue? defaultValue = default) - where TModel : IPublishedElement + var alias = GetAlias(model, property); + return model.Value(publishedValueFallback, alias, culture, segment, fallback, defaultValue); + } + + // fixme that one should be public so ppl can use it + private static string GetAlias(TModel model, Expression> property) + { + if (property.NodeType != ExpressionType.Lambda) { - var alias = GetAlias(model, property); - return model.Value(publishedValueFallback, alias, culture, segment, fallback, defaultValue); + throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); } - // fixme that one should be public so ppl can use it - private static string GetAlias(TModel model, Expression> property) + var lambda = (LambdaExpression)property; + Expression lambdaBody = lambda.Body; + + if (lambdaBody.NodeType != ExpressionType.MemberAccess) { - if (property.NodeType != ExpressionType.Lambda) - throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); - - var lambda = (LambdaExpression) property; - var lambdaBody = lambda.Body; - - if (lambdaBody.NodeType != ExpressionType.MemberAccess) - throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); - - var memberExpression = (MemberExpression) lambdaBody; - if (memberExpression.Expression?.NodeType != ExpressionType.Parameter) - throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); - - var member = memberExpression.Member; - - var attribute = member.GetCustomAttribute(); - if (attribute == null) - throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - - return attribute.Alias; + throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); } + + var memberExpression = (MemberExpression)lambdaBody; + if (memberExpression.Expression?.NodeType != ExpressionType.Parameter) + { + throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); + } + + MemberInfo member = memberExpression.Member; + + ImplementPropertyTypeAttribute? attribute = member.GetCustomAttribute(); + if (attribute == null) + { + throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); + } + + return attribute.Alias; } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs index b782751dd8..cfcbd82229 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/PublishedModelUtility.cs @@ -1,74 +1,77 @@ -using System; -using System.Linq; using System.Linq.Expressions; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +/// +/// This is called from within the generated model classes +/// +/// +/// DO NOT REMOVE - although there are not code references this is used directly by the generated models. +/// +public static class PublishedModelUtility { - /// - /// This is called from within the generated model classes - /// - /// - /// DO NOT REMOVE - although there are not code references this is used directly by the generated models. - /// - public static class PublishedModelUtility + // looks safer but probably useless... ppl should not call these methods directly + // and if they do... they have to take care about not doing stupid things + + // public static PublishedPropertyType GetModelPropertyType2(Expression> selector) + // where T : PublishedContentModel + // { + // var type = typeof (T); + // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); + // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; + // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); + // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; + + // var contentType = PublishedContentType.Get(itemType, alias); + // // etc... + // } + public static IPublishedContentType? GetModelContentType( + IPublishedSnapshotAccessor publishedSnapshotAccessor, + PublishedItemType itemType, + string alias) { - // looks safer but probably useless... ppl should not call these methods directly - // and if they do... they have to take care about not doing stupid things - - //public static PublishedPropertyType GetModelPropertyType2(Expression> selector) - // where T : PublishedContentModel - //{ - // var type = typeof (T); - // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); - // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; - // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); - // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; - - // var contentType = PublishedContentType.Get(itemType, alias); - // // etc... - //} - - public static IPublishedContentType? GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor, PublishedItemType itemType, string alias) + IPublishedSnapshot publishedSnapshot = publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); + switch (itemType) { - var publishedSnapshot = publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); - switch (itemType) - { - case PublishedItemType.Content: - return publishedSnapshot.Content?.GetContentType(alias); - case PublishedItemType.Media: - return publishedSnapshot.Media?.GetContentType(alias); - case PublishedItemType.Member: - return publishedSnapshot.Members?.GetContentType(alias); - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - } - - public static IPublishedPropertyType? GetModelPropertyType(IPublishedContentType contentType, Expression> selector) - //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel - { - // fixme therefore, missing a check on TModel here - - var expr = selector.Body as MemberExpression; - - if (expr == null) - throw new ArgumentException("Not a property expression.", nameof(selector)); - - // there _is_ a risk that contentType and T do not match - // see note above : accepted risk... - - var attr = expr.Member - .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) - .OfType() - .SingleOrDefault(); - - if (string.IsNullOrWhiteSpace(attr?.Alias)) - throw new InvalidOperationException($"Could not figure out property alias for property \"{expr.Member.Name}\"."); - - return contentType.GetPropertyType(attr.Alias); + case PublishedItemType.Content: + return publishedSnapshot.Content?.GetContentType(alias); + case PublishedItemType.Media: + return publishedSnapshot.Media?.GetContentType(alias); + case PublishedItemType.Member: + return publishedSnapshot.Members?.GetContentType(alias); + default: + throw new ArgumentOutOfRangeException(nameof(itemType)); } } + + public static IPublishedPropertyType? GetModelPropertyType( + IPublishedContentType contentType, + Expression> selector) + + // where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel + { + // fixme therefore, missing a check on TModel here + if (selector.Body is not MemberExpression expr) + { + throw new ArgumentException("Not a property expression.", nameof(selector)); + } + + // there _is_ a risk that contentType and T do not match + // see note above : accepted risk... + ImplementPropertyTypeAttribute? attr = expr.Member + .GetCustomAttributes(typeof(ImplementPropertyTypeAttribute), false) + .OfType() + .SingleOrDefault(); + + if (string.IsNullOrWhiteSpace(attr?.Alias)) + { + throw new InvalidOperationException( + $"Could not figure out property alias for property \"{expr.Member.Name}\"."); + } + + return contentType.GetPropertyType(attr.Alias); + } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs index fd4b4495d9..4a0fcdb0e7 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/RoslynCompiler.cs @@ -1,80 +1,78 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyModel; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public class RoslynCompiler { - public class RoslynCompiler + public const string GeneratedAssemblyName = "ModelsGeneratedAssembly"; + + private readonly OutputKind _outputKind; + private readonly CSharpParseOptions _parseOptions; + private readonly IEnumerable _refs; + + /// + /// Initializes a new instance of the class. + /// + /// + /// Roslyn compiler which can be used to compile a c# file to a Dll assembly + /// + public RoslynCompiler() { - public const string GeneratedAssemblyName = "ModelsGeneratedAssembly"; + _outputKind = OutputKind.DynamicallyLinkedLibrary; + _parseOptions = + CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion + .Latest); // What languageversion should we default to? - private readonly OutputKind _outputKind; - private readonly CSharpParseOptions _parseOptions; - private readonly IEnumerable _refs; - - /// - /// Initializes a new instance of the class. - /// - /// - /// Roslyn compiler which can be used to compile a c# file to a Dll assembly - /// - public RoslynCompiler() - { - _outputKind = OutputKind.DynamicallyLinkedLibrary; - _parseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); // What languageversion should we default to? - - // In order to dynamically compile the assembly, we need to add all refs from our current - // application. This will also add the correct framework dependencies and we won't have to worry - // about the specific framework that is currently being run. - // This was borrowed from: https://github.com/dotnet/core/issues/2082#issuecomment-442713181 - // because we were running into the same error as that thread because we were either: - // - not adding enough of the runtime dependencies OR - // - we were explicitly adding the wrong runtime dependencies - // ... at least that the gist of what I can tell. - MetadataReference[] refs = - DependencyContext.Default.CompileLibraries + // In order to dynamically compile the assembly, we need to add all refs from our current + // application. This will also add the correct framework dependencies and we won't have to worry + // about the specific framework that is currently being run. + // This was borrowed from: https://github.com/dotnet/core/issues/2082#issuecomment-442713181 + // because we were running into the same error as that thread because we were either: + // - not adding enough of the runtime dependencies OR + // - we were explicitly adding the wrong runtime dependencies + // ... at least that the gist of what I can tell. + MetadataReference[] refs = + DependencyContext.Default.CompileLibraries .SelectMany(cl => cl.ResolveReferencePaths()) .Select(asm => MetadataReference.CreateFromFile(asm)) .ToArray(); - _refs = refs.ToList(); - } + _refs = refs.ToList(); + } - /// - /// Compile a source file to a dll - /// - /// Path to the source file containing the code to be compiled. - /// The path where the output assembly will be saved. - public void CompileToFile(string pathToSourceFile, string savePath) + /// + /// Compile a source file to a dll + /// + /// Path to the source file containing the code to be compiled. + /// The path where the output assembly will be saved. + public void CompileToFile(string pathToSourceFile, string savePath) + { + var sourceCode = File.ReadAllText(pathToSourceFile); + + var sourceText = SourceText.From(sourceCode); + + SyntaxTree syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); + + // Not entirely certain that assemblyIdentityComparer is nececary? + var compilation = CSharpCompilation.Create( + GeneratedAssemblyName, + new[] { syntaxTree }, + _refs, + new CSharpCompilationOptions( + _outputKind, + optimizationLevel: OptimizationLevel.Release, + assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); + + EmitResult emitResult = compilation.Emit(savePath); + + if (!emitResult.Success) { - var sourceCode = File.ReadAllText(pathToSourceFile); - - var sourceText = SourceText.From(sourceCode); - - var syntaxTree = SyntaxFactory.ParseSyntaxTree(sourceText, _parseOptions); - - var compilation = CSharpCompilation.Create( - GeneratedAssemblyName, - new[] { syntaxTree }, - references: _refs, - options: new CSharpCompilationOptions( - _outputKind, - optimizationLevel: OptimizationLevel.Release, - // Not entirely certain that assemblyIdentityComparer is nececary? - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); - - var emitResult = compilation.Emit(savePath); - - if (!emitResult.Success) - { - throw new InvalidOperationException("Roslyn compiler could not create ModelsBuilder dll:\n" + - string.Join("\n", emitResult.Diagnostics.Select(x=>x.GetMessage()))); - } + throw new InvalidOperationException("Roslyn compiler could not create ModelsBuilder dll:\n" + + string.Join("\n", emitResult.Diagnostics.Select(x => x.GetMessage()))); } } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs index 5d3187c707..7f8b029284 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/TypeExtensions.cs @@ -1,22 +1,21 @@ -using System; +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +internal static class TypeExtensions { - internal static class TypeExtensions + /// + /// Creates a generic instance of a generic type with the proper actual type of an object. + /// + /// A generic type such as Something{} + /// An object whose type is used as generic type param. + /// Arguments for the constructor. + /// A generic instance of the generic type with the proper type. + /// + /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return + /// a Something{Type1} if object1.GetType() is Type1. + /// + public static object? CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) { - /// - /// Creates a generic instance of a generic type with the proper actual type of an object. - /// - /// A generic type such as Something{} - /// An object whose type is used as generic type param. - /// Arguments for the constructor. - /// A generic instance of the generic type with the proper type. - /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return - /// a Something{Type1} if object1.GetType() is Type1. - public static object? CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) - { - var type = genericType.MakeGenericType(typeParmObj.GetType()); - return Activator.CreateInstance(type, ctorArgs); - } + Type type = genericType.MakeGenericType(typeParmObj.GetType()); + return Activator.CreateInstance(type, ctorArgs); } } diff --git a/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs index 8d096ee9e2..3711cd89c2 100644 --- a/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs +++ b/src/Umbraco.Infrastructure/ModelsBuilder/UmbracoServices.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -9,200 +6,236 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.ModelsBuilder.Building; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.ModelsBuilder +namespace Umbraco.Cms.Infrastructure.ModelsBuilder; + +public sealed class UmbracoServices { + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; + private readonly IShortStringHelper _shortStringHelper; - public sealed class UmbracoServices + /// + /// Initializes a new instance of the class. + /// + public UmbracoServices( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedContentTypeFactory publishedContentTypeFactory, + IShortStringHelper shortStringHelper) { - private readonly IContentTypeService _contentTypeService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IMemberTypeService _memberTypeService; - private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; - private readonly IShortStringHelper _shortStringHelper; + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _publishedContentTypeFactory = publishedContentTypeFactory; + _shortStringHelper = shortStringHelper; + } - /// - /// Initializes a new instance of the class. - /// - public UmbracoServices( - IContentTypeService contentTypeService, - IMediaTypeService mediaTypeService, - IMemberTypeService memberTypeService, - IPublishedContentTypeFactory publishedContentTypeFactory, - IShortStringHelper shortStringHelper) + public static string GetClrName(IShortStringHelper shortStringHelper, string? name, string alias) => + + // ModelsBuilder's legacy - but not ideal + alias.ToCleanString(shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); + + #region Services + + public IList GetAllTypes() + { + var types = new List(); + + // TODO: this will require 3 rather large SQL queries on startup in ModelsMode.InMemoryAuto mode. I know that these will be cached after lookup but it will slow + // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can + // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup + // in more than one place. + types.AddRange(GetTypes( + PublishedItemType.Content, + _contentTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes( + PublishedItemType.Media, + _mediaTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes( + PublishedItemType.Member, + _memberTypeService.GetAll().Cast().ToArray())); + + return EnsureDistinctAliases(types); + } + + public IList GetContentTypes() + { + IContentTypeComposition[] contentTypes = _contentTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here + } + + public IList GetMediaTypes() + { + IContentTypeComposition[] contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here + } + + public IList GetMemberTypes() + { + IContentTypeComposition[] memberTypes = _memberTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here + } + + internal static IList EnsureDistinctAliases(IList typeModels) + { + IEnumerable> groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); + foreach (IGrouping group in groups.Where(x => x.Count() > 1)) { - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _memberTypeService = memberTypeService; - _publishedContentTypeFactory = publishedContentTypeFactory; - _shortStringHelper = shortStringHelper; + throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" + + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." + + " One of the aliases must be modified in order to use the ModelsBuilder."); } - #region Services + return typeModels; + } - public IList GetAllTypes() + private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) + { + var typeModels = new List(); + var uniqueTypes = new HashSet(); + + // get the types and the properties + foreach (IContentTypeComposition contentType in contentTypes) { - var types = new List(); - - // TODO: this will require 3 rather large SQL queries on startup in ModelsMode.InMemoryAuto mode. I know that these will be cached after lookup but it will slow - // down startup time ... BUT these queries are also used in NuCache on startup so we can't really avoid them. Maybe one day we can - // load all of these in in one query and still have them cached per service, and/or somehow improve the perf of these since they are used on startup - // in more than one place. - types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); - - return EnsureDistinctAliases(types); - } - - public IList GetContentTypes() - { - var contentTypes = _contentTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here - } - - public IList GetMediaTypes() - { - var contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here - } - - public IList GetMemberTypes() - { - var memberTypes = _memberTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here - } - - public static string GetClrName(IShortStringHelper shortStringHelper, string? name, string alias) - { - // ModelsBuilder's legacy - but not ideal - return alias.ToCleanString(shortStringHelper, CleanStringType.ConvertCase | CleanStringType.PascalCase); - } - - private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) - { - var typeModels = new List(); - var uniqueTypes = new HashSet(); - - // get the types and the properties - foreach (var contentType in contentTypes) + var typeModel = new TypeModel { - var typeModel = new TypeModel - { - Id = contentType.Id, - Alias = contentType.Alias, - ClrName = GetClrName(_shortStringHelper, contentType.Name, contentType.Alias), - ParentId = contentType.ParentId, + Id = contentType.Id, + Alias = contentType.Alias, + ClrName = GetClrName(_shortStringHelper, contentType.Name, contentType.Alias), + ParentId = contentType.ParentId, + Name = contentType.Name, + Description = contentType.Description, + }; - Name = contentType.Name, - Description = contentType.Description + // of course this should never happen, but when it happens, better detect it + // else we end up with weird nullrefs everywhere + if (uniqueTypes.Contains(typeModel.ClrName)) + { + throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); + } + + uniqueTypes.Add(typeModel.ClrName); + + IPublishedContentType publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); + switch (itemType) + { + case PublishedItemType.Content: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Content; + break; + case PublishedItemType.Media: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Media; + break; + case PublishedItemType.Member: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Member; + break; + default: + throw new InvalidOperationException(string.Format( + "Unsupported PublishedItemType \"{0}\".", + itemType)); + } + + typeModels.Add(typeModel); + + foreach (IPropertyType propertyType in contentType.PropertyTypes) + { + var propertyModel = new PropertyModel + { + Alias = propertyType.Alias, + ClrName = GetClrName(_shortStringHelper, propertyType.Name, propertyType.Alias), + Name = propertyType.Name, + Description = propertyType.Description, }; - // of course this should never happen, but when it happens, better detect it - // else we end up with weird nullrefs everywhere - if (uniqueTypes.Contains(typeModel.ClrName)) - throw new PanicException($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); - uniqueTypes.Add(typeModel.ClrName); - - var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); - switch (itemType) + IPublishedPropertyType? publishedPropertyType = + publishedContentType.GetPropertyType(propertyType.Alias); + if (publishedPropertyType == null) { - case PublishedItemType.Content: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Content; - break; - case PublishedItemType.Media: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Media; - break; - case PublishedItemType.Member: - typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element - ? TypeModel.ItemTypes.Element - : TypeModel.ItemTypes.Member; - break; - default: - throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); + throw new PanicException( + $"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); } - typeModels.Add(typeModel); + propertyModel.ModelClrType = publishedPropertyType.ModelClrType; - foreach (var propertyType in contentType.PropertyTypes) - { - var propertyModel = new PropertyModel - { - Alias = propertyType.Alias, - ClrName = GetClrName(_shortStringHelper, propertyType.Name, propertyType.Alias), - - Name = propertyType.Name, - Description = propertyType.Description - }; - - var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); - if (publishedPropertyType == null) - throw new PanicException($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); - - propertyModel.ModelClrType = publishedPropertyType.ModelClrType; - - typeModel.Properties.Add(propertyModel); - } + typeModel.Properties.Add(propertyModel); } - - // wire the base types - foreach (var typeModel in typeModels.Where(x => x.ParentId > 0)) - { - typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); - // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might - // not be a content type - here we assume that BaseType being null while ParentId > 0 means that - // the parent is a container (and we don't check). - typeModel.IsParent = typeModel.BaseType != null; - } - - // discover mixins - foreach (var contentType in contentTypes) - { - var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); - if (typeModel == null) throw new PanicException("Panic: no type model matching content type."); - - IEnumerable compositionTypes; - var contentTypeAsMedia = contentType as IMediaType; - var contentTypeAsContent = contentType as IContentType; - var contentTypeAsMember = contentType as IMemberType; - if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; - else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; - else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; - else throw new PanicException(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); - - foreach (var compositionType in compositionTypes) - { - var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); - if (compositionModel == null) throw new PanicException("Panic: composition type does not exist."); - - if (compositionType.Id == contentType.ParentId) continue; - - // add to mixins - typeModel.MixinTypes.Add(compositionModel); - - // mark as mixin - as well as parents - compositionModel.IsMixin = true; - while ((compositionModel = compositionModel.BaseType) != null) - compositionModel.IsMixin = true; - } - } - - return typeModels; } - internal static IList EnsureDistinctAliases(IList typeModels) + // wire the base types + foreach (TypeModel typeModel in typeModels.Where(x => x.ParentId > 0)) { - var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); - foreach (var group in groups.Where(x => x.Count() > 1)) - throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" - + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." - + " One of the aliases must be modified in order to use the ModelsBuilder."); - return typeModels; + typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); + + // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might + // not be a content type - here we assume that BaseType being null while ParentId > 0 means that + // the parent is a container (and we don't check). + typeModel.IsParent = typeModel.BaseType != null; } - #endregion + // discover mixins + foreach (IContentTypeComposition contentType in contentTypes) + { + TypeModel? typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); + if (typeModel == null) + { + throw new PanicException("Panic: no type model matching content type."); + } + + IEnumerable compositionTypes; + if (contentType is IMediaType contentTypeAsMedia) + { + compositionTypes = contentTypeAsMedia.ContentTypeComposition; + } + else if (contentType is IContentType contentTypeAsContent) + { + compositionTypes = contentTypeAsContent.ContentTypeComposition; + } + else if (contentType is IMemberType contentTypeAsMember) + { + compositionTypes = contentTypeAsMember.ContentTypeComposition; + } + else + { + throw new PanicException(string.Format( + "Panic: unsupported type \"{0}\".", + contentType.GetType().FullName)); + } + + foreach (IContentTypeComposition compositionType in compositionTypes) + { + TypeModel? compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); + if (compositionModel == null) + { + throw new PanicException("Panic: composition type does not exist."); + } + + if (compositionType.Id == contentType.ParentId) + { + continue; + } + + // add to mixins + typeModel.MixinTypes.Add(compositionModel); + + // mark as mixin - as well as parents + compositionModel.IsMixin = true; + while ((compositionModel = compositionModel.BaseType) != null) + { + compositionModel.IsMixin = true; + } + } + } + + return typeModels; } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs index 92e1086fbd..cec42492a3 100644 --- a/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/AutomaticPackageMigrationPlan.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; @@ -7,43 +6,52 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +/// +/// Used to automatically indicate that a package has an embedded package data manifest that needs to be installed +/// +public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan { - /// - /// Used to automatically indicate that a package has an embedded package data manifest that needs to be installed - /// - public abstract class AutomaticPackageMigrationPlan : PackageMigrationPlan + protected AutomaticPackageMigrationPlan(string packageName) + : this(packageName, packageName) { - protected AutomaticPackageMigrationPlan(string packageName) - : this(packageName, packageName) - { } + } - protected AutomaticPackageMigrationPlan(string packageName, string planName) - : base(packageName, planName) - { } + protected AutomaticPackageMigrationPlan(string packageName, string planName) + : base(packageName, planName) + { + } - protected sealed override void DefinePlan() + protected sealed override void DefinePlan() + { + // calculate the final state based on the hash value of the embedded resource + Type planType = GetType(); + var hash = PackageMigrationResource.GetEmbeddedPackageDataManifestHash(planType); + + var finalId = hash.ToGuid(); + To(finalId); + } + + private class MigrateToPackageData : PackageMigrationBase + { + public MigrateToPackageData( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) + : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context) { - // calculate the final state based on the hash value of the embedded resource - Type planType = GetType(); - var hash = PackageMigrationResource.GetEmbeddedPackageDataManifestHash(planType); - - var finalId = hash.ToGuid(); - To(finalId); } - private class MigrateToPackageData : PackageMigrationBase + protected override void Migrate() { - public MigrateToPackageData(IPackagingService packagingService, IMediaService mediaService, MediaFileManager mediaFileManager, MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, IMigrationContext context) : base(packagingService, mediaService, mediaFileManager, mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, context) - { - } + var plan = (AutomaticPackageMigrationPlan)Context.Plan; - protected override void Migrate() - { - var plan = (AutomaticPackageMigrationPlan)Context.Plan; - - ImportPackage.FromEmbeddedResource(plan.GetType()).Do(); - } + ImportPackage.FromEmbeddedResource(plan.GetType()).Do(); } } } diff --git a/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs index 994ac643c6..f826dd9dfe 100644 --- a/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/IImportPackageBuilder.cs @@ -1,17 +1,15 @@ -using System; using System.Xml.Linq; using Umbraco.Cms.Infrastructure.Migrations.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public interface IImportPackageBuilder : IFluentBuilder { - public interface IImportPackageBuilder : IFluentBuilder - { - IExecutableBuilder FromEmbeddedResource() - where TPackageMigration : PackageMigrationBase; + IExecutableBuilder FromEmbeddedResource() + where TPackageMigration : PackageMigrationBase; - IExecutableBuilder FromEmbeddedResource(Type packageMigrationType); + IExecutableBuilder FromEmbeddedResource(Type packageMigrationType); - IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest); - } + IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest); } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs index fef61a54c3..8b28628e4c 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Xml.Linq; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -10,50 +9,50 @@ using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Infrastructure.Migrations.Expressions; using Umbraco.Cms.Infrastructure.Migrations.Expressions.Common; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +internal class ImportPackageBuilder : ExpressionBuilderBase, IImportPackageBuilder, + IExecutableBuilder { - internal class ImportPackageBuilder : ExpressionBuilderBase, IImportPackageBuilder, IExecutableBuilder + public ImportPackageBuilder( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions options) + : base(new ImportPackageBuilderExpression( + packagingService, + mediaService, + mediaFileManager, + mediaUrlGenerators, + shortStringHelper, + contentTypeBaseServiceProvider, + context, + options)) { - public ImportPackageBuilder( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions options) - : base(new ImportPackageBuilderExpression( - packagingService, - mediaService, - mediaFileManager, - mediaUrlGenerators, - shortStringHelper, - contentTypeBaseServiceProvider, - context, - options)) - { - } + } - public void Do() => Expression.Execute(); + public void Do() => Expression.Execute(); - public IExecutableBuilder FromEmbeddedResource() - where TPackageMigration : PackageMigrationBase - { - Expression.EmbeddedResourceMigrationType = typeof(TPackageMigration); - return this; - } + public IExecutableBuilder FromEmbeddedResource() + where TPackageMigration : PackageMigrationBase + { + Expression.EmbeddedResourceMigrationType = typeof(TPackageMigration); + return this; + } - public IExecutableBuilder FromEmbeddedResource(Type packageMigrationType) - { - Expression.EmbeddedResourceMigrationType = packageMigrationType; - return this; - } + public IExecutableBuilder FromEmbeddedResource(Type packageMigrationType) + { + Expression.EmbeddedResourceMigrationType = packageMigrationType; + return this; + } - public IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest) - { - Expression.PackageDataManifest = packageDataManifest; - return this; - } + public IExecutableBuilder FromXmlDataManifest(XDocument packageDataManifest) + { + Expression.PackageDataManifest = packageDataManifest; + return this; } } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs index 04abcfa8a0..0e4bf757e8 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Microsoft.Extensions.Logging; @@ -17,136 +14,137 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +internal class ImportPackageBuilderExpression : MigrationExpressionBase { - internal class ImportPackageBuilderExpression : MigrationExpressionBase + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly PackageMigrationSettings _packageMigrationSettings; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; + + private bool _executed; + + public ImportPackageBuilderExpression( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions packageMigrationSettings) + : base(context) { - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly MediaFileManager _mediaFileManager; - private readonly IMediaService _mediaService; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IPackagingService _packagingService; - private readonly IShortStringHelper _shortStringHelper; - private readonly PackageMigrationSettings _packageMigrationSettings; + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationSettings = packageMigrationSettings.Value; + } - private bool _executed; + /// + /// The type of the migration which dictates the namespace of the embedded resource + /// + public Type? EmbeddedResourceMigrationType { get; set; } - public ImportPackageBuilderExpression( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions packageMigrationSettings) : base(context) + public XDocument? PackageDataManifest { get; set; } + + public override void Execute() + { + if (_executed) { - _packagingService = packagingService; - _mediaService = mediaService; - _mediaFileManager = mediaFileManager; - _mediaUrlGenerators = mediaUrlGenerators; - _shortStringHelper = shortStringHelper; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _packageMigrationSettings = packageMigrationSettings.Value; + throw new InvalidOperationException("This expression has already been executed."); } - /// - /// The type of the migration which dictates the namespace of the embedded resource - /// - public Type? EmbeddedResourceMigrationType { get; set; } + _executed = true; - public XDocument? PackageDataManifest { get; set; } + Context.BuildingExpression = false; - public override void Execute() + if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) { - if (_executed) - { - throw new InvalidOperationException("This expression has already been executed."); - } + throw new InvalidOperationException( + $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); + } - _executed = true; + if (!_packageMigrationSettings.RunSchemaAndContentMigrations) + { + Logger.LogInformation("Skipping import of embedded schema file, due to configuration"); + return; + } - Context.BuildingExpression = false; - - if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) - { - throw new InvalidOperationException( - $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); - } - - if (!_packageMigrationSettings.RunSchemaAndContentMigrations) - { - Logger.LogInformation("Skipping import of embedded schema file, due to configuration"); - return; - } - - InstallationSummary installationSummary; - if (EmbeddedResourceMigrationType != null) - { - if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( + InstallationSummary installationSummary; + if (EmbeddedResourceMigrationType != null) + { + if (PackageMigrationResource.TryGetEmbeddedPackageDataManifest( EmbeddedResourceMigrationType, - out XDocument? xml, out ZipArchive? zipPackage)) + out XDocument? xml, + out ZipArchive? zipPackage)) + { + // first install the package + installationSummary = _packagingService.InstallCompiledPackageData(xml!); + + if (zipPackage is not null) { - // first install the package - installationSummary = _packagingService.InstallCompiledPackageData(xml!); - - if (zipPackage is not null) + // get the embedded resource + using (zipPackage) { - // get the embedded resource - using (zipPackage) + // then we need to save each file to the saved media items + var mediaWithFiles = xml!.XPathSelectElements( + "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") + .ToDictionary( + x => x.AttributeValue("key"), + x => x.AttributeValue("mediaFilePath")); + + // Any existing media by GUID will not be installed by the package service, it will just be skipped + // so you cannot 'update' media (or content) using a package since those are not schema type items. + // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled + // will be empty for any existing media which means that the files will also not be updated. + foreach (IMedia media in installationSummary.MediaInstalled) { - // then we need to save each file to the saved media items - var mediaWithFiles = xml!.XPathSelectElements( - "./umbPackage/MediaItems/MediaSet//*[@id][@mediaFilePath]") - .ToDictionary( - x => x.AttributeValue("key"), - x => x.AttributeValue("mediaFilePath")); - - // Any existing media by GUID will not be installed by the package service, it will just be skipped - // so you cannot 'update' media (or content) using a package since those are not schema type items. - // This means you cannot 'update' the media file either. The installationSummary.MediaInstalled - // will be empty for any existing media which means that the files will also not be updated. - foreach (IMedia media in installationSummary.MediaInstalled) + if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) { - if (mediaWithFiles.TryGetValue(media.Key, out var mediaFilePath)) + // this is a media item that has a file, so find that file in the zip + var entryPath = $"media{mediaFilePath!.EnsureStartsWith('/')}"; + ZipArchiveEntry? mediaEntry = zipPackage.GetEntry(entryPath); + if (mediaEntry == null) { - // this is a media item that has a file, so find that file in the zip - var entryPath = $"media{mediaFilePath!.EnsureStartsWith('/')}"; - ZipArchiveEntry? mediaEntry = zipPackage.GetEntry(entryPath); - if (mediaEntry == null) - { - throw new InvalidOperationException( - "No media file found in package zip for path " + - entryPath); - } - - // read the media file and save it to the media item - // using the current file system provider. - using (Stream mediaStream = mediaEntry.Open()) - { - media.SetValue( - _mediaFileManager, - _mediaUrlGenerators, - _shortStringHelper, - _contentTypeBaseServiceProvider, - Constants.Conventions.Media.File, - Path.GetFileName(mediaFilePath)!, - mediaStream); - } - - _mediaService.Save(media); + throw new InvalidOperationException( + "No media file found in package zip for path " + + entryPath); } + + // read the media file and save it to the media item + // using the current file system provider. + using (Stream mediaStream = mediaEntry.Open()) + { + media.SetValue( + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Constants.Conventions.Media.File, + Path.GetFileName(mediaFilePath)!, + mediaStream); + } + + _mediaService.Save(media); } } } } - else - { - installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); - } - - Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); } + else + { + installationSummary = _packagingService.InstallCompiledPackageData(PackageDataManifest); + } + + Logger.LogInformation($"Package migration executed. Summary: {installationSummary}"); } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index c870f2b7c6..b9960ab153 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -115,7 +115,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId) { - using (var scope = _scopeProvider.CreateScope()) + using (IScope scope = _scopeProvider.CreateScope()) { var installationSummary = new InstallationSummary(compiledPackage.Name) { @@ -200,10 +200,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// /// Imports and saves package xml as /// - /// Xml to import + /// The root contents to import from + /// The content type base service /// Optional parent Id for the content being imported /// A dictionary of already imported document types (basically used as a cache) /// Optional Id of the user performing the import + /// The content service base /// An enumerable list of generated content public IEnumerable ImportContentBase( IEnumerable roots, @@ -267,8 +269,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging importedContentTypes.Add(contentTypeAlias, contentType); } - if (TryCreateContentFromXml(root, importedContentTypes[contentTypeAlias], null, parentId, service, - out var content)) + if (TryCreateContentFromXml(root, importedContentTypes[contentTypeAlias], null, parentId, service, out TContentBase content)) { contents.Add(content); } @@ -296,19 +297,19 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var list = new List(); - foreach (var child in children) + foreach (XElement child in children) { string contentTypeAlias = child.Name.LocalName; if (importedContentTypes.ContainsKey(contentTypeAlias) == false) { - var contentType = FindContentTypeByAlias(contentTypeAlias, typeService); + TContentTypeComposition contentType = FindContentTypeByAlias(contentTypeAlias, typeService); importedContentTypes.Add(contentTypeAlias, contentType); } // Create and add the child to the list if (TryCreateContentFromXml(child, importedContentTypes[contentTypeAlias], parent, default, service, - out var content)) + out TContentBase content)) { list.Add(content); } @@ -338,7 +339,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging Guid key = element.RequiredAttributeValue("key"); // we need to check if the content already exists and if so we ignore the installation for this item - var value = service.GetById(key); + TContentBase? value = service.GetById(key); if (value != null) { output = value; @@ -348,16 +349,15 @@ namespace Umbraco.Cms.Infrastructure.Packaging var level = element.Attribute("level")?.Value ?? string.Empty; var sortOrder = element.Attribute("sortOrder")?.Value ?? string.Empty; var nodeName = element.Attribute("nodeName")?.Value ?? string.Empty; - var path = element.Attribute("path")?.Value; var templateId = element.AttributeValue("template"); - var properties = from property in element.Elements() + IEnumerable? properties = from property in element.Elements() where property.Attribute("isDoc") == null select property; //TODO: This will almost never work, we can't reference a template by an INT Id within a package manifest, we need to change the // packager to package templates by UDI and resolve by the same, in 98% of cases, this isn't going to work, or it will resolve the wrong template. - var template = templateId.HasValue ? _fileService.GetTemplate(templateId.Value) : null; + ITemplate? template = templateId.HasValue ? _fileService.GetTemplate(templateId.Value) : null; //now double check this is correct since its an INT it could very well be pointing to an invalid template :/ if (template != null && contentType is IContentType contentTypex) @@ -390,7 +390,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging // Get the installed culture iso names, we create a localized content node with a culture that does not exist in the project // We have to use Invariant comparisons, because when we get them from ContentBase in EntityXmlSerializer they're all lowercase. var installedLanguages = _localizationService.GetAllLanguages().Select(l => l.IsoCode).ToArray(); - foreach (var localizedNodeName in element.Attributes() + foreach (XAttribute localizedNodeName in element.Attributes() .Where(a => a.Name.LocalName.InvariantStartsWith(nodeNamePrefix))) { var newCulture = localizedNodeName.Name.LocalName.Substring(nodeNamePrefix.Length); @@ -403,12 +403,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging //Here we make sure that we take composition properties in account as well //otherwise we would skip them and end up losing content - var propTypes = contentType.CompositionPropertyTypes.Any() + Dictionary propTypes = contentType.CompositionPropertyTypes.Any() ? contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x) : contentType.PropertyTypes.ToDictionary(x => x.Alias, x => x); var foundLanguages = new HashSet(); - foreach (var property in properties) + foreach (XElement property in properties) { string propertyTypeAlias = property.Name.LocalName; if (content.HasProperty(propertyTypeAlias)) @@ -418,7 +418,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging // Handle properties language attributes var propertyLang = property.Attribute(XName.Get("lang"))?.Value ?? null; foundLanguages.Add(propertyLang); - if (propTypes.TryGetValue(propertyTypeAlias, out var propertyType)) + if (propTypes.TryGetValue(propertyTypeAlias, out _)) { // set property value // Skip unsupported language variation, otherwise we'll get a "not supported error" @@ -522,9 +522,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Xml to import /// Boolean indicating whether or not to import the /// Optional id of the User performing the operation. Default is zero (admin). + /// The content type service. /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, - bool importStructure, int userId, IContentTypeBaseService service) + public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId, IContentTypeBaseService service) where T : class, IContentTypeComposition => ImportDocumentTypes(unsortedDocumentTypes, importStructure, userId, service); @@ -534,10 +534,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Xml to import /// Boolean indicating whether or not to import the /// Optional id of the User performing the operation. Default is zero (admin). + /// The content type service /// Collection of entity containers installed by the package to be populated with those created in installing data types. /// An enumerable list of generated ContentTypes - public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, - bool importStructure, int userId, IContentTypeBaseService service, + public IReadOnlyList ImportDocumentTypes( + IReadOnlyCollection unsortedDocumentTypes, + bool importStructure, int userId, + IContentTypeBaseService service, out IEnumerable entityContainersInstalled) where T : class, IContentTypeComposition { @@ -548,17 +551,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); var isSingleDocTypeImport = unsortedDocumentTypes.Count == 1; - var importedFolders = + Dictionary importedFolders = CreateContentTypeFolderStructure(unsortedDocumentTypes, out entityContainersInstalled); if (isSingleDocTypeImport == false) { //NOTE Here we sort the doctype XElements based on dependencies //before creating the doc types - this should also allow for a better structure/inheritance support. - foreach (var documentType in unsortedDocumentTypes) + foreach (XElement documentType in unsortedDocumentTypes) { - var elementCopy = documentType; - var infoElement = elementCopy.Element("Info"); + XElement elementCopy = documentType; + XElement? infoElement = elementCopy.Element("Info"); var dependencies = new HashSet(); //Add the Master as a dependency @@ -568,13 +571,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging } //Add compositions as dependencies - var compositionsElement = infoElement?.Element("Compositions"); + XElement? compositionsElement = infoElement?.Element("Compositions"); if (compositionsElement != null && compositionsElement.HasElements) { - var compositions = compositionsElement.Elements("Composition"); + IEnumerable? compositions = compositionsElement.Elements("Composition").ToArray(); if (compositions.Any()) { - foreach (var composition in compositions) + foreach (XElement composition in compositions) { dependencies.Add(composition.Value); } @@ -606,9 +609,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging } } - foreach (var contentType in importedContentTypes) + foreach (KeyValuePair contentType in importedContentTypes) { - var ct = contentType.Value; + T ct = contentType.Value; if (importedFolders.ContainsKey(ct.Alias)) { ct.ParentId = importedFolders[ct.Alias]; @@ -625,15 +628,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var updatedContentTypes = new List(); //Update the structure here - we can't do it until all DocTypes have been created - foreach (var documentType in documentTypes) + foreach (XElement documentType in documentTypes) { var alias = documentType.Element("Info")?.Element("Alias")?.Value; - var structureElement = documentType.Element("Structure"); + XElement? structureElement = documentType.Element("Structure"); //Ensure that we only update ContentTypes which has actual structure-elements if (structureElement == null || structureElement.Elements().Any() == false || alias is null) + { continue; + } - var updated = UpdateContentTypesStructure(importedContentTypes[alias], structureElement, + T updated = UpdateContentTypesStructure(importedContentTypes[alias], structureElement, importedContentTypes, service); updatedContentTypes.Add(updated); } @@ -714,7 +719,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging for (var i = 1; i < folders.Length; i++) { var folderName = WebUtility.UrlDecode(folders[i]); - Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; + Guid? folderKey = folderKeys.Length == folders.Length ? folderKeys[i] : null; current = CreateContentTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); trackEntityContainersInstalled.Add(current!); importedFolders[alias!] = current!.Id; @@ -728,7 +733,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private EntityContainer? CreateContentTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) { - var children = _entityService.GetChildren(current.Id).ToArray(); + IEntitySlim[] children = _entityService.GetChildren(current.Id).ToArray(); var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); if (found) { @@ -736,7 +741,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging return _contentTypeService.GetContainer(containerId); } - var tryCreateFolder = _contentTypeService.CreateContainer(current.Id, folderKey, folderName); + Attempt?> tryCreateFolder = _contentTypeService.CreateContainer(current.Id, folderKey, folderName); if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); @@ -769,7 +774,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging } var alias = infoElement?.Element("Alias")?.Value; - var contentType = CreateContentType(key, parent, -1, alias!); + T? contentType = CreateContentType(key, parent, -1, alias!); if (parent != null) { @@ -818,8 +823,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var key = Guid.Parse(documentType.Element("Info")!.Element("Key")!.Value); - var infoElement = documentType.Element("Info"); - var defaultTemplateElement = infoElement?.Element("DefaultTemplate"); + XElement? infoElement = documentType.Element("Info"); + XElement? defaultTemplateElement = infoElement?.Element("DefaultTemplate"); if (contentType is null) { @@ -828,31 +833,42 @@ namespace Umbraco.Cms.Infrastructure.Packaging contentType.Key = key; contentType.Name = infoElement!.Element("Name")!.Value; if (infoElement.Element("Key") != null) + { contentType.Key = new Guid(infoElement.Element("Key")!.Value); + } + contentType.Icon = infoElement.Element("Icon")?.Value; contentType.Thumbnail = infoElement.Element("Thumbnail")?.Value; contentType.Description = infoElement.Element("Description")?.Value; //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it. - var allowAtRoot = infoElement.Element("AllowAtRoot"); + XElement? allowAtRoot = infoElement.Element("AllowAtRoot"); if (allowAtRoot != null) + { contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true"); + } - var isListView = infoElement.Element("IsListView"); + XElement? isListView = infoElement.Element("IsListView"); if (isListView != null) + { contentType.IsContainer = isListView.Value.InvariantEquals("true"); + } - var isElement = infoElement.Element("IsElement"); + XElement? isElement = infoElement.Element("IsElement"); if (isElement != null) + { contentType.IsElement = isElement.Value.InvariantEquals("true"); + } - var variationsElement = infoElement.Element("Variations"); + XElement? variationsElement = infoElement.Element("Variations"); if (variationsElement != null) + { contentType.Variations = (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value); + } //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set - var masterElement = infoElement.Element("Master"); + XElement? masterElement = infoElement.Element("Master"); if (masterElement != null) { var masterAlias = masterElement.Value; @@ -864,16 +880,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging } //Update Compositions on the ContentType to ensure that they are as is defined in the package xml - var compositionsElement = infoElement.Element("Compositions"); + XElement? compositionsElement = infoElement.Element("Compositions"); if (compositionsElement != null && compositionsElement.HasElements) { - var compositions = compositionsElement.Elements("Composition"); + XElement[] compositions = compositionsElement.Elements("Composition").ToArray(); if (compositions.Any()) { - foreach (var composition in compositions) + foreach (XElement composition in compositions) { var compositionAlias = composition.Value; - var compositionContentType = importedContentTypes.ContainsKey(compositionAlias) + T? compositionContentType = importedContentTypes.ContainsKey(compositionAlias) ? importedContentTypes[compositionAlias] : service.Get(compositionAlias); contentType.AddContentType(compositionContentType); @@ -937,14 +953,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (allowedTemplatesElement != null && allowedTemplatesElement.Elements("Template").Any()) { var allowedTemplates = contentType.AllowedTemplates?.ToList(); - foreach (var templateElement in allowedTemplatesElement.Elements("Template")) + foreach (XElement templateElement in allowedTemplatesElement.Elements("Template")) { var alias = templateElement.Value; - var template = _fileService.GetTemplate(alias.ToSafeAlias(_shortStringHelper)); + ITemplate? template = _fileService.GetTemplate(alias.ToSafeAlias(_shortStringHelper)); if (template != null) { if (allowedTemplates?.Any(x => x.Id == template.Id) ?? true) + { continue; + } + allowedTemplates.Add(template); } else @@ -960,7 +979,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (string.IsNullOrEmpty((string?)defaultTemplateElement) == false) { - var defaultTemplate = + ITemplate? defaultTemplate = _fileService.GetTemplate(defaultTemplateElement.Value.ToSafeAlias(_shortStringHelper)); if (defaultTemplate != null) { @@ -979,10 +998,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging where T : IContentTypeComposition { if (propertyGroupsContainer == null) + { return; + } - var propertyGroupElements = propertyGroupsContainer.Elements("Tab"); - foreach (var propertyGroupElement in propertyGroupElements) + IEnumerable propertyGroupElements = propertyGroupsContainer.Elements("Tab"); + foreach (XElement propertyGroupElement in propertyGroupElements) { var name = propertyGroupElement.Element("Caption")! .Value; // TODO Rename to Name (same in EntityXmlSerializer) @@ -994,14 +1015,14 @@ namespace Umbraco.Cms.Infrastructure.Packaging } contentType.AddPropertyGroup(alias, name); - var propertyGroup = contentType.PropertyGroups[alias]; + PropertyGroup propertyGroup = contentType.PropertyGroups[alias]; - if (Guid.TryParse(propertyGroupElement.Element("Key")?.Value, out var key)) + if (Guid.TryParse(propertyGroupElement.Element("Key")?.Value, out Guid key)) { propertyGroup.Key = key; } - if (Enum.TryParse(propertyGroupElement.Element("Type")?.Value, out var type)) + if (Enum.TryParse(propertyGroupElement.Element("Type")?.Value, out PropertyGroupType type)) { propertyGroup.Type = type; } @@ -1022,13 +1043,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging { return; } - var properties = genericPropertiesElement.Elements("GenericProperty"); - foreach (var property in properties) + IEnumerable properties = genericPropertiesElement.Elements("GenericProperty"); + foreach (XElement property in properties) { var dataTypeDefinitionId = new Guid(property.Element("Definition")!.Value); //Unique Id for a DataTypeDefinition - var dataTypeDefinition = _dataTypeService.GetDataType(dataTypeDefinitionId); + IDataType? dataTypeDefinition = _dataTypeService.GetDataType(dataTypeDefinitionId); //If no DataTypeDefinition with the guid from the xml wasn't found OR the ControlId on the DataTypeDefinition didn't match the DataType Id //We look up a DataTypeDefinition that matches @@ -1042,7 +1063,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (dataTypeDefinition == null) { - var dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); + IDataType[]? dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias).ToArray(); if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) { dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); @@ -1050,7 +1071,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging } else if (dataTypeDefinition.EditorAlias != propertyEditorAlias) { - var dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias); + IDataType[]? dataTypeDefinitions = _dataTypeService.GetByEditorAlias(propertyEditorAlias).ToArray(); if (dataTypeDefinitions != null && dataTypeDefinitions.Any()) { dataTypeDefinition = dataTypeDefinitions.FirstOrDefault(); @@ -1071,11 +1092,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging .FirstOrDefault(); //if for some odd reason this isn't there then ignore if (dataTypeDefinition == null) + { continue; + } } var sortOrder = 0; - var sortOrderElement = property.Element("SortOrder"); + XElement? sortOrderElement = property.Element("SortOrder"); if (sortOrderElement != null) { int.TryParse(sortOrderElement.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, @@ -1108,7 +1131,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging propertyType.Key = new Guid(property.Element("Key")!.Value); } - var propertyGroupElement = property.Element("Tab"); + XElement? propertyGroupElement = property.Element("Tab"); if (propertyGroupElement == null || string.IsNullOrEmpty(propertyGroupElement.Value)) { contentType.AddPropertyType(propertyType); @@ -1133,11 +1156,11 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var allowedChildren = contentType.AllowedContentTypes?.ToList(); int sortOrder = allowedChildren?.Any() ?? false ? allowedChildren.Last().SortOrder : 0; - foreach (var element in structureElement.Elements()) + foreach (XElement element in structureElement.Elements()) { var alias = element.Value; - var allowedChild = importedContentTypes.ContainsKey(alias) + T? allowedChild = importedContentTypes.ContainsKey(alias) ? importedContentTypes[alias] : service.Get(alias); if (allowedChild == null) @@ -1149,7 +1172,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging } if (allowedChildren?.Any(x => x.Id.IsValueCreated && x.Id.Value == allowedChild.Id) ?? false) + { continue; + } allowedChildren?.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), sortOrder, allowedChild.Alias)); @@ -1163,15 +1188,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// /// Used during Content import to ensure that the ContentType of a content item exists /// - /// /// private T FindContentTypeByAlias(string contentTypeAlias, IContentTypeBaseService typeService) where T : IContentTypeComposition { - var contentType = typeService.Get(contentTypeAlias); + T? contentType = typeService.Get(contentTypeAlias); if (contentType == null) + { throw new Exception($"ContentType matching the passed in Alias: '{contentTypeAlias}' was null"); + } return contentType; } @@ -1201,25 +1227,27 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var dataTypes = new List(); - var importedFolders = CreateDataTypeFolderStructure(dataTypeElements, out entityContainersInstalled); + Dictionary importedFolders = CreateDataTypeFolderStructure(dataTypeElements, out entityContainersInstalled); - foreach (var dataTypeElement in dataTypeElements) + foreach (XElement dataTypeElement in dataTypeElements) { var dataTypeDefinitionName = dataTypeElement.AttributeValue("Name"); - var dataTypeDefinitionId = dataTypeElement.RequiredAttributeValue("Definition"); - var databaseTypeAttribute = dataTypeElement.Attribute("DatabaseType"); + Guid dataTypeDefinitionId = dataTypeElement.RequiredAttributeValue("Definition"); + XAttribute? databaseTypeAttribute = dataTypeElement.Attribute("DatabaseType"); var parentId = -1; if (dataTypeDefinitionName is not null && importedFolders.ContainsKey(dataTypeDefinitionName)) + { parentId = importedFolders[dataTypeDefinitionName]; + } - var definition = _dataTypeService.GetDataType(dataTypeDefinitionId); + IDataType? definition = _dataTypeService.GetDataType(dataTypeDefinitionId); //If the datatype definition doesn't already exist we create a new according to the one in the package xml if (definition == null) { - var databaseType = databaseTypeAttribute?.Value.EnumParse(true) ?? - ValueStorageType.Ntext; + ValueStorageType databaseType = databaseTypeAttribute?.Value.EnumParse(true) ?? + ValueStorageType.Ntext; // the Id field is actually the string property editor Alias // however, the actual editor with this alias could be installed with the package, and @@ -1227,8 +1255,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging // the actual editor - going with a void editor var editorAlias = dataTypeElement.Attribute("Id")?.Value?.Trim(); - if (!_propertyEditors.TryGet(editorAlias, out var editor)) + if (!_propertyEditors.TryGet(editorAlias, out IDataEditor? editor)) + { editor = new VoidEditor(_dataValueEditorFactory) {Alias = editorAlias ?? string.Empty}; + } var dataType = new DataType(editor, _serializer) { @@ -1240,8 +1270,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging var configurationAttributeValue = dataTypeElement.Attribute("Configuration")?.Value; if (!string.IsNullOrWhiteSpace(configurationAttributeValue)) + { dataType.Configuration = editor.GetConfigurationEditor() .FromDatabase(configurationAttributeValue, _serializer); + } dataTypes.Add(dataType); } @@ -1265,17 +1297,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var importedFolders = new Dictionary(); var trackEntityContainersInstalled = new List(); - foreach (var datatypeElement in datatypeElements) + foreach (XElement datatypeElement in datatypeElements) { - var foldersAttribute = datatypeElement.Attribute("Folders"); + XAttribute? foldersAttribute = datatypeElement.Attribute("Folders"); if (foldersAttribute != null) { var name = datatypeElement.Attribute("Name")?.Value; var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); - var folderKeysAttribute = datatypeElement.Attribute("FolderKeys"); + XAttribute? folderKeysAttribute = datatypeElement.Attribute("FolderKeys"); - var folderKeys = Array.Empty(); + Guid[] folderKeys = Array.Empty(); if (folderKeysAttribute != null) { folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash) @@ -1283,13 +1315,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging } var rootFolder = WebUtility.UrlDecode(folders[0]); - var rootFolderKey = folderKeys.Length > 0 ? folderKeys[0] : Guid.NewGuid(); + Guid rootFolderKey = folderKeys.Length > 0 ? folderKeys[0] : Guid.NewGuid(); //there will only be a single result by name for level 1 (root) containers - var current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); + EntityContainer? current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); if (current == null) { - var tryCreateFolder = _dataTypeService.CreateContainer(-1, rootFolderKey, rootFolder); + Attempt?> tryCreateFolder = _dataTypeService.CreateContainer(-1, rootFolderKey, rootFolder); if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", @@ -1306,8 +1338,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging for (var i = 1; i < folders.Length; i++) { var folderName = WebUtility.UrlDecode(folders[i]); - Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; - current = CreateDataTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current!); + Guid? folderKey = folderKeys.Length == folders.Length ? folderKeys[i] : null; + current = CreateDataTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); trackEntityContainersInstalled.Add(current!); importedFolders[name!] = current!.Id; } @@ -1320,7 +1352,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private EntityContainer? CreateDataTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) { - var children = _entityService.GetChildren(current.Id).ToArray(); + IEntitySlim[] children = _entityService.GetChildren(current.Id).ToArray(); var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); if (found) { @@ -1328,7 +1360,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging return _dataTypeService.GetContainer(containerId); } - var tryCreateFolder = _dataTypeService.CreateContainer(current.Id, folderKey, folderName); + Attempt?> tryCreateFolder = _dataTypeService.CreateContainer(current.Id, folderKey, folderName); if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", folderName); @@ -1398,7 +1430,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging List languages) { var translations = dictionaryItem.Translations.ToList(); - foreach (var valueElement in dictionaryItemElement.Elements("Value") + foreach (XElement valueElement in dictionaryItemElement.Elements("Value") .Where(v => DictionaryValueIsNew(translations, v))) { AddDictionaryTranslation(translations, valueElement, languages); @@ -1438,7 +1470,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging XElement valueElement, IEnumerable languages) { var languageId = valueElement.Attribute("LanguageCultureAlias")?.Value; - var language = languages.SingleOrDefault(l => l.IsoCode == languageId); + ILanguage? language = languages.SingleOrDefault(l => l.IsoCode == languageId); if (language == null) { return; @@ -1461,7 +1493,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging public IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId) { var list = new List(); - foreach (var languageElement in languageElements) + foreach (XElement languageElement in languageElements) { var isoCode = languageElement.AttributeValue("CultureAlias"); if (string.IsNullOrEmpty(isoCode)) @@ -1469,7 +1501,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging continue; } - var existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); + ILanguage? existingLanguage = _localizationService.GetLanguageByIsoCode(isoCode); if (existingLanguage != null) { continue; @@ -1560,35 +1592,35 @@ namespace Umbraco.Cms.Infrastructure.Packaging var macroSource = macroElement.Element("macroSource")!.Value; //Following xml elements are treated as nullable properties - var useInEditorElement = macroElement.Element("useInEditor"); + XElement? useInEditorElement = macroElement.Element("useInEditor"); var useInEditor = false; if (useInEditorElement != null && string.IsNullOrEmpty((string)useInEditorElement) == false) { useInEditor = bool.Parse(useInEditorElement.Value); } - var cacheDurationElement = macroElement.Element("refreshRate"); + XElement? cacheDurationElement = macroElement.Element("refreshRate"); var cacheDuration = 0; if (cacheDurationElement != null && string.IsNullOrEmpty((string)cacheDurationElement) == false) { cacheDuration = int.Parse(cacheDurationElement.Value, CultureInfo.InvariantCulture); } - var cacheByMemberElement = macroElement.Element("cacheByMember"); + XElement? cacheByMemberElement = macroElement.Element("cacheByMember"); var cacheByMember = false; if (cacheByMemberElement != null && string.IsNullOrEmpty((string)cacheByMemberElement) == false) { cacheByMember = bool.Parse(cacheByMemberElement.Value); } - var cacheByPageElement = macroElement.Element("cacheByPage"); + XElement? cacheByPageElement = macroElement.Element("cacheByPage"); var cacheByPage = false; if (cacheByPageElement != null && string.IsNullOrEmpty((string)cacheByPageElement) == false) { cacheByPage = bool.Parse(cacheByPageElement.Value); } - var dontRenderElement = macroElement.Element("dontRender"); + XElement? dontRenderElement = macroElement.Element("dontRender"); var dontRender = true; if (dontRenderElement != null && string.IsNullOrEmpty((string)dontRenderElement) == false) { @@ -1596,16 +1628,16 @@ namespace Umbraco.Cms.Infrastructure.Packaging } var existingMacro = _macroService.GetById(macroKey) as Macro; - var macro = existingMacro ?? new Macro(_shortStringHelper, macroAlias, macroName, macroSource, + Macro macro = existingMacro ?? new Macro(_shortStringHelper, macroAlias, macroName, macroSource, cacheByPage, cacheByMember, dontRender, useInEditor, cacheDuration) {Key = macroKey}; - var properties = macroElement.Element("properties"); + XElement? properties = macroElement.Element("properties"); if (properties != null) { int sortOrder = 0; foreach (XElement property in properties.Elements()) { - var propertyKey = property.RequiredAttributeValue("key"); + Guid propertyKey = property.RequiredAttributeValue("key"); var propertyName = property.Attribute("name")?.Value; var propertyAlias = property.Attribute("alias")!.Value; var editorAlias = property.Attribute("propertyType")!.Value; @@ -1723,10 +1755,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging _fileService.SaveStylesheet(s, userId); } - foreach (var prop in n.XPathSelectElements("Properties/Property")) + foreach (XElement prop in n.XPathSelectElements("Properties/Property")) { var alias = prop.Element("Alias")!.Value; - var sp = s.Properties?.SingleOrDefault(p => p != null && p.Alias == alias); + IStylesheetProperty? sp = s.Properties?.SingleOrDefault(p => p != null && p.Alias == alias); var name = prop.Element("Name")!.Value; if (sp == null) { @@ -1776,10 +1808,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); - foreach (var tempElement in templateElements) + foreach (XElement tempElement in templateElements) { var dependencies = new List(); - var elementCopy = tempElement; + XElement elementCopy = tempElement; //Ensure that the Master of the current template is part of the import, otherwise we ignore this dependency as part of the dependency sorting. if (string.IsNullOrEmpty((string?)elementCopy.Element("Master")) == false && templateElements.Any(x => (string?)x.Element("Alias") == (string?)elementCopy.Element("Master"))) @@ -1800,22 +1832,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging } //Sort templates by dependencies to a potential master template - var sorted = graph.GetSortedItems(); - foreach (var item in sorted) + IEnumerable> sorted = graph.GetSortedItems(); + foreach (TopoGraph.Node? item in sorted) { - var templateElement = item.Item; + XElement templateElement = item.Item; var templateName = templateElement.Element("Name")?.Value; var alias = templateElement.Element("Alias")!.Value; var design = templateElement.Element("Design")?.Value; - var masterElement = templateElement.Element("Master"); + XElement? masterElement = templateElement.Element("Master"); var existingTemplate = _fileService.GetTemplate(alias) as Template; - var template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); + Template? template = existingTemplate ?? new Template(_shortStringHelper, templateName, alias); // For new templates, use the serialized key if avaialble. - if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out var key)) + if (existingTemplate == null && Guid.TryParse(templateElement.Element("Key")?.Value, out Guid key)) { template.Key = key; } @@ -1825,16 +1857,20 @@ namespace Umbraco.Cms.Infrastructure.Packaging if (masterElement != null && string.IsNullOrEmpty((string)masterElement) == false) { template.MasterTemplateAlias = masterElement.Value; - var masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); + ITemplate? masterTemplate = templates.FirstOrDefault(x => x.Alias == masterElement.Value); if (masterTemplate != null) + { template.MasterTemplateId = new Lazy(() => masterTemplate.Id); + } } templates.Add(template); } if (templates.Any()) + { _fileService.SaveTemplate(templates, userId); + } return templates; } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs index bb9866e116..ecd17fac4f 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageInstallation.cs @@ -1,84 +1,101 @@ -using System; -using System.Linq; using System.Xml.Linq; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Packaging; using Umbraco.Cms.Core.Packaging; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public class PackageInstallation : IPackageInstallation { + private readonly PackageDataInstallation _packageDataInstallation; + private readonly CompiledPackageXmlParser _parser; - public class PackageInstallation : IPackageInstallation + /// + /// Initializes a new instance of the class. + /// + public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) { - private readonly PackageDataInstallation _packageDataInstallation; - private readonly CompiledPackageXmlParser _parser; + _packageDataInstallation = + packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + } - - /// - /// Initializes a new instance of the class. - /// - public PackageInstallation(PackageDataInstallation packageDataInstallation, CompiledPackageXmlParser parser) + public CompiledPackage ReadPackage(XDocument? packageXmlFile) + { + if (packageXmlFile == null) { - _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); - _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + throw new ArgumentNullException(nameof(packageXmlFile)); } - public CompiledPackage ReadPackage(XDocument? packageXmlFile) - { - if (packageXmlFile == null) - throw new ArgumentNullException(nameof(packageXmlFile)); + var compiledPackage = _parser.ToCompiledPackage(packageXmlFile); + return compiledPackage; + } - var compiledPackage = _parser.ToCompiledPackage(packageXmlFile); - return compiledPackage; + public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition) + { + packageDefinition = new PackageDefinition { Name = compiledPackage.Name }; + + InstallationSummary installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId); + + // Make sure the definition is up to date with everything (note: macro partial views are embedded in macros) + foreach (IDataType x in installationSummary.DataTypesInstalled) + { + packageDefinition.DataTypes.Add(x.Id.ToInvariantString()); } - public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId, out PackageDefinition packageDefinition) + foreach (ILanguage x in installationSummary.LanguagesInstalled) { - packageDefinition = new PackageDefinition - { - Name = compiledPackage.Name - }; - - InstallationSummary installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId); - - // Make sure the definition is up to date with everything (note: macro partial views are embedded in macros) - foreach (var x in installationSummary.DataTypesInstalled) - packageDefinition.DataTypes.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.LanguagesInstalled) - packageDefinition.Languages.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.DictionaryItemsInstalled) - packageDefinition.DictionaryItems.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.MacrosInstalled) - packageDefinition.Macros.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.TemplatesInstalled) - packageDefinition.Templates.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.DocumentTypesInstalled) - packageDefinition.DocumentTypes.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.MediaTypesInstalled) - packageDefinition.MediaTypes.Add(x.Id.ToInvariantString()); - - foreach (var x in installationSummary.StylesheetsInstalled) - packageDefinition.Stylesheets.Add(x.Path); - - foreach (var x in installationSummary.ScriptsInstalled) - packageDefinition.Scripts.Add(x.Path); - - foreach (var x in installationSummary.PartialViewsInstalled) - packageDefinition.PartialViews.Add(x.Path); - - packageDefinition.ContentNodeId = installationSummary.ContentInstalled.FirstOrDefault()?.Id.ToInvariantString(); - - foreach (var x in installationSummary.MediaInstalled) - packageDefinition.MediaUdis.Add(x.GetUdi()); - - return installationSummary; + packageDefinition.Languages.Add(x.Id.ToInvariantString()); } + foreach (IDictionaryItem x in installationSummary.DictionaryItemsInstalled) + { + packageDefinition.DictionaryItems.Add(x.Id.ToInvariantString()); + } + + foreach (IMacro x in installationSummary.MacrosInstalled) + { + packageDefinition.Macros.Add(x.Id.ToInvariantString()); + } + + foreach (ITemplate x in installationSummary.TemplatesInstalled) + { + packageDefinition.Templates.Add(x.Id.ToInvariantString()); + } + + foreach (IContentType x in installationSummary.DocumentTypesInstalled) + { + packageDefinition.DocumentTypes.Add(x.Id.ToInvariantString()); + } + + foreach (IMediaType x in installationSummary.MediaTypesInstalled) + { + packageDefinition.MediaTypes.Add(x.Id.ToInvariantString()); + } + + foreach (IFile x in installationSummary.StylesheetsInstalled) + { + packageDefinition.Stylesheets.Add(x.Path); + } + + foreach (IScript x in installationSummary.ScriptsInstalled) + { + packageDefinition.Scripts.Add(x.Path); + } + + foreach (IPartialView x in installationSummary.PartialViewsInstalled) + { + packageDefinition.PartialViews.Add(x.Path); + } + + packageDefinition.ContentNodeId = installationSummary.ContentInstalled.FirstOrDefault()?.Id.ToInvariantString(); + + foreach (IMedia x in installationSummary.MediaInstalled) + { + packageDefinition.MediaUdis.Add(x.GetUdi()); + } + + return installationSummary; } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs index 54b96955d4..ae739c4361 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -10,70 +9,68 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Packaging +namespace Umbraco.Cms.Infrastructure.Packaging; + +public abstract class PackageMigrationBase : MigrationBase { - public abstract class PackageMigrationBase : MigrationBase + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IOptions _packageMigrationsSettings; + private readonly IPackagingService _packagingService; + private readonly IShortStringHelper _shortStringHelper; + + public PackageMigrationBase( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context, + IOptions packageMigrationsSettings) + : base(context) { - private readonly IPackagingService _packagingService; - private readonly IMediaService _mediaService; - private readonly MediaFileManager _mediaFileManager; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IShortStringHelper _shortStringHelper; - private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; - private readonly IOptions _packageMigrationsSettings; - - public PackageMigrationBase( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context, - IOptions packageMigrationsSettings) - : base(context) - { - _packagingService = packagingService; - _mediaService = mediaService; - _mediaFileManager = mediaFileManager; - _mediaUrlGenerators = mediaUrlGenerators; - _shortStringHelper = shortStringHelper; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _packageMigrationsSettings = packageMigrationsSettings; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use ctor with all params")] - public PackageMigrationBase( - IPackagingService packagingService, - IMediaService mediaService, - MediaFileManager mediaFileManager, - MediaUrlGeneratorCollection mediaUrlGenerators, - IShortStringHelper shortStringHelper, - IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context) - : this( - packagingService, - mediaService, - mediaFileManager, - mediaUrlGenerators, - shortStringHelper, - contentTypeBaseServiceProvider, - context, - StaticServiceProvider.Instance.GetRequiredService>()) - { - } - - public IImportPackageBuilder ImportPackage => BeginBuild( - new ImportPackageBuilder( - _packagingService, - _mediaService, - _mediaFileManager, - _mediaUrlGenerators, - _shortStringHelper, - _contentTypeBaseServiceProvider, - Context, - _packageMigrationsSettings)); - + _packagingService = packagingService; + _mediaService = mediaService; + _mediaFileManager = mediaFileManager; + _mediaUrlGenerators = mediaUrlGenerators; + _shortStringHelper = shortStringHelper; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationsSettings = packageMigrationsSettings; } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use ctor with all params")] + public PackageMigrationBase( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) + : this( + packagingService, + mediaService, + mediaFileManager, + mediaUrlGenerators, + shortStringHelper, + contentTypeBaseServiceProvider, + context, + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public IImportPackageBuilder ImportPackage => BeginBuild( + new ImportPackageBuilder( + _packagingService, + _mediaService, + _mediaFileManager, + _mediaUrlGenerators, + _shortStringHelper, + _contentTypeBaseServiceProvider, + Context, + _packageMigrationsSettings)); } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs index d25c65cfb8..bdbce82fcb 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlan.cs @@ -1,53 +1,53 @@ -using System; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Infrastructure.Migrations; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// Base class for package migration plans +/// +public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable { + /// + /// Creates a package migration plan + /// + /// The name of the package. If the package has a package.manifest these must match. + protected PackageMigrationPlan(string packageName) + : this(packageName, packageName) + { + } /// - /// Base class for package migration plans + /// Create a plan for a Package Name /// - public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable + /// + /// The package name that the plan is for. If the package has a package.manifest these must + /// match. + /// + /// + /// The plan name for the package. This should be the same name as the + /// package name if there is only one plan in the package. + /// + protected PackageMigrationPlan(string packageName, string planName) + : base(planName) { - /// - /// Creates a package migration plan - /// - /// The name of the package. If the package has a package.manifest these must match. - protected PackageMigrationPlan(string packageName) : this(packageName, packageName) - { - - } - - /// - /// Create a plan for a Package Name - /// - /// The package name that the plan is for. If the package has a package.manifest these must match. - /// - /// The plan name for the package. This should be the same name as the - /// package name if there is only one plan in the package. - /// - protected PackageMigrationPlan(string packageName, string planName) : base(planName) - { - // A call to From must be done first - From(string.Empty); - - DefinePlan(); - PackageName = packageName; - } - - /// - /// Inform the plan executor to ignore all saved package state and - /// run the migration from initial state to it's end state. - /// - public override bool IgnoreCurrentState => true; - - /// - /// Returns the Package Name for this plan - /// - public string PackageName { get; } - - protected abstract void DefinePlan(); + // A call to From must be done first + From(string.Empty); + DefinePlan(); + PackageName = packageName; } + + /// + /// Inform the plan executor to ignore all saved package state and + /// run the migration from initial state to it's end state. + /// + public override bool IgnoreCurrentState => true; + + /// + /// Returns the Package Name for this plan + /// + public string PackageName { get; } + + protected abstract void DefinePlan(); } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs index aa390dcaa4..565f53b57c 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollection.cs @@ -1,16 +1,14 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +/// +/// A collection of +/// +public class PackageMigrationPlanCollection : BuilderCollectionBase { - /// - /// A collection of - /// - public class PackageMigrationPlanCollection : BuilderCollectionBase + public PackageMigrationPlanCollection(Func> items) + : base(items) { - public PackageMigrationPlanCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs index bf496852c6..91b1364139 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationPlanCollectionBuilder.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase { - public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase - { - protected override PackageMigrationPlanCollectionBuilder This => this; - } + protected override PackageMigrationPlanCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs b/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs index efefcfcc7a..2931011f38 100644 --- a/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs +++ b/src/Umbraco.Infrastructure/Packaging/PendingPackageMigrations.cs @@ -1,65 +1,61 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.Packaging +namespace Umbraco.Cms.Core.Packaging; + +public class PendingPackageMigrations { - public class PendingPackageMigrations + private readonly ILogger _logger; + private readonly PackageMigrationPlanCollection _packageMigrationPlans; + + public PendingPackageMigrations( + ILogger logger, + PackageMigrationPlanCollection packageMigrationPlans) { - private readonly ILogger _logger; - private readonly PackageMigrationPlanCollection _packageMigrationPlans; + _logger = logger; + _packageMigrationPlans = packageMigrationPlans; + } - public PendingPackageMigrations( - ILogger logger, - PackageMigrationPlanCollection packageMigrationPlans) + /// + /// Returns what package migration names are pending + /// + /// + /// These are the key/value pairs from the keyvalue storage of migration names and their final values + /// + /// + public IReadOnlyList GetPendingPackageMigrations(IReadOnlyDictionary? keyValues) + { + var packageMigrationPlans = _packageMigrationPlans.ToList(); + + var pendingMigrations = new List(packageMigrationPlans.Count); + + foreach (PackageMigrationPlan plan in packageMigrationPlans) { - _logger = logger; - _packageMigrationPlans = packageMigrationPlans; - - } - - /// - /// Returns what package migration names are pending - /// - /// - /// These are the key/value pairs from the keyvalue storage of migration names and their final values - /// - /// - public IReadOnlyList GetPendingPackageMigrations(IReadOnlyDictionary? keyValues) - { - var packageMigrationPlans = _packageMigrationPlans.ToList(); - - var pendingMigrations = new List(packageMigrationPlans.Count); - - foreach (PackageMigrationPlan plan in packageMigrationPlans) + string? currentMigrationState = null; + var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; + if (keyValues?.TryGetValue(planKeyValueKey, out var value) ?? false) { - string? currentMigrationState = null; - var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; - if (keyValues?.TryGetValue(planKeyValueKey, out var value) ?? false) - { - currentMigrationState = value; + currentMigrationState = value; - if (!plan.FinalState.InvariantEquals(value)) - { - // Not equal so we need to run - pendingMigrations.Add(plan.Name); - } - } - else + if (!plan.FinalState.InvariantEquals(value)) { - // If there is nothing in the DB then we need to run + // Not equal so we need to run pendingMigrations.Add(plan.Name); } - - _logger.LogDebug("Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", - plan.Name, - plan.FinalState, - currentMigrationState ?? ""); + } + else + { + // If there is nothing in the DB then we need to run + pendingMigrations.Add(plan.Name); } - return pendingMigrations; + _logger.LogDebug( + "Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", + plan.Name, + plan.FinalState, + currentMigrationState ?? ""); } + + return pendingMigrations; } } diff --git a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs index 3797d4a433..f266df71ff 100644 --- a/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/CustomConnectionStringDatabaseProviderMetadata.cs @@ -1,11 +1,10 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Install.Models; namespace Umbraco.Cms.Infrastructure.Persistence; /// -/// Provider metadata for custom connection string setup. +/// Provider metadata for custom connection string setup. /// [DataContract] public class CustomConnectionStringDatabaseProviderMetadata : IDatabaseProviderMetadata diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs index 8b8386c93f..be89cb2ef6 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ConstraintAttribute.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a db constraint +/// +[AttributeUsage(AttributeTargets.Property)] +public class ConstraintAttribute : Attribute { /// - /// Attribute that represents a db constraint + /// Gets or sets the name of the constraint /// - [AttributeUsage(AttributeTargets.Property)] - public class ConstraintAttribute : Attribute - { - /// - /// Gets or sets the name of the constraint - /// - /// - /// Overrides the default naming of a property constraint: - /// DF_tableName_propertyName - /// - public string? Name { get; set; } + /// + /// Overrides the default naming of a property constraint: + /// DF_tableName_propertyName + /// + public string? Name { get; set; } - /// - /// Gets or sets the Default value - /// - public object? Default { get; set; } - } + /// + /// Gets or sets the Default value + /// + public object? Default { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs index a2f053415c..0eca49b8dd 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ForeignKeyAttribute.cs @@ -1,40 +1,40 @@ -using System; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Attribute that represents a Foreign Key reference +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] +public class ForeignKeyAttribute : ReferencesAttribute { - /// - /// Attribute that represents a Foreign Key reference - /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] - public class ForeignKeyAttribute : ReferencesAttribute + public ForeignKeyAttribute(Type type) + : base(type) { - public ForeignKeyAttribute(Type type) : base(type) - { } - - /// - /// Gets or sets the cascade rule for deletions. - /// - public Rule OnDelete { get; set; } = Rule.None; - - /// - /// Gets or sets the cascade rule for updates. - /// - public Rule OnUpdate { get; set; } = Rule.None; - - /// - /// Gets or sets the name of the foreign key reference - /// - /// - /// Overrides the default naming of a foreign key reference: - /// FK_thisTableName_refTableName - /// - public string? Name { get; set; } - - /// - /// Gets or sets the name of the Column that this foreign key should reference. - /// - /// PrimaryKey column is used by default - public string? Column { get; set; } } + + /// + /// Gets or sets the cascade rule for deletions. + /// + public Rule OnDelete { get; set; } = Rule.None; + + /// + /// Gets or sets the cascade rule for updates. + /// + public Rule OnUpdate { get; set; } = Rule.None; + + /// + /// Gets or sets the name of the foreign key reference + /// + /// + /// Overrides the default naming of a foreign key reference: + /// FK_thisTableName_refTableName + /// + public string? Name { get; set; } + + /// + /// Gets or sets the name of the Column that this foreign key should reference. + /// + /// PrimaryKey column is used by default + public string? Column { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs index 053e5b825d..826e56ad89 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexAttribute.cs @@ -1,40 +1,34 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents an Index +/// +[AttributeUsage(AttributeTargets.Property)] +public class IndexAttribute : Attribute { + public IndexAttribute(IndexTypes indexType) => IndexType = indexType; + /// - /// Attribute that represents an Index + /// Gets or sets the name of the Index /// - [AttributeUsage(AttributeTargets.Property)] - public class IndexAttribute : Attribute - { - public IndexAttribute(IndexTypes indexType) - { - IndexType = indexType; - } + /// + /// Overrides default naming of indexes: + /// IX_tableName + /// + public string? Name { get; set; } // Overrides default naming of indexes: IX_tableName - /// - /// Gets or sets the name of the Index - /// - /// - /// Overrides default naming of indexes: - /// IX_tableName - /// - public string? Name { get; set; }//Overrides default naming of indexes: IX_tableName + /// + /// Gets or sets the type of index to create + /// + public IndexTypes IndexType { get; } - /// - /// Gets or sets the type of index to create - /// - public IndexTypes IndexType { get; private set; } + /// + /// 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 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; } - } + /// + /// 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/DatabaseAnnotations/IndexTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs index 65516bb8c4..46697b9c97 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/IndexTypes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Enum for the 3 types of indexes that can be created +/// +public enum IndexTypes { - /// - /// Enum for the 3 types of indexes that can be created - /// - public enum IndexTypes - { - Clustered, - NonClustered, - UniqueNonClustered - } + Clustered, + NonClustered, + UniqueNonClustered, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs index 8e77b4bf96..a277e9b028 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/LengthAttribute.cs @@ -1,22 +1,16 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the length of a column +/// +/// Used to define the length of fixed sized columns - typically used for nvarchar +[AttributeUsage(AttributeTargets.Property)] +public class LengthAttribute : Attribute { - /// - /// Attribute that represents the length of a column - /// - /// Used to define the length of fixed sized columns - typically used for nvarchar - [AttributeUsage(AttributeTargets.Property)] - public class LengthAttribute : Attribute - { - public LengthAttribute(int length) - { - Length = length; - } + public LengthAttribute(int length) => Length = length; - /// - /// Gets or sets the length of a column - /// - public int Length { get; private set; } - } + /// + /// Gets or sets the length of a column + /// + public int Length { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs index 0db6433e94..f67447099d 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettingAttribute.cs @@ -1,20 +1,17 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the Null-setting of a column +/// +/// +/// This should only be used for Columns that can be Null. +/// By convention the Columns will be "NOT NULL". +/// +[AttributeUsage(AttributeTargets.Property)] +public class NullSettingAttribute : Attribute { /// - /// Attribute that represents the Null-setting of a column + /// Gets or sets the for a column /// - /// - /// This should only be used for Columns that can be Null. - /// By convention the Columns will be "NOT NULL". - /// - [AttributeUsage(AttributeTargets.Property)] - public class NullSettingAttribute : Attribute - { - /// - /// Gets or sets the for a column - /// - public NullSettings NullSetting { get; set; } - } + public NullSettings NullSetting { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs index 70c901c61e..d9140a0f14 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/NullSettings.cs @@ -1,11 +1,10 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Enum with the 2 possible Null settings: Null or Not Null +/// +public enum NullSettings { - /// - /// Enum with the 2 possible Null settings: Null or Not Null - /// - public enum NullSettings - { - Null, - NotNull - } + Null, + NotNull, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs index c4c5579028..d6cdf4ec7a 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/PrimaryKeyColumnAttribute.cs @@ -1,59 +1,56 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a Primary Key +/// +/// +/// By default, Clustered and AutoIncrement is set to true. +/// +[AttributeUsage(AttributeTargets.Property)] +public class PrimaryKeyColumnAttribute : Attribute { + public PrimaryKeyColumnAttribute() + { + Clustered = true; + AutoIncrement = true; + } + /// - /// Attribute that represents a Primary Key + /// Gets or sets a boolean indicating whether the primary key is clustered. + /// + /// Defaults to true + public bool Clustered { get; set; } + + /// + /// Gets or sets a boolean indicating whether the primary key is auto incremented. + /// + /// Defaults to true + public bool AutoIncrement { get; set; } + + /// + /// Gets or sets the name of the PrimaryKey. /// /// - /// By default, Clustered and AutoIncrement is set to true. + /// Overrides the default naming of a PrimaryKey constraint: + /// PK_tableName /// - [AttributeUsage(AttributeTargets.Property)] - public class PrimaryKeyColumnAttribute : Attribute - { - public PrimaryKeyColumnAttribute() - { - Clustered = true; - AutoIncrement = true; - } + public string? Name { get; set; } - /// - /// Gets or sets a boolean indicating whether the primary key is clustered. - /// - /// Defaults to true - public bool Clustered { get; set; } + /// + /// Gets or sets the names of the columns for this PrimaryKey. + /// + /// + /// Should only be used if the PrimaryKey spans over multiple columns. + /// Usage: [nodeId], [otherColumn] + /// + public string? OnColumns { get; set; } - /// - /// Gets or sets a boolean indicating whether the primary key is auto incremented. - /// - /// Defaults to true - public bool AutoIncrement { get; set; } - - /// - /// Gets or sets the name of the PrimaryKey. - /// - /// - /// Overrides the default naming of a PrimaryKey constraint: - /// PK_tableName - /// - public string? Name { get; set; } - - /// - /// Gets or sets the names of the columns for this PrimaryKey. - /// - /// - /// Should only be used if the PrimaryKey spans over multiple columns. - /// Usage: [nodeId], [otherColumn] - /// - public string? OnColumns { get; set; } - - /// - /// Gets or sets the Identity Seed, which is used for Sql Ce databases. - /// - /// - /// We'll only look for changes to seeding and apply them if the configured database - /// is an Sql Ce database. - /// - public int IdentitySeed { get; set; } - } + /// + /// Gets or sets the Identity Seed, which is used for Sql Ce databases. + /// + /// + /// We'll only look for changes to seeding and apply them if the configured database + /// is an Sql Ce database. + /// + public int IdentitySeed { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs index f008aa7e22..a324ddb358 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/ReferencesAttribute.cs @@ -1,21 +1,15 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents a reference between two tables/DTOs +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] +public class ReferencesAttribute : Attribute { - /// - /// Attribute that represents a reference between two tables/DTOs - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] - public class ReferencesAttribute : Attribute - { - public ReferencesAttribute(Type type) - { - Type = type; - } + public ReferencesAttribute(Type type) => Type = type; - /// - /// Gets or sets the Type of the referenced DTO/table - /// - public Type Type { get; set; } - } + /// + /// Gets or sets the Type of the referenced DTO/table + /// + public Type Type { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs index 41570d7b95..b5e57a3f3f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbType.cs @@ -1,43 +1,44 @@ -using System; -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Allows for specifying custom DB types that are not natively mapped. +/// +public struct SpecialDbType : IEquatable { - /// - /// Allows for specifying custom DB types that are not natively mapped. - /// - public struct SpecialDbType : IEquatable + private readonly string _dbType; + + public SpecialDbType(string dbType) { - private readonly string _dbType; - - public SpecialDbType(string dbType) + if (string.IsNullOrWhiteSpace(dbType)) { - if (string.IsNullOrWhiteSpace(dbType)) - { - throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); - } - - _dbType = dbType; + throw new ArgumentException($"'{nameof(dbType)}' cannot be null or whitespace.", nameof(dbType)); } - public SpecialDbType(SpecialDbTypes specialDbTypes) - => _dbType = specialDbTypes.ToString(); - - public static SpecialDbType NTEXT { get; } = new SpecialDbType(SpecialDbTypes.NTEXT); - public static SpecialDbType NCHAR { get; } = new SpecialDbType(SpecialDbTypes.NCHAR); - public static SpecialDbType NVARCHARMAX { get; } = new SpecialDbType(SpecialDbTypes.NVARCHARMAX); - - public override bool Equals(object? obj) => obj is SpecialDbType types && Equals(types); - public bool Equals(SpecialDbType other) => _dbType == other._dbType; - public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); - - public override string ToString() => _dbType.ToString(); - - // Make this directly castable to string - public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); - - // direct equality operators with SpecialDbTypes enum - public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; - public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; + _dbType = dbType; } + + public SpecialDbType(SpecialDbTypes specialDbTypes) + => _dbType = specialDbTypes.ToString(); + + public static SpecialDbType NTEXT { get; } = new(SpecialDbTypes.NTEXT); + + public static SpecialDbType NCHAR { get; } = new(SpecialDbTypes.NCHAR); + + public static SpecialDbType NVARCHARMAX { get; } = new(SpecialDbTypes.NVARCHARMAX); + + // Make this directly castable to string + public static implicit operator string(SpecialDbType dbType) => dbType.ToString(); + + public override bool Equals(object? obj) => obj is SpecialDbType types && Equals(types); + + public bool Equals(SpecialDbType other) => _dbType == other._dbType; + + public override int GetHashCode() => 1038481724 + EqualityComparer.Default.GetHashCode(_dbType); + + public override string ToString() => _dbType; + + // direct equality operators with SpecialDbTypes enum + public static bool operator ==(SpecialDbTypes x, SpecialDbType y) => x.ToString() == y; + + public static bool operator !=(SpecialDbTypes x, SpecialDbType y) => x.ToString() != y; } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs index d7fd2ff34f..cfdd4d80aa 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypeAttribute.cs @@ -1,25 +1,22 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +/// +/// Attribute that represents the usage of a special type +/// +/// +/// Should only be used when the .NET type can't be directly translated to a DbType. +/// +[AttributeUsage(AttributeTargets.Property)] +public class SpecialDbTypeAttribute : Attribute { + public SpecialDbTypeAttribute(SpecialDbTypes databaseType) + => DatabaseType = new SpecialDbType(databaseType); + + public SpecialDbTypeAttribute(string databaseType) + => DatabaseType = new SpecialDbType(databaseType); + /// - /// Attribute that represents the usage of a special type + /// Gets or sets the for this column /// - /// - /// Should only be used when the .NET type can't be directly translated to a DbType. - /// - [AttributeUsage(AttributeTargets.Property)] - public class SpecialDbTypeAttribute : Attribute - { - public SpecialDbTypeAttribute(SpecialDbTypes databaseType) - => DatabaseType = new SpecialDbType(databaseType); - - public SpecialDbTypeAttribute(string databaseType) - => DatabaseType = new SpecialDbType(databaseType); - - /// - /// Gets or sets the for this column - /// - public SpecialDbType DatabaseType { get; private set; } - } + public SpecialDbType DatabaseType { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs index d867d6f682..98b0558a2e 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseAnnotations/SpecialDbTypes.cs @@ -1,12 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +/// +/// Known special DB types required for Umbraco. +/// +public enum SpecialDbTypes { - /// - /// Known special DB types required for Umbraco. - /// - public enum SpecialDbTypes - { - NTEXT, - NCHAR, - NVARCHARMAX, - } + NTEXT, + NCHAR, + NVARCHARMAX, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs index f54691994e..e2efea4251 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseDebugHelper.cs @@ -14,7 +14,8 @@ namespace Umbraco.Cms.Core.Persistence internal static class DatabaseDebugHelper { private const int CommandsSize = 100; - private static readonly Queue>> Commands = new Queue>>(); + private static readonly Queue>> Commands = + new Queue>>(); public static void SetCommand(IDbCommand command, string context) { @@ -122,7 +123,8 @@ namespace Umbraco.Cms.Core.Persistence //var rdr = objTarget as DbDataReader; try { - var commandProp = objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic); + var commandProp = + objTarget.GetType().GetProperty("Command", BindingFlags.Instance | BindingFlags.NonPublic); if (commandProp == null) throw new Exception($"panic: failed to get Command property of {objTarget.GetType().FullName}."); cmd = commandProp.GetValue(objTarget, null) as DbCommand; diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs index a80bbbe3f6..99605476b3 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ColumnDefinition.cs @@ -1,38 +1,53 @@ -using System; using System.Data; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class ColumnDefinition { - public class ColumnDefinition - { - public virtual string Name { get; set; } = null!; - //This type is typically used as part of a migration - public virtual DbType? Type { get; set; } - //When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. - //This type is typically used as part of an initial table creation - public Type PropertyType { get; set; } = null!; + public virtual string Name { get; set; } = null!; - /// - /// Used for column types that cannot be natively mapped. - /// - public SpecialDbType? CustomDbType { get; set; } + // This type is typically used as part of a migration + public virtual DbType? Type { get; set; } - public virtual int Seeding { get; set; } - public virtual int Size { get; set; } - public virtual int Precision { get; set; } - public virtual string? CustomType { get; set; } - public virtual object? DefaultValue { get; set; } - public virtual string? ConstraintName { get; set; } - public virtual bool IsForeignKey { get; set; } - public virtual bool IsIdentity { get; set; } - public virtual bool IsIndexed { get; set; }//Clustered? - public virtual bool IsPrimaryKey { get; set; } - public virtual string? PrimaryKeyName { get; set; } - public virtual string? PrimaryKeyColumns { get; set; }//When the primary key spans multiple columns - public virtual bool IsNullable { get; set; } - public virtual bool IsUnique { get; set; } - public virtual string? TableName { get; set; } - public virtual ModificationType ModificationType { get; set; } - } + // When DbType isn't set explicitly the Type will be used to find the right DbType in the SqlSyntaxProvider. + // This type is typically used as part of an initial table creation + public Type PropertyType { get; set; } = null!; + + /// + /// Used for column types that cannot be natively mapped. + /// + public SpecialDbType? CustomDbType { get; set; } + + public virtual int Seeding { get; set; } + + public virtual int Size { get; set; } + + public virtual int Precision { get; set; } + + public virtual string? CustomType { get; set; } + + public virtual object? DefaultValue { get; set; } + + public virtual string? ConstraintName { get; set; } + + public virtual bool IsForeignKey { get; set; } + + public virtual bool IsIdentity { get; set; } + + public virtual bool IsIndexed { get; set; } // Clustered? + + public virtual bool IsPrimaryKey { get; set; } + + public virtual string? PrimaryKeyName { get; set; } + + public virtual string? PrimaryKeyColumns { get; set; } // When the primary key spans multiple columns + + public virtual bool IsNullable { get; set; } + + public virtual bool IsUnique { get; set; } + + public virtual string? TableName { get; set; } + + public virtual ModificationType ModificationType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs index 87066ce206..da67fd22c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintDefinition.cs @@ -1,23 +1,23 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class ConstraintDefinition { - public class ConstraintDefinition - { - public ConstraintDefinition(ConstraintType type) - { - _constraintType = type; - } + public ICollection Columns = new HashSet(); + private readonly ConstraintType _constraintType; - private readonly ConstraintType _constraintType; - public bool IsPrimaryKeyConstraint => ConstraintType.PrimaryKey == _constraintType; - public bool IsUniqueConstraint => ConstraintType.Unique == _constraintType; - public bool IsNonUniqueConstraint => ConstraintType.NonUnique == _constraintType; + public ConstraintDefinition(ConstraintType type) => _constraintType = type; - public string? SchemaName { get; set; } - public string? ConstraintName { get; set; } - public string? TableName { get; set; } - public ICollection Columns = new HashSet(); - public bool IsPrimaryKeyClustered { get; set; } - } + public bool IsPrimaryKeyConstraint => _constraintType == ConstraintType.PrimaryKey; + + public bool IsUniqueConstraint => _constraintType == ConstraintType.Unique; + + public bool IsNonUniqueConstraint => _constraintType == ConstraintType.NonUnique; + + public string? SchemaName { get; set; } + + public string? ConstraintName { get; set; } + + public string? TableName { get; set; } + + public bool IsPrimaryKeyClustered { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs index 4592f1f14f..1d7fbc6408 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ConstraintType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum ConstraintType { - public enum ConstraintType - { - PrimaryKey, - Unique, - NonUnique - } + PrimaryKey, + Unique, + NonUnique, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs index df73074a35..4c2cf0a69f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DbIndexDefinition.cs @@ -1,23 +1,23 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +/// +/// Represents a database index definition retrieved by querying the database +/// +internal class DbIndexDefinition { - /// - /// Represents a database index definition retrieved by querying the database - /// - internal class DbIndexDefinition + public DbIndexDefinition(Tuple data) { - public DbIndexDefinition(Tuple data) - { - TableName = data.Item1; - IndexName = data.Item2; - ColumnName = data.Item3; - IsUnique = data.Item4; - } - - public string IndexName { get; } - public string TableName { get; } - public string ColumnName { get; } - public bool IsUnique { get; } + TableName = data.Item1; + IndexName = data.Item2; + ColumnName = data.Item3; + IsUnique = data.Item4; } + + public string IndexName { get; } + + public string TableName { get; } + + public string ColumnName { get; } + + public bool IsUnique { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 9e26c2722a..32bdd213e6 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using System.Reflection; using NPoco; using Umbraco.Cms.Core; @@ -7,174 +5,189 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public static class DefinitionFactory { - public static class DefinitionFactory + public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) { - public static TableDefinition GetTableDefinition(Type modelType, ISqlSyntaxProvider sqlSyntax) + // Looks for NPoco's TableNameAtribute for the name of the table + // If no attribute is set we use the name of the Type as the default convention + TableNameAttribute? tableNameAttribute = modelType.FirstAttribute(); + var tableName = tableNameAttribute == null ? modelType.Name : tableNameAttribute.Value; + + var tableDefinition = new TableDefinition { Name = tableName }; + var objProperties = modelType.GetProperties().ToList(); + foreach (PropertyInfo propertyInfo in objProperties) { - //Looks for NPoco's TableNameAtribute for the name of the table - //If no attribute is set we use the name of the Type as the default convention - var tableNameAttribute = modelType.FirstAttribute(); - string tableName = tableNameAttribute == null ? modelType.Name : tableNameAttribute.Value; - - var tableDefinition = new TableDefinition {Name = tableName}; - var objProperties = modelType.GetProperties().ToList(); - foreach (var propertyInfo in objProperties) + // If current property has an IgnoreAttribute then skip it + IgnoreAttribute? ignoreAttribute = propertyInfo.FirstAttribute(); + if (ignoreAttribute != null) { - //If current property has an IgnoreAttribute then skip it - var ignoreAttribute = propertyInfo.FirstAttribute(); - if (ignoreAttribute != null) continue; + continue; + } - //If current property has a ResultColumnAttribute then skip it - var resultColumnAttribute = propertyInfo.FirstAttribute(); - if (resultColumnAttribute != null) continue; + // If current property has a ResultColumnAttribute then skip it + ResultColumnAttribute? resultColumnAttribute = propertyInfo.FirstAttribute(); + if (resultColumnAttribute != null) + { + continue; + } - //Looks for ColumnAttribute with the name of the column, which would exist with ExplicitColumns - //Otherwise use the name of the property itself as the default convention - var columnAttribute = propertyInfo.FirstAttribute(); - string columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; - var columnDefinition = GetColumnDefinition(modelType, propertyInfo, columnName, tableName, sqlSyntax); - tableDefinition.Columns.Add(columnDefinition); + // Looks for ColumnAttribute with the name of the column, which would exist with ExplicitColumns + // Otherwise use the name of the property itself as the default convention + ColumnAttribute? columnAttribute = propertyInfo.FirstAttribute(); + var columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; + ColumnDefinition columnDefinition = + GetColumnDefinition(modelType, propertyInfo, columnName, tableName, sqlSyntax); + tableDefinition.Columns.Add(columnDefinition); - //Creates a foreignkey definition and adds it to the collection on the table definition - var foreignKeyAttributes = propertyInfo.MultipleAttribute(); - if (foreignKeyAttributes != null) + // Creates a foreignkey definition and adds it to the collection on the table definition + IEnumerable? foreignKeyAttributes = + propertyInfo.MultipleAttribute(); + if (foreignKeyAttributes != null) + { + foreach (ForeignKeyAttribute foreignKeyAttribute in foreignKeyAttributes) { - foreach (var foreignKeyAttribute in foreignKeyAttributes) - { - var foreignKeyDefinition = GetForeignKeyDefinition(modelType, propertyInfo, foreignKeyAttribute, columnName, tableName); - tableDefinition.ForeignKeys.Add(foreignKeyDefinition); - } - } - - //Creates an index definition and adds it to the collection on the table definition - var indexAttribute = propertyInfo.FirstAttribute(); - if (indexAttribute != null) - { - var indexDefinition = GetIndexDefinition(modelType, propertyInfo, indexAttribute, columnName, tableName); - tableDefinition.Indexes.Add(indexDefinition); - } - } - - return tableDefinition; - } - - public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, string tableName, ISqlSyntaxProvider sqlSyntax) - { - var definition = new ColumnDefinition{ Name = columnName, TableName = tableName, ModificationType = ModificationType.Create }; - - //Look for specific Null setting attributed a column - var nullSettingAttribute = propertyInfo.FirstAttribute(); - if (nullSettingAttribute != null) - { - definition.IsNullable = nullSettingAttribute.NullSetting == NullSettings.Null; - } - - //Look for specific DbType attributed a column - var databaseTypeAttribute = propertyInfo.FirstAttribute(); - if (databaseTypeAttribute != null) - { - definition.CustomDbType = databaseTypeAttribute.DatabaseType; - } - else - { - definition.PropertyType = propertyInfo.PropertyType; - } - - //Look for Primary Key for the current column - var primaryKeyColumnAttribute = propertyInfo.FirstAttribute(); - if (primaryKeyColumnAttribute != null) - { - string primaryKeyName = string.IsNullOrEmpty(primaryKeyColumnAttribute.Name) - ? string.Format("PK_{0}", tableName) - : primaryKeyColumnAttribute.Name; - - definition.IsPrimaryKey = true; - definition.IsIdentity = primaryKeyColumnAttribute.AutoIncrement; - definition.IsIndexed = primaryKeyColumnAttribute.Clustered; - definition.PrimaryKeyName = primaryKeyName; - definition.PrimaryKeyColumns = primaryKeyColumnAttribute.OnColumns ?? string.Empty; - definition.Seeding = primaryKeyColumnAttribute.IdentitySeed; - } - - //Look for Size/Length of DbType - var lengthAttribute = propertyInfo.FirstAttribute(); - if (lengthAttribute != null) - { - definition.Size = lengthAttribute.Length; - } - - //Look for Constraint for the current column - var constraintAttribute = propertyInfo.FirstAttribute(); - if (constraintAttribute != null) - { - definition.ConstraintName = constraintAttribute.Name ?? string.Empty; - definition.DefaultValue = constraintAttribute.Default ?? string.Empty; - } - - return definition; - } - - public static ForeignKeyDefinition GetForeignKeyDefinition(Type modelType, PropertyInfo propertyInfo, - ForeignKeyAttribute attribute, string columnName, string tableName) - { - var referencedTable = attribute.Type.FirstAttribute(); - var referencedPrimaryKey = attribute.Type.FirstAttribute(); - - string referencedColumn = string.IsNullOrEmpty(attribute.Column) - ? referencedPrimaryKey!.Value - : attribute.Column; - - string foreignKeyName = string.IsNullOrEmpty(attribute.Name) - ? string.Format("FK_{0}_{1}_{2}", tableName, referencedTable!.Value, referencedColumn) - : attribute.Name; - - var definition = new ForeignKeyDefinition - { - Name = foreignKeyName, - ForeignTable = tableName, - PrimaryTable = referencedTable!.Value, - OnDelete = attribute.OnDelete, - OnUpdate = attribute.OnUpdate - }; - definition.ForeignColumns.Add(columnName); - definition.PrimaryColumns.Add(referencedColumn); - - return definition; - } - - public static IndexDefinition GetIndexDefinition(Type modelType, PropertyInfo propertyInfo, IndexAttribute attribute, string columnName, string tableName) - { - string indexName = string.IsNullOrEmpty(attribute.Name) - ? string.Format("IX_{0}_{1}", tableName, columnName) - : attribute.Name; - - var definition = new IndexDefinition - { - Name = indexName, - IndexType = attribute.IndexType, - ColumnName = columnName, - TableName = tableName, - }; - - if (string.IsNullOrEmpty(attribute.ForColumns) == false) - { - var columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim()); - foreach (var column in columns) - { - definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending}); + ForeignKeyDefinition foreignKeyDefinition = GetForeignKeyDefinition(modelType, propertyInfo, foreignKeyAttribute, columnName, tableName); + tableDefinition.ForeignKeys.Add(foreignKeyDefinition); } } - if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + + // Creates an index definition and adds it to the collection on the table definition + IndexAttribute? indexAttribute = propertyInfo.FirstAttribute(); + if (indexAttribute != null) { - var columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim()); - foreach (var column in columns) - { - definition.IncludeColumns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); - } + IndexDefinition indexDefinition = + GetIndexDefinition(modelType, propertyInfo, indexAttribute, columnName, tableName); + tableDefinition.Indexes.Add(indexDefinition); } - return definition; } + + return tableDefinition; + } + + public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, string tableName, ISqlSyntaxProvider sqlSyntax) + { + var definition = new ColumnDefinition + { + Name = columnName, + TableName = tableName, + ModificationType = ModificationType.Create, + }; + + // Look for specific Null setting attributed a column + NullSettingAttribute? nullSettingAttribute = propertyInfo.FirstAttribute(); + if (nullSettingAttribute != null) + { + definition.IsNullable = nullSettingAttribute.NullSetting == NullSettings.Null; + } + + // Look for specific DbType attributed a column + SpecialDbTypeAttribute? databaseTypeAttribute = propertyInfo.FirstAttribute(); + if (databaseTypeAttribute != null) + { + definition.CustomDbType = databaseTypeAttribute.DatabaseType; + } + else + { + definition.PropertyType = propertyInfo.PropertyType; + } + + // Look for Primary Key for the current column + PrimaryKeyColumnAttribute? primaryKeyColumnAttribute = propertyInfo.FirstAttribute(); + if (primaryKeyColumnAttribute != null) + { + var primaryKeyName = string.IsNullOrEmpty(primaryKeyColumnAttribute.Name) + ? string.Format("PK_{0}", tableName) + : primaryKeyColumnAttribute.Name; + + definition.IsPrimaryKey = true; + definition.IsIdentity = primaryKeyColumnAttribute.AutoIncrement; + definition.IsIndexed = primaryKeyColumnAttribute.Clustered; + definition.PrimaryKeyName = primaryKeyName; + definition.PrimaryKeyColumns = primaryKeyColumnAttribute.OnColumns ?? string.Empty; + definition.Seeding = primaryKeyColumnAttribute.IdentitySeed; + } + + // Look for Size/Length of DbType + LengthAttribute? lengthAttribute = propertyInfo.FirstAttribute(); + if (lengthAttribute != null) + { + definition.Size = lengthAttribute.Length; + } + + // Look for Constraint for the current column + ConstraintAttribute? constraintAttribute = propertyInfo.FirstAttribute(); + if (constraintAttribute != null) + { + definition.ConstraintName = constraintAttribute.Name ?? string.Empty; + definition.DefaultValue = constraintAttribute.Default ?? string.Empty; + } + + return definition; + } + + public static ForeignKeyDefinition GetForeignKeyDefinition(Type modelType, PropertyInfo propertyInfo, ForeignKeyAttribute attribute, string columnName, string tableName) + { + TableNameAttribute? referencedTable = attribute.Type.FirstAttribute(); + PrimaryKeyAttribute? referencedPrimaryKey = attribute.Type.FirstAttribute(); + + var referencedColumn = string.IsNullOrEmpty(attribute.Column) + ? referencedPrimaryKey!.Value + : attribute.Column; + + var foreignKeyName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("FK_{0}_{1}_{2}", tableName, referencedTable!.Value, referencedColumn) + : attribute.Name; + + var definition = new ForeignKeyDefinition + { + Name = foreignKeyName, + ForeignTable = tableName, + PrimaryTable = referencedTable!.Value, + OnDelete = attribute.OnDelete, + OnUpdate = attribute.OnUpdate, + }; + definition.ForeignColumns.Add(columnName); + definition.PrimaryColumns.Add(referencedColumn); + + return definition; + } + + public static IndexDefinition GetIndexDefinition(Type modelType, PropertyInfo propertyInfo, IndexAttribute attribute, string columnName, string tableName) + { + var indexName = string.IsNullOrEmpty(attribute.Name) + ? string.Format("IX_{0}_{1}", tableName, columnName) + : attribute.Name; + + var definition = new IndexDefinition + { + Name = indexName, + IndexType = attribute.IndexType, + ColumnName = columnName, + TableName = tableName, + }; + + if (string.IsNullOrEmpty(attribute.ForColumns) == false) + { + IEnumerable columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim()); + foreach (var column in columns) + { + definition.Columns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending }); + } + } + + if (string.IsNullOrEmpty(attribute.IncludeColumns) == false) + { + IEnumerable 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/DeletionDataDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs index c6033f898d..796e52aebf 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/DeletionDataDefinition.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class DeletionDataDefinition : List> { - public class DeletionDataDefinition : List> - { - - } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs index e6752159de..b1bc9c2117 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ForeignKeyDefinition.cs @@ -1,27 +1,34 @@ -using System.Collections.Generic; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions -{ - public class ForeignKeyDefinition - { - public ForeignKeyDefinition() - { - ForeignColumns = new List(); - PrimaryColumns = new List(); - //Set to None by Default - OnDelete = Rule.None; - OnUpdate = Rule.None; - } +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; - public virtual string? Name { get; set; } - public virtual string? ForeignTable { get; set; } - public virtual string? ForeignTableSchema { get; set; } - public virtual string? PrimaryTable { get; set; } - public virtual string? PrimaryTableSchema { get; set; } - public virtual Rule OnDelete { get; set; } - public virtual Rule OnUpdate { get; set; } - public virtual ICollection ForeignColumns { get; set; } - public virtual ICollection PrimaryColumns { get; set; } +public class ForeignKeyDefinition +{ + public ForeignKeyDefinition() + { + ForeignColumns = new List(); + PrimaryColumns = new List(); + + // Set to None by Default + OnDelete = Rule.None; + OnUpdate = Rule.None; } + + public virtual string? Name { get; set; } + + public virtual string? ForeignTable { get; set; } + + public virtual string? ForeignTableSchema { get; set; } + + public virtual string? PrimaryTable { get; set; } + + public virtual string? PrimaryTableSchema { get; set; } + + public virtual Rule OnDelete { get; set; } + + public virtual Rule OnUpdate { get; set; } + + public virtual ICollection ForeignColumns { get; set; } + + public virtual ICollection PrimaryColumns { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs index 6f3e34e0e4..7805d2ba93 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexColumnDefinition.cs @@ -1,10 +1,10 @@ -using Umbraco.Cms.Core; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public class IndexColumnDefinition { - public class IndexColumnDefinition - { - public virtual string? Name { get; set; } - public virtual Direction Direction { get; set; } - } + public virtual string? Name { get; set; } + + public virtual Direction Direction { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs index 8761ae2a29..c4deba8b84 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/IndexDefinition.cs @@ -1,17 +1,20 @@ -using System.Collections.Generic; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions -{ - public class IndexDefinition - { - public virtual string? Name { get; set; } - public virtual string? SchemaName { get; set; } - public virtual string? TableName { get; set; } - public virtual string? ColumnName { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; - public virtual ICollection Columns { get; set; } = new List(); - public virtual ICollection IncludeColumns { get; set; } = new List(); - public IndexTypes IndexType { get; set; } - } +public class IndexDefinition +{ + 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; } = new List(); + + public virtual ICollection IncludeColumns { get; set; } = new List(); + + public IndexTypes IndexType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs index a09bcaafdf..d3b5341c87 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/InsertionDataDefinition.cs @@ -1,9 +1,5 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class InsertionDataDefinition : List> { - public class InsertionDataDefinition : List> - { - - } } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs index 490b06e41d..1928e8c994 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/ModificationType.cs @@ -1,13 +1,12 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum ModificationType { - public enum ModificationType - { - Create, - Alter, - Drop, - Rename, - Insert, - Update, - Delete - } + Create, + Alter, + Drop, + Rename, + Insert, + Update, + Delete, } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs index 24daa49f35..6836b96582 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/SystemMethods.cs @@ -1,10 +1,11 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +public enum SystemMethods { - public enum SystemMethods - { - NewGuid, - CurrentDateTime, - //NewSequentialId, - //CurrentUTCDateTime - } + NewGuid, + + CurrentDateTime, + + // NewSequentialId, + // CurrentUTCDateTime } diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs index 4ab479b8fd..32c1cbe364 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseModelDefinitions/TableDefinition.cs @@ -1,20 +1,21 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions +public class TableDefinition { - public class TableDefinition + public TableDefinition() { - public TableDefinition() - { - Columns = new List(); - ForeignKeys = new List(); - Indexes = new List(); - } - - public virtual string Name { get; set; } = null!; - public virtual string SchemaName { get; set; } = null!; - public virtual ICollection Columns { get; set; } - public virtual ICollection ForeignKeys { get; set; } - public virtual ICollection Indexes { get; set; } + Columns = new List(); + ForeignKeys = new List(); + Indexes = new List(); } + + public virtual string Name { get; set; } = null!; + + public virtual string SchemaName { get; set; } = null!; + + public virtual ICollection Columns { get; set; } + + public virtual ICollection ForeignKeys { get; set; } + + public virtual ICollection Indexes { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs index f70da7c8fb..9201d79c2f 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbCommandExtensions.cs @@ -1,29 +1,33 @@ -using System.Data; +using System.Data; +using StackExchange.Profiling.Data; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class DbCommandExtensions { - internal static class DbCommandExtensions + /// + /// Unwraps a database command. + /// + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database command. + /// + public static IDbCommand UnwrapUmbraco(this IDbCommand command) { - /// - /// Unwraps a database command. - /// - /// UmbracoDatabase wraps the original database connection in various layers (see - /// OnConnectionOpened); this unwraps and returns the original database command. - public static IDbCommand UnwrapUmbraco(this IDbCommand command) + IDbCommand unwrapped; + + IDbCommand c = command; + do { - IDbCommand unwrapped; + unwrapped = c; - var c = command; - do + if (unwrapped is ProfiledDbCommand profiled) { - unwrapped = c; - - var profiled = unwrapped as StackExchange.Profiling.Data.ProfiledDbCommand; - if (profiled != null) unwrapped = profiled.InternalCommand; - - } while (c != unwrapped); - - return unwrapped; + unwrapped = profiled.InternalCommand; + } } + while (c != unwrapped); + + return unwrapped; } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs index 61601fef95..e4bec34987 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbConnectionExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Data; using System.Data.Common; using Microsoft.Extensions.Logging; @@ -6,68 +5,71 @@ using StackExchange.Profiling.Data; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class DbConnectionExtensions { - public static class DbConnectionExtensions + public static bool IsConnectionAvailable(string? connectionString, DbProviderFactory? factory) { - public static bool IsConnectionAvailable(string? connectionString, DbProviderFactory? factory) + DbConnection? connection = factory?.CreateConnection(); + + if (connection == null) { - var connection = factory?.CreateConnection(); - - if (connection == null) - throw new InvalidOperationException($"Could not create a connection for provider \"{factory}\"."); - - connection.ConnectionString = connectionString; - using (connection) - { - return connection.IsAvailable(); - } + throw new InvalidOperationException($"Could not create a connection for provider \"{factory}\"."); } - public static bool IsAvailable(this IDbConnection connection) + connection.ConnectionString = connectionString; + using (connection) { - try - { - connection.Open(); - connection.Close(); - } - catch (DbException e) - { - // Don't swallow this error, the exception is super handy for knowing "why" its not available - StaticApplicationLogging.Logger.LogWarning(e, "Configured database is reporting as not being available."); - return false; - } - - return true; - } - - /// - /// Unwraps a database connection. - /// - /// UmbracoDatabase wraps the original database connection in various layers (see - /// OnConnectionOpened); this unwraps and returns the original database connection. - internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) - { - var unwrapped = connection; - - IDbConnection c; - do - { - c = unwrapped; - - if (unwrapped is ProfiledDbConnection profiled) - { - unwrapped = profiled.WrappedConnection; - } - - if (unwrapped is RetryDbConnection retrying) - { - unwrapped = retrying.Inner; - } - } - while (c != unwrapped); - - return unwrapped; + return connection.IsAvailable(); } } + + public static bool IsAvailable(this IDbConnection connection) + { + try + { + connection.Open(); + connection.Close(); + } + catch (DbException e) + { + // Don't swallow this error, the exception is super handy for knowing "why" its not available + StaticApplicationLogging.Logger.LogWarning(e, "Configured database is reporting as not being available."); + return false; + } + + return true; + } + + /// + /// Unwraps a database connection. + /// + /// + /// UmbracoDatabase wraps the original database connection in various layers (see + /// OnConnectionOpened); this unwraps and returns the original database connection. + /// + internal static IDbConnection UnwrapUmbraco(this IDbConnection connection) + { + IDbConnection? unwrapped = connection; + + IDbConnection c; + do + { + c = unwrapped; + + if (unwrapped is ProfiledDbConnection profiled) + { + unwrapped = profiled.WrappedConnection; + } + + if (unwrapped is RetryDbConnection retrying) + { + unwrapped = retrying.Inner; + } + } + while (c != unwrapped); + + return unwrapped; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 0177475609..05b8a41f1e 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; using System.Data.Common; -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence -{ - public class DbProviderFactoryCreator : IDbProviderFactoryCreator - { - private readonly Func _getFactory; - private readonly IEnumerable _providerSpecificInterceptors; - private readonly IDictionary _databaseCreators; - private readonly IDictionary _syntaxProviders; - private readonly IDictionary _bulkSqlInsertProviders; - private readonly IDictionary _providerSpecificMapperFactories; +namespace Umbraco.Cms.Infrastructure.Persistence; - [Obsolete("Please use an alternative constructor.")] - public DbProviderFactoryCreator( - Func getFactory, - IEnumerable syntaxProviders, - IEnumerable bulkSqlInsertProviders, - IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories) +public class DbProviderFactoryCreator : IDbProviderFactoryCreator +{ + private readonly IDictionary _bulkSqlInsertProviders; + private readonly IDictionary _databaseCreators; + private readonly Func _getFactory; + private readonly IEnumerable _providerSpecificInterceptors; + private readonly IDictionary _providerSpecificMapperFactories; + private readonly IDictionary _syntaxProviders; + + [Obsolete("Please use an alternative constructor.")] + public DbProviderFactoryCreator( + Func getFactory, + IEnumerable syntaxProviders, + IEnumerable bulkSqlInsertProviders, + IEnumerable databaseCreators, + IEnumerable providerSpecificMapperFactories) : this( getFactory, syntaxProviders, @@ -30,74 +27,76 @@ namespace Umbraco.Cms.Infrastructure.Persistence databaseCreators, providerSpecificMapperFactories, Enumerable.Empty()) - { - } - - public DbProviderFactoryCreator( - Func getFactory, - IEnumerable syntaxProviders, - IEnumerable bulkSqlInsertProviders, - IEnumerable databaseCreators, - IEnumerable providerSpecificMapperFactories, - IEnumerable providerSpecificInterceptors) - - { - _getFactory = getFactory; - _providerSpecificInterceptors = providerSpecificInterceptors; - _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); - _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); - _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); - _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); - } - - public DbProviderFactory? CreateFactory(string? providerName) - { - if (string.IsNullOrEmpty(providerName)) - return null; - return _getFactory(providerName); - } - - // gets the sql syntax provider that corresponds, from attribute - public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) - { - - if (!_syntaxProviders.TryGetValue(providerName, out var result)) - { - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - } - - return result; - } - - public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) - { - if (!_bulkSqlInsertProviders.TryGetValue(providerName, out var result)) - { - throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); - } - - return result; - } - - public void CreateDatabase(string providerName, string connectionString) - { - if (_databaseCreators.TryGetValue(providerName, out var creator)) - { - creator.Create(connectionString); - } - } - - public NPocoMapperCollection ProviderSpecificMappers(string providerName) - { - if (_providerSpecificMapperFactories.TryGetValue(providerName, out var mapperFactory)) - { - return mapperFactory.Mappers; - } - - return new NPocoMapperCollection(() => Enumerable.Empty()); - } - - public IEnumerable GetProviderSpecificInterceptors(string providerName) - => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); + { } + + public DbProviderFactoryCreator( + Func getFactory, + IEnumerable syntaxProviders, + IEnumerable bulkSqlInsertProviders, + IEnumerable databaseCreators, + IEnumerable providerSpecificMapperFactories, + IEnumerable providerSpecificInterceptors) + { + _getFactory = getFactory; + _providerSpecificInterceptors = providerSpecificInterceptors; + _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); + _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); + _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); + _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); + } + + public DbProviderFactory? CreateFactory(string? providerName) + { + if (string.IsNullOrEmpty(providerName)) + { + return null; + } + + return _getFactory(providerName); + } + + // gets the sql syntax provider that corresponds, from attribute + public ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName) + { + if (!_syntaxProviders.TryGetValue(providerName, out ISqlSyntaxProvider? result)) + { + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); + } + + return result; + } + + public IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName) + { + if (!_bulkSqlInsertProviders.TryGetValue(providerName, out IBulkSqlInsertProvider? result)) + { + throw new InvalidOperationException($"Unknown provider name \"{providerName}\""); + } + + return result; + } + + public void CreateDatabase(string providerName, string connectionString) + { + if (_databaseCreators.TryGetValue(providerName, out IDatabaseCreator? creator)) + { + creator.Create(connectionString); + } + } + + public NPocoMapperCollection ProviderSpecificMappers(string providerName) + { + if (_providerSpecificMapperFactories.TryGetValue( + providerName, + out IProviderSpecificMapperFactory? mapperFactory)) + { + return mapperFactory.Mappers; + } + + return new NPocoMapperCollection(() => Enumerable.Empty()); + } + + public IEnumerable GetProviderSpecificInterceptors(string providerName) + => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs index e8842b7cfd..354083dfa8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs @@ -1,43 +1,41 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Access)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class AccessDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Access)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class AccessDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoAccess", AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoAccess", AutoIncrement = false)] + public Guid Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoAccess_nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoAccess_nodeId")] + public int NodeId { get; set; } - [Column("loginNodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id1")] - public int LoginNodeId { get; set; } + [Column("loginNodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id1")] + public int LoginNodeId { get; set; } - [Column("noAccessNodeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")] - public int NoAccessNodeId { get; set; } + [Column("noAccessNodeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")] + public int NoAccessNodeId { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "AccessId")] - public List Rules { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "AccessId")] + public List Rules { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs index 307f91337b..3aba928bda 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs @@ -1,36 +1,35 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.AccessRule)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class AccessRuleDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.AccessRule)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class AccessRuleDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoAccessRule", AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoAccessRule", AutoIncrement = false)] + public Guid Id { get; set; } - [Column("accessId")] - [ForeignKey(typeof(AccessDto), Name = "FK_umbracoAccessRule_umbracoAccess_id")] - public Guid AccessId { get; set; } + [Column("accessId")] + [ForeignKey(typeof(AccessDto), Name = "FK_umbracoAccessRule_umbracoAccess_id")] + public Guid AccessId { get; set; } - [Column("ruleValue")] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "ruleValue,ruleType,accessId", Name = "IX_umbracoAccessRule")] - public string? RuleValue { get; set; } + [Column("ruleValue")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "ruleValue,ruleType,accessId", Name = "IX_umbracoAccessRule")] + public string? RuleValue { get; set; } - [Column("ruleType")] - public string? RuleType { get; set; } + [Column("ruleType")] + public string? RuleType { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs index 18ffda302e..38e63ffc20 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs @@ -1,57 +1,53 @@ -using System; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.AuditEntry)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class AuditEntryDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.AuditEntry)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class AuditEntryDto - { + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + // there is NO foreign key to the users table here, neither for performing user nor for + // affected user, so we can delete users and NOT delete the associated audit trails, and + // users can still be identified via the details free-form text fields. + [Column("performingUserId")] + public int PerformingUserId { get; set; } - // there is NO foreign key to the users table here, neither for performing user nor for - // affected user, so we can delete users and NOT delete the associated audit trails, and - // users can still be identified via the details free-form text fields. + [Column("performingDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? PerformingDetails { get; set; } - [Column("performingUserId")] - public int PerformingUserId { get; set; } + [Column("performingIp")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.IpLength)] + public string? PerformingIp { get; set; } - [Column("performingDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? PerformingDetails { get; set; } + [Column("eventDateUtc")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime EventDateUtc { get; set; } - [Column("performingIp")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.IpLength)] - public string? PerformingIp { get; set; } + [Column("affectedUserId")] + public int AffectedUserId { get; set; } - [Column("eventDateUtc")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime EventDateUtc { get; set; } + [Column("affectedDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? AffectedDetails { get; set; } - [Column("affectedUserId")] - public int AffectedUserId { get; set; } + [Column("eventType")] + [Length(Constants.Audit.EventTypeLength)] + public string? EventType { get; set; } - [Column("affectedDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? AffectedDetails { get; set; } - - [Column("eventType")] - [Length(Constants.Audit.EventTypeLength)] - public string? EventType { get; set; } - - [Column("eventDetails")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(Constants.Audit.DetailsLength)] - public string? EventDetails { get; set; } - } + [Column("eventDetails")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(Constants.Audit.DetailsLength)] + public string? EventDetails { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs index 431502a896..2c1c68c1f0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs @@ -1,16 +1,15 @@ using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +internal class AxisDefintionDto { - internal class AxisDefintionDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + public int NodeId { get; set; } - [Column("alias")] - public string? Alias { get; set; } + [Column("alias")] + public string? Alias { get; set; } - [Column("ParentID")] - public int ParentId { get; set; } - } + [Column("ParentID")] + public int ParentId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs index 7d57fca606..0e73112f76 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CacheInstructionDto.cs @@ -1,36 +1,35 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.CacheInstruction)] +[PrimaryKey("id")] +[ExplicitColumns] +public class CacheInstructionDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.CacheInstruction)] - [PrimaryKey("id")] - [ExplicitColumns] - public class CacheInstructionDto - { - [Column("id")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] - public int Id { get; set; } + [Column("id")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] + public int Id { get; set; } - [Column("utcStamp")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime UtcStamp { get; set; } + [Column("utcStamp")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime UtcStamp { get; set; } - [Column("jsonInstruction")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Instructions { get; set; } = null!; + [Column("jsonInstruction")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Instructions { get; set; } = null!; - [Column("originated")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(500)] - public string OriginIdentity { get; set; } = null!; + [Column("originated")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(500)] + public string OriginIdentity { get; set; } = null!; - [Column("instructionCount")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = 1)] - public int InstructionCount { get; set; } - } + [Column("instructionCount")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = 1)] + public int InstructionCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs index e0c9b73c78..c6f9006b29 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs @@ -1,43 +1,42 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Consent)] +[PrimaryKey("id")] +[ExplicitColumns] +public class ConsentDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Consent)] - [PrimaryKey("id")] - [ExplicitColumns] - public class ConsentDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("current")] - public bool Current { get; set; } + [Column("current")] + public bool Current { get; set; } - [Column("source")] - [Length(512)] - public string? Source { get; set; } + [Column("source")] + [Length(512)] + public string? Source { get; set; } - [Column("context")] - [Length(128)] - public string? Context { get; set; } + [Column("context")] + [Length(128)] + public string? Context { get; set; } - [Column("action")] - [Length(512)] - public string? Action { get; set; } + [Column("action")] + [Length(512)] + public string? Action { get; set; } - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("state")] - public int State { get; set; } + [Column("state")] + public int State { get; set; } - [Column("comment")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Comment { get; set; } - } + [Column("comment")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Comment { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs index 232f055e85..21e94349bd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentDto.cs @@ -1,33 +1,33 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class ContentDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class ContentDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Content; + public const string TableName = Constants.DatabaseSchema.Tables.Content; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "NodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "NodeId")] + public int ContentTypeId { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; - // although a content has many content versions, - // they can only be loaded one by one (as several content), - // so this here is a OneToOne reference - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + // although a content has many content versions, + // they can only be loaded one by one (as several content), + // so this here is a OneToOne reference + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs index eb3077cb3b..7f64054d14 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentNuDto.cs @@ -1,40 +1,38 @@ using System.Data; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.NodeData)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class ContentNuDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.NodeData)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class ContentNuDto - { - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")] - [ForeignKey(typeof(ContentDto), Column = "nodeId", OnDelete = Rule.Cascade)] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsContentNu", OnColumns = "nodeId, published")] + [ForeignKey(typeof(ContentDto), Column = "nodeId", OnDelete = Rule.Cascade)] + public int NodeId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] + public bool Published { get; set; } - /// - /// Stores serialized JSON representing the content item's property and culture name values - /// - /// - /// Pretty much anything that would require a 1:M lookup is serialized here - /// - [Column("data")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Data { get; set; } + /// + /// Stores serialized JSON representing the content item's property and culture name values + /// + /// + /// Pretty much anything that would require a 1:M lookup is serialized here + /// + [Column("data")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Data { get; set; } - [Column("rv")] - public long Rv { get; set; } + [Column("rv")] + public long Rv { get; set; } - [Column("dataRaw")] - [NullSetting(NullSetting = NullSettings.Null)] - public byte[]? RawData { get; set; } - - - } + [Column("dataRaw")] + [NullSetting(NullSetting = NullSettings.Null)] + public byte[]? RawData { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs index d50da8a124..ad4c03ac53 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs @@ -1,33 +1,32 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentScheduleDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentScheduleDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentSchedule; + public const string TableName = Constants.DatabaseSchema.Tables.ContentSchedule; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - public Guid Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [NullSetting(NullSetting = NullSettings.Null)] // can be invariant - public int? LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [NullSetting(NullSetting = NullSettings.Null)] // can be invariant + public int? LanguageId { get; set; } - [Column("date")] - public DateTime Date { get; set; } + [Column("date")] + public DateTime Date { get; set; } - [Column("action")] - public string? Action { get; set; } - } + [Column("action")] + public string? Action { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs index 2bda31c1fc..3931f2c5e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentType2ContentTypeDto.cs @@ -1,19 +1,19 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ElementTypeTree)] - [ExplicitColumns] - internal class ContentType2ContentTypeDto - { - [Column("parentContentTypeId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentType2ContentType", OnColumns = "parentContentTypeId, childContentTypeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_parent")] - public int ParentId { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("childContentTypeId")] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_child")] - public int ChildId { get; set; } - } +[TableName(Constants.DatabaseSchema.Tables.ElementTypeTree)] +[ExplicitColumns] +internal class ContentType2ContentTypeDto +{ + [Column("parentContentTypeId")] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentType2ContentType", OnColumns = "parentContentTypeId, childContentTypeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_parent")] + public int ParentId { get; set; } + + [Column("childContentTypeId")] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsContentType2ContentType_umbracoNode_child")] + public int ChildId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs index c9a4b274b7..fec7983d1f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeAllowedContentTypeDto.cs @@ -1,28 +1,28 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.ContentChildType)] +[PrimaryKey("Id", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentTypeAllowedContentTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ContentChildType)] - [PrimaryKey("Id", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentTypeAllowedContentTypeDto - { - [Column("Id")] - [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType", Column = "nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentTypeAllowedContentType", OnColumns = "Id, AllowedId")] - public int Id { get; set; } + [Column("Id")] + [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType", Column = "nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_cmsContentTypeAllowedContentType", OnColumns = "Id, AllowedId")] + public int Id { get; set; } - [Column("AllowedId")] - [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType1", Column = "nodeId")] - public int AllowedId { get; set; } + [Column("AllowedId")] + [ForeignKey(typeof(ContentTypeDto), Name = "FK_cmsContentTypeAllowedContentType_cmsContentType1", Column = "nodeId")] + public int AllowedId { get; set; } - [Column("SortOrder")] - [Constraint(Name = "df_cmsContentTypeAllowedContentType_sortOrder", Default = "0")] - public int SortOrder { get; set; } + [Column("SortOrder")] + [Constraint(Name = "df_cmsContentTypeAllowedContentType_sortOrder", Default = "0")] + public int SortOrder { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentTypeDto? ContentTypeDto { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentTypeDto? ContentTypeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs index 8d4019de09..75df880478 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs @@ -1,61 +1,61 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class ContentTypeDto { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class ContentTypeDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentType; - private string? _alias; + public const string TableName = Constants.DatabaseSchema.Tables.ContentType; + private string? _alias; - [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 700)] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn(IdentitySeed = 700)] + public int PrimaryKey { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsContentType")] + public int NodeId { get; set; } - [Column("alias")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } + [Column("alias")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } - [Column("icon")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } + [Column("icon")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } - [Column("thumbnail")] - [Constraint(Default = "folder.png")] - public string? Thumbnail { get; set; } + [Column("thumbnail")] + [Constraint(Default = "folder.png")] + public string? Thumbnail { get; set; } - [Column("description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(1500)] - public string? Description { get; set; } + [Column("description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(1500)] + public string? Description { get; set; } - [Column("isContainer")] - [Constraint(Default = "0")] - public bool IsContainer { get; set; } + [Column("isContainer")] + [Constraint(Default = "0")] + public bool IsContainer { get; set; } - [Column("isElement")] - [Constraint(Default = "0")] - public bool IsElement { get; set; } + [Column("isElement")] + [Constraint(Default = "0")] + public bool IsElement { get; set; } - [Column("allowAtRoot")] - [Constraint(Default = "0")] - public bool AllowAtRoot { get; set; } + [Column("allowAtRoot")] + [Constraint(Default = "0")] + public bool AllowAtRoot { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs index 0b79aeb7aa..ad653f2759 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeTemplateDto.cs @@ -1,29 +1,29 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.DocumentType)] +[PrimaryKey("contentTypeNodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentTypeTemplateDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.DocumentType)] - [PrimaryKey("contentTypeNodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentTypeTemplateDto - { - [Column("contentTypeNodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", OnColumns = "contentTypeNodeId, templateNodeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - [ForeignKey(typeof(NodeDto))] - public int ContentTypeNodeId { get; set; } + [Column("contentTypeNodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", OnColumns = "contentTypeNodeId, templateNodeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + [ForeignKey(typeof(NodeDto))] + public int ContentTypeNodeId { get; set; } - [Column("templateNodeId")] - [ForeignKey(typeof(TemplateDto), Column = "nodeId")] - public int TemplateNodeId { get; set; } + [Column("templateNodeId")] + [ForeignKey(typeof(TemplateDto), Column = "nodeId")] + public int TemplateNodeId { get; set; } - [Column("IsDefault")] - [Constraint(Default = "0")] - public bool IsDefault { get; set; } + [Column("IsDefault")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentTypeDto? ContentTypeDto { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentTypeDto? ContentTypeDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs index 4b2faa166f..da0771df01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs @@ -1,34 +1,32 @@ -using System; using System.Data; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("contentTypeId", AutoIncrement = false)] +[ExplicitColumns] +internal class ContentVersionCleanupPolicyDto { - [TableName(TableName)] - [PrimaryKey("contentTypeId", AutoIncrement = false)] - [ExplicitColumns] - internal class ContentVersionCleanupPolicyDto - { - public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy; + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy; - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId", OnDelete = Rule.Cascade)] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId", OnDelete = Rule.Cascade)] + public int ContentTypeId { get; set; } - [Column("preventCleanup")] - public bool PreventCleanup { get; set; } + [Column("preventCleanup")] + public bool PreventCleanup { get; set; } - [Column("keepAllVersionsNewerThanDays")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? KeepAllVersionsNewerThanDays { get; set; } + [Column("keepAllVersionsNewerThanDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepAllVersionsNewerThanDays { get; set; } - [Column("keepLatestVersionPerDayForDays")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? KeepLatestVersionPerDayForDays { get; set; } + [Column("keepLatestVersionPerDayForDays")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? KeepLatestVersionPerDayForDays { get; set; } - [Column("updated")] - public DateTime Updated { get; set; } - } + [Column("updated")] + public DateTime Updated { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs index 32307efb2b..48c6ee97ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -1,44 +1,47 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class ContentVersionCultureVariationDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class ContentVersionCultureVariationDto + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; + private int? _updateUserId; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,languageId")] + public int VersionId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } + + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string? Culture { get; set; } + + [Column("name")] + public string? Name { get; set; } + + [Column("date")] // TODO: db rename to 'updateDate' + public DateTime UpdateDate { get; set; } + + [Column("availableUserId")] // TODO: db rename to 'updateDate' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UpdateUserId { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation; - private int? _updateUserId; - - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } - - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,languageId")] - public int VersionId { get; set; } - - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - public int LanguageId { get; set; } - - // this is convenient to carry the culture around, but has no db counterpart - [Ignore] - public string? Culture { get; set; } - - [Column("name")] - public string? Name { get; set; } - - [Column("date")] // TODO: db rename to 'updateDate' - public DateTime UpdateDate { get; set; } - - [Column("availableUserId")] // TODO: db rename to 'updateDate' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UpdateUserId { get => _updateUserId == 0 ? null : _updateUserId; set => _updateUserId = value; } //return null if zero - } + get => _updateUserId == 0 ? null : _updateUserId; + set => _updateUserId = value; + } // return null if zero } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs index a000811c55..3a6aae2aff 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs @@ -1,57 +1,55 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +public class ContentVersionDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - public class ContentVersionDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion; - private int? _userId; + public const string TableName = Constants.DatabaseSchema.Tables.ContentVersion; + private int? _userId; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(ContentDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(ContentDto))] + [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' - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime VersionDate { get; set; } + [Column("versionDate")] // TODO: db rename to 'updateDate' + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime VersionDate { get; set; } - [Column("userId")] // TODO: db rename to 'updateUserId' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + [Column("userId")] // TODO: db rename to 'updateUserId' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + 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; } + [Column("current")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")] + public bool Current { get; set; } - // about current: - // there is nothing in the DB that guarantees that there will be one, and exactly one, current version per content item. - // that would require circular FKs that are impossible (well, it is possible to create them, but not to insert). - // we could use a content.currentVersionId FK that would need to be nullable, or (better?) an additional table - // linking a content itemt to its current version (nodeId, versionId) - that would guarantee uniqueness BUT it would - // not guarantee existence - so, really... we are trusting our code to manage 'current' correctly. + // about current: + // there is nothing in the DB that guarantees that there will be one, and exactly one, current version per content item. + // that would require circular FKs that are impossible (well, it is possible to create them, but not to insert). + // we could use a content.currentVersionId FK that would need to be nullable, or (better?) an additional table + // linking a content itemt to its current version (nodeId, versionId) - that would guarantee uniqueness BUT it would + // not guarantee existence - so, really... we are trusting our code to manage 'current' correctly. + [Column("text")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Text { get; set; } - [Column("text")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Text { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId", ReferenceMemberName = "NodeId")] + public ContentDto? ContentDto { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId", ReferenceMemberName = "NodeId")] - public ContentDto? ContentDto { get; set; } - - [Column("preventCleanup")] - [Constraint(Default = "0")] - public bool PreventCleanup { get; set; } - } + [Column("preventCleanup")] + [Constraint(Default = "0")] + public bool PreventCleanup { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs index ae6b922657..060c9d5a23 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs @@ -1,38 +1,37 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("id")] +public class CreatedPackageSchemaDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("id")] - public class CreatedPackageSchemaDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.CreatedPackageSchema; + public const string TableName = Constants.DatabaseSchema.Tables.CreatedPackageSchema; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("name")] - [Length(255)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] - public string Name { get; set; } = null!; + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } = null!; - [Column("value")] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Value { get; set; } = null!; + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } = null!; - [Column("updateDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } - [Column("packageId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public Guid PackageId { get; set; } - } + [Column("packageId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid PackageId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs index c51ce4947c..f3d376b078 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DataTypeDto.cs @@ -1,32 +1,32 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.DataType)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class DataTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.DataType)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class DataTypeDto - { - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("propertyEditorAlias")] - public string EditorAlias { get; set; } = null!; // TODO: should this have a length + [Column("propertyEditorAlias")] + public string EditorAlias { get; set; } = null!; // TODO: should this have a length - [Column("dbType")] - [Length(50)] - public string DbType { get; set; } = null!; + [Column("dbType")] + [Length(50)] + public string DbType { get; set; } = null!; - [Column("config")] - [SpecialDbType(SpecialDbTypes.NTEXT)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Configuration { get; set; } + [Column("config")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Configuration { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs index ad14f20c6b..8d93616ac8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DictionaryDto.cs @@ -1,38 +1,36 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +public class DictionaryDto // public as required to be accessible from Deploy for the RepairDictionaryIdsWorkItem. { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - public class DictionaryDto // public as required to be accessible from Deploy for the RepairDictionaryIdsWorkItem. - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DictionaryEntry; + public const string TableName = Constants.DatabaseSchema.Tables.DictionaryEntry; - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("id")] - [Index(IndexTypes.UniqueNonClustered)] - public Guid UniqueId { get; set; } + [Column("id")] + [Index(IndexTypes.UniqueNonClustered)] + public Guid UniqueId { get; set; } - [Column("parent")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(DictionaryDto), Column = "id")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Parent")] - public Guid? Parent { get; set; } + [Column("parent")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(DictionaryDto), Column = "id")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Parent")] + public Guid? Parent { get; set; } - [Column("key")] - [Length(450)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_key")] - public string Key { get; set; } = null!; + [Column("key")] + [Length(450)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_key")] + public string Key { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.Many, ColumnName = "UniqueId", ReferenceMemberName = "UniqueId")] - public List? LanguageTextDtos { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.Many, ColumnName = "UniqueId", ReferenceMemberName = "UniqueId")] + public List? LanguageTextDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs index e13d19ae34..2bd9f559ec 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs @@ -1,52 +1,52 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class DocumentCultureVariationDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class DocumentCultureVariationDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation; + public const string TableName = Constants.DatabaseSchema.Tables.DocumentCultureVariation; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,languageId")] + public int NodeId { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - public int LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + public int LanguageId { get; set; } - // this is convenient to carry the culture around, but has no db counterpart - [Ignore] - public string? Culture { get; set; } + // this is convenient to carry the culture around, but has no db counterpart + [Ignore] + public string? Culture { get; set; } - // authority on whether a culture has been edited - [Column("edited")] - public bool Edited { get; set; } + // authority on whether a culture has been edited + [Column("edited")] + public bool Edited { get; set; } - // de-normalized for perfs - // (means there is a current content version culture variation for the language) - [Column("available")] - public bool Available { get; set; } + // de-normalized for perfs + // (means there is a current content version culture variation for the language) + [Column("available")] + public bool Available { get; set; } - // de-normalized for perfs - // (means there is a published content version culture variation for the language) - [Column("published")] - public bool Published { get; set; } + // de-normalized for perfs + // (means there is a published content version culture variation for the language) + [Column("published")] + public bool Published { get; set; } - // de-normalized for perfs - // (when available, copies name from current content version culture variation for the language) - // (otherwise, it's the published one, 'cos we need to have one) - [Column("name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } - } + // de-normalized for perfs + // (when available, copies name from current content version culture variation for the language) + // (otherwise, it's the published one, 'cos we need to have one) + [Column("name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs index 39e4e933b2..715d588ff4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs @@ -1,58 +1,56 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +public class DocumentDto { + private const string TableName = Constants.DatabaseSchema.Tables.Document; - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - public class DocumentDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Document; + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } + [Column("published")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")] + public bool Published { get; set; } - [Column("published")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")] - public bool Published { get; set; } + [Column("edited")] + public bool Edited { get; set; } - [Column("edited")] - public bool Edited { get; set; } + // [Column("publishDate")] + // [NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version + // public DateTime? PublishDate { get; set; } - //[Column("publishDate")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version - //public DateTime? PublishDate { get; set; } + // [Column("publishUserId")] + // [NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.UserId for the published version + // public int? PublishUserId { get; set; } - //[Column("publishUserId")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.UserId for the published version - //public int? PublishUserId { get; set; } + // [Column("publishName")] + // [NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.Text for the published version + // public string PublishName { get; set; } - //[Column("publishName")] - //[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.Text for the published version - //public string PublishName { get; set; } + // [Column("publishTemplateId")] + // [NullSetting(NullSetting = NullSettings.Null)] // is documentVersionDto.TemplateId for the published version + // public int? PublishTemplateId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; - //[Column("publishTemplateId")] - //[NullSetting(NullSetting = NullSettings.Null)] // is documentVersionDto.TemplateId for the published version - //public int? PublishTemplateId { get; set; } + // although a content has many content versions, + // they can only be loaded one by one (as several content), + // so this here is a OneToOne reference + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public DocumentVersionDto DocumentVersionDto { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; - - // although a content has many content versions, - // they can only be loaded one by one (as several content), - // so this here is a OneToOne reference - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public DocumentVersionDto DocumentVersionDto { get; set; } = null!; - - // same - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public DocumentVersionDto PublishedVersionDto { get; set; } = null!; - } + // same + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public DocumentVersionDto? PublishedVersionDto { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs index a6fcd6b319..2f0b2ed5f5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs @@ -1,26 +1,25 @@ -using System; using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Document)] +[PrimaryKey("versionId", AutoIncrement = false)] +[ExplicitColumns] +internal class DocumentPublishedReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Document)] - [PrimaryKey("versionId", AutoIncrement = false)] - [ExplicitColumns] - internal class DocumentPublishedReadOnlyDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + public int NodeId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] + public bool Published { get; set; } - [Column("versionId")] - public Guid VersionId { get; set; } + [Column("versionId")] + public Guid VersionId { get; set; } - [Column("newest")] - public bool Newest { get; set; } + [Column("newest")] + public bool Newest { get; set; } - [Column("updateDate")] - public DateTime VersionDate { get; set; } - } + [Column("updateDate")] + public DateTime VersionDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs index 2d06129ba6..75dea080a2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentVersionDto.cs @@ -1,30 +1,30 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +public class DocumentVersionDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - public class DocumentVersionDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion; + private const string TableName = Constants.DatabaseSchema.Tables.DocumentVersion; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentVersionDto))] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentVersionDto))] + public int Id { get; set; } - [Column("templateId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(TemplateDto), Column = "nodeId")] - public int? TemplateId { get; set; } + [Column("templateId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(TemplateDto), Column = "nodeId")] + public int? TemplateId { get; set; } - [Column("published")] - public bool Published { get; set; } + [Column("published")] + public bool Published { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs index 60f0635035..31a04fd664 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DomainDto.cs @@ -1,33 +1,33 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Domain)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class DomainDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Domain)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class DomainDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("domainDefaultLanguage")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? DefaultLanguage { get; set; } + [Column("domainDefaultLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? DefaultLanguage { get; set; } - [Column("domainRootStructureID")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto))] - public int? RootStructureId { get; set; } + [Column("domainRootStructureID")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto))] + public int? RootStructureId { get; set; } - [Column("domainName")] - public string DomainName { get; set; } = null!; + [Column("domainName")] + public string DomainName { get; set; } = null!; - /// - /// Used for a result on the query to get the associated language for a domain if there is one - /// - [ResultColumn("languageISOCode")] - public string IsoCode { get; set; } = null!; - } + /// + /// Used for a result on the query to get the associated language for a domain if there is one + /// + [ResultColumn("languageISOCode")] + public string IsoCode { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index b6eae7f234..017ab3c6e4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -1,57 +1,56 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class ExternalLoginDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class ExternalLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLogin; + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLogin; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Obsolete("This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] - [ResultColumn("userId")] - public int? UserId { get; set; } + [Obsolete("This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [ResultColumn("userId")] + public int? UserId { get; set; } - [Column("userOrMemberKey")] - [Index(IndexTypes.NonClustered)] - public Guid UserOrMemberKey { get; set; } + [Column("userOrMemberKey")] + [Index(IndexTypes.NonClustered)] + public Guid UserOrMemberKey { get; set; } - /// - /// Used to store the name of the provider (i.e. Facebook, Google) - /// - [Column("loginProvider")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", Name = "IX_" + TableName + "_LoginProvider")] - public string LoginProvider { get; set; } = null!; + /// + /// Used to store the name of the provider (i.e. Facebook, Google) + /// + [Column("loginProvider")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", Name = "IX_" + TableName + "_LoginProvider")] + public string LoginProvider { get; set; } = null!; - /// - /// Stores the key the provider uses to lookup the login - /// - [Column("providerKey")] - [Length(4000)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")] - public string ProviderKey { get; set; } = null!; + /// + /// Stores the key the provider uses to lookup the login + /// + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")] + public string ProviderKey { get; set; } = null!; - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - /// - /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider - /// - [Column("userData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? UserData { get; set; } - } + /// + /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider + /// + [Column("userData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? UserData { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs index cd16703bdc..b9ae050960 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs @@ -1,42 +1,41 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class ExternalLoginTokenDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class ExternalLoginTokenDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.ExternalLoginToken; + public const string TableName = Constants.DatabaseSchema.Tables.ExternalLoginToken; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("externalLoginId")] - [ForeignKey(typeof(ExternalLoginDto), Column = "id")] - public int ExternalLoginId { get; set; } + [Column("externalLoginId")] + [ForeignKey(typeof(ExternalLoginDto), Column = "id")] + public int ExternalLoginId { get; set; } - [Column("name")] - [Length(255)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "externalLoginId,name", Name = "IX_" + TableName + "_Name")] - public string Name { get; set; } = null!; + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "externalLoginId,name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } = null!; - [Column("value")] - [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Value { get; set; } = null!; + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } = null!; - [Column("createDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "ExternalLoginId")] - public ExternalLoginDto ExternalLoginDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "ExternalLoginId")] + public ExternalLoginDto ExternalLoginDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs index 654d3071b0..c5829873fe 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs @@ -1,26 +1,25 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.KeyValue)] +[PrimaryKey("key", AutoIncrement = false)] +[ExplicitColumns] +internal class KeyValueDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.KeyValue)] - [PrimaryKey("key", AutoIncrement = false)] - [ExplicitColumns] - internal class KeyValueDto - { - [Column("key")] - [Length(256)] - [PrimaryKeyColumn(AutoIncrement = false, Clustered = true)] - public string Key { get; set; } = null!; + [Column("key")] + [Length(256)] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true)] + public string Key { get; set; } = null!; - [Column("value")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Value { get; set; } + [Column("value")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Value { get; set; } - [Column("updated")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } - } + [Column("updated")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs index e5b25fa166..bcf8403b73 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs @@ -1,60 +1,60 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LanguageDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LanguageDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Language; + public const string TableName = Constants.DatabaseSchema.Tables.Language; - /// - /// Gets or sets the identifier of the language. - /// - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 2)] - public short Id { get; set; } + /// + /// Gets or sets the identifier of the language. + /// + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 2)] + public short Id { get; set; } - /// - /// Gets or sets the ISO code of the language. - /// - [Column("languageISOCode")] - [Index(IndexTypes.UniqueNonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(14)] - public string? IsoCode { get; set; } + /// + /// Gets or sets the ISO code of the language. + /// + [Column("languageISOCode")] + [Index(IndexTypes.UniqueNonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(14)] + public string? IsoCode { get; set; } - /// - /// Gets or sets the culture name of the language. - /// - [Column("languageCultureName")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(100)] - public string? CultureName { get; set; } + /// + /// Gets or sets the culture name of the language. + /// + [Column("languageCultureName")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(100)] + public string? CultureName { get; set; } - /// - /// Gets or sets a value indicating whether the language is the default language. - /// - [Column("isDefaultVariantLang")] - [Constraint(Default = "0")] - public bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether the language is the default language. + /// + [Column("isDefaultVariantLang")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } - /// - /// Gets or sets a value indicating whether the language is mandatory. - /// - [Column("mandatory")] - [Constraint(Default = "0")] - public bool IsMandatory { get; set; } + /// + /// Gets or sets a value indicating whether the language is mandatory. + /// + [Column("mandatory")] + [Constraint(Default = "0")] + public bool IsMandatory { get; set; } - /// - /// Gets or sets the identifier of a fallback language. - /// - [Column("fallbackLanguageId")] - [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.NonClustered)] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FallbackLanguageId { get; set; } - } + /// + /// Gets or sets the identifier of a fallback language. + /// + [Column("fallbackLanguageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.NonClustered)] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FallbackLanguageId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs index 3d08c9de98..1cbc3a9a15 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageTextDto.cs @@ -1,31 +1,30 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("pk")] +[ExplicitColumns] +public class LanguageTextDto { - [TableName(TableName)] - [PrimaryKey("pk")] - [ExplicitColumns] - public class LanguageTextDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.DictionaryValue; + public const string TableName = Constants.DatabaseSchema.Tables.DictionaryValue; - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto), Column = "id")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_languageId", ForColumns = "languageId,UniqueId")] - public int LanguageId { get; set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_languageId", ForColumns = "languageId,UniqueId")] + public int LanguageId { get; set; } - [Column("UniqueId")] - [ForeignKey(typeof(DictionaryDto), Column = "id")] - public Guid UniqueId { get; set; } + [Column("UniqueId")] + [ForeignKey(typeof(DictionaryDto), Column = "id")] + public Guid UniqueId { get; set; } - [Column("value")] - [Length(1000)] - public string Value { get; set; } = null!; - } + [Column("value")] + [Length(1000)] + public string Value { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs index 21fe45c22b..5b1fce4623 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LockDto.cs @@ -1,24 +1,24 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Lock)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class LockDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Lock)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class LockDto - { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoLock", AutoIncrement = false)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoLock", AutoIncrement = false)] + public int Id { get; set; } - [Column("value")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int Value { get; set; } = 1; + [Column("value")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int Value { get; set; } = 1; - [Column("name")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(64)] - public string Name { get; set; } = null!; - } + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(64)] + public string Name { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs index 3b2009f3da..b464d6628d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs @@ -1,61 +1,60 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LogDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LogDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Log; + public const string TableName = Constants.DatabaseSchema.Tables.Log; - private int? _userId; + private int? _userId; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("userId")] - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + [Column("userId")] + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } // return null if zero - [Column("NodeId")] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoLog")] - public int NodeId { get; set; } + [Column("NodeId")] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoLog")] + public int NodeId { get; set; } - /// - /// This is the entity type associated with the log - /// - [Column("entityType")] - [Length(50)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? EntityType { get; set; } + /// + /// This is the entity type associated with the log + /// + [Column("entityType")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? EntityType { get; set; } - // TODO: Should we have an index on this since we allow searching on it? - [Column("Datestamp")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime Datestamp { get; set; } + // TODO: Should we have an index on this since we allow searching on it? + [Column("Datestamp")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime Datestamp { get; set; } - // TODO: Should we have an index on this since we allow searching on it? - [Column("logHeader")] - [Length(50)] - public string Header { get; set; } = null!; + // TODO: Should we have an index on this since we allow searching on it? + [Column("logHeader")] + [Length(50)] + public string Header { get; set; } = null!; - [Column("logComment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(4000)] - public string? Comment { get; set; } + [Column("logComment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(4000)] + public string? Comment { get; set; } - /// - /// Used to store additional data parameters for the log - /// - [Column("parameters")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? Parameters { get; set; } - } + /// + /// Used to store additional data parameters for the log + /// + [Column("parameters")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Parameters { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs index 66b4a1902c..a39c4ef756 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogViewerQueryDto.cs @@ -1,22 +1,22 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.LogViewerQuery)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class LogViewerQueryDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class LogViewerQueryDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("name")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_LogViewerQuery_name")] - public string? Name { get; set; } + [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_LogViewerQuery_name")] + public string? Name { get; set; } - [Column("query")] - public string? Query { get; set; } - } + [Column("query")] + public string? Query { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs index 3f9dae2744..eb46f8f5b9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroDto.cs @@ -1,61 +1,59 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Macro)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class MacroDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Macro)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class MacroDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("uniqueId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacro_UniqueId")] - public Guid UniqueId { get; set; } + [Column("uniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacro_UniqueId")] + public Guid UniqueId { get; set; } - [Column("macroUseInEditor")] - [Constraint(Default = "0")] - public bool UseInEditor { get; set; } + [Column("macroUseInEditor")] + [Constraint(Default = "0")] + public bool UseInEditor { get; set; } - [Column("macroRefreshRate")] - [Constraint(Default = "0")] - public int RefreshRate { get; set; } + [Column("macroRefreshRate")] + [Constraint(Default = "0")] + public int RefreshRate { get; set; } - [Column("macroAlias")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroPropertyAlias")] - public string Alias { get; set; } = string.Empty; + [Column("macroAlias")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroPropertyAlias")] + public string Alias { get; set; } = string.Empty; - [Column("macroName")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } + [Column("macroName")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } - [Column("macroCacheByPage")] - [Constraint(Default = "1")] - public bool CacheByPage { get; set; } + [Column("macroCacheByPage")] + [Constraint(Default = "1")] + public bool CacheByPage { get; set; } - [Column("macroCachePersonalized")] - [Constraint(Default = "0")] - public bool CachePersonalized { get; set; } + [Column("macroCachePersonalized")] + [Constraint(Default = "0")] + public bool CachePersonalized { get; set; } - [Column("macroDontRender")] - [Constraint(Default = "0")] - public bool DontRender { get; set; } + [Column("macroDontRender")] + [Constraint(Default = "0")] + public bool DontRender { get; set; } - [Column("macroSource")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string MacroSource { get; set; } = null!; + [Column("macroSource")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string MacroSource { get; set; } = null!; - [Column("macroType")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public int MacroType { get; set; } + [Column("macroType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int MacroType { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "Macro")] - public List? MacroPropertyDtos { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "Macro")] + public List? MacroPropertyDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs index 62e64e77a9..98eb9de0b6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MacroPropertyDto.cs @@ -1,40 +1,39 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.MacroProperty)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class MacroPropertyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.MacroProperty)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class MacroPropertyDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - // important to use column name != cmsMacro.uniqueId (fix in v8) - [Column("uniquePropertyId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_UniquePropertyId")] - public Guid UniqueId { get; set; } + // important to use column name != cmsMacro.uniqueId (fix in v8) + [Column("uniquePropertyId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_UniquePropertyId")] + public Guid UniqueId { get; set; } - [Column("editorAlias")] - public string EditorAlias { get; set; } = null!; + [Column("editorAlias")] + public string EditorAlias { get; set; } = null!; - [Column("macro")] - [ForeignKey(typeof(MacroDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_Alias", ForColumns = "macro, macroPropertyAlias")] - public int Macro { get; set; } + [Column("macro")] + [ForeignKey(typeof(MacroDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsMacroProperty_Alias", ForColumns = "macro, macroPropertyAlias")] + public int Macro { get; set; } - [Column("macroPropertySortOrder")] - [Constraint(Default = "0")] - public byte SortOrder { get; set; } + [Column("macroPropertySortOrder")] + [Constraint(Default = "0")] + public byte SortOrder { get; set; } - [Column("macroPropertyAlias")] - [Length(50)] - public string Alias { get; set; } = null!; + [Column("macroPropertyAlias")] + [Length(50)] + public string Alias { get; set; } = null!; - [Column("macroPropertyName")] - public string? Name { get; set; } - } + [Column("macroPropertyName")] + public string? Name { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs index 374f2437ff..bb6c672f32 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaDto.cs @@ -1,21 +1,19 @@ -using NPoco; +using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +// this is a special Dto that does not have a corresponding table +// and is only used in our code to represent a media item, similar +// to document items. +internal class MediaDto { - // this is a special Dto that does not have a corresponding table - // and is only used in our code to represent a media item, similar - // to document items. + public int NodeId { get; set; } - internal class MediaDto - { - public int NodeId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; - - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public MediaVersionDto MediaVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public MediaVersionDto MediaVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs index dabdb14ca7..223414a976 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MediaVersionDto.cs @@ -1,27 +1,27 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class MediaVersionDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - internal class MediaVersionDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion; + public const string TableName = Constants.DatabaseSchema.Tables.MediaVersion; - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName, ForColumns = "id, path")] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName, ForColumns = "id, path")] + public int Id { get; set; } - [Column("path")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Path { get; set; } + [Column("path")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Path { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne)] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne)] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs index a32257a087..3fa9d3980a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/Member2MemberGroupDto.cs @@ -1,20 +1,20 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Member2MemberGroup)] - [PrimaryKey("Member", AutoIncrement = false)] - [ExplicitColumns] - internal class Member2MemberGroupDto - { - [Column("Member")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsMember2MemberGroup", OnColumns = "Member, MemberGroup")] - [ForeignKey(typeof(MemberDto))] - public int Member { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("MemberGroup")] - [ForeignKey(typeof(NodeDto))] - public int MemberGroup { get; set; } - } +[TableName(Constants.DatabaseSchema.Tables.Member2MemberGroup)] +[PrimaryKey("Member", AutoIncrement = false)] +[ExplicitColumns] +internal class Member2MemberGroupDto +{ + [Column("Member")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsMember2MemberGroup", OnColumns = "Member, MemberGroup")] + [ForeignKey(typeof(MemberDto))] + public int Member { get; set; } + + [Column("MemberGroup")] + [ForeignKey(typeof(NodeDto))] + public int MemberGroup { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs index 6c24bad7c4..77b500eef5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs @@ -1,85 +1,84 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class MemberDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class MemberDto - { - private const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Member; + private const string TableName = Constants.DatabaseSchema.Tables.Member; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false)] - [ForeignKey(typeof(ContentDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false)] + [ForeignKey(typeof(ContentDto))] + public int NodeId { get; set; } - [Column("Email")] - [Length(1000)] - [Constraint(Default = "''")] - public string Email { get; set; } = null!; + [Column("Email")] + [Length(1000)] + [Constraint(Default = "''")] + public string Email { get; set; } = null!; - [Column("LoginName")] - [Length(1000)] - [Constraint(Default = "''")] - [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] - public string LoginName { get; set; } = null!; + [Column("LoginName")] + [Length(1000)] + [Constraint(Default = "''")] + [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] + public string LoginName { get; set; } = null!; - [Column("Password")] - [Length(1000)] - [Constraint(Default = "''")] - public string? Password { get; set; } + [Column("Password")] + [Length(1000)] + [Constraint(Default = "''")] + public string? Password { get; set; } - /// - /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) - /// - [Column("passwordConfig")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? PasswordConfig { get; set; } + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } - [Column("securityStampToken")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(255)] - public string? SecurityStampToken { get; set; } + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } - [Column("emailConfirmedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? EmailConfirmedDate { get; set; } + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } - [Column("failedPasswordAttempts")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FailedPasswordAttempts { get; set; } + [Column("failedPasswordAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedPasswordAttempts { get; set; } - [Column("isLockedOut")] - [Constraint(Default = 0)] - [NullSetting(NullSetting = NullSettings.Null)] - public bool IsLockedOut { get; set; } + [Column("isLockedOut")] + [Constraint(Default = 0)] + [NullSetting(NullSetting = NullSettings.Null)] + public bool IsLockedOut { get; set; } - [Column("isApproved")] - [Constraint(Default = 1)] - public bool IsApproved { get; set; } + [Column("isApproved")] + [Constraint(Default = 1)] + public bool IsApproved { get; set; } - [Column("lastLoginDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLoginDate { get; set; } + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } - [Column("lastLockoutDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLockoutDate { get; set; } + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } - [Column("lastPasswordChangeDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastPasswordChangeDate { get; set; } + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentDto ContentDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentDto ContentDto { get; set; } = null!; - [ResultColumn] - [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] - public ContentVersionDto ContentVersionDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")] + public ContentVersionDto ContentVersionDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs index 9e9b97daf3..5b27863c8a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberPropertyTypeDto.cs @@ -1,35 +1,35 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.MemberPropertyType)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class MemberPropertyTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.MemberPropertyType)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class MemberPropertyTypeDto - { - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("NodeId")] - [ForeignKey(typeof(NodeDto))] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int NodeId { get; set; } + [Column("NodeId")] + [ForeignKey(typeof(NodeDto))] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int NodeId { get; set; } - [Column("propertytypeId")] - public int PropertyTypeId { get; set; } + [Column("propertytypeId")] + public int PropertyTypeId { get; set; } - [Column("memberCanEdit")] - [Constraint(Default = "0")] - public bool CanEdit { get; set; } + [Column("memberCanEdit")] + [Constraint(Default = "0")] + public bool CanEdit { get; set; } - [Column("viewOnProfile")] - [Constraint(Default = "0")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] + [Constraint(Default = "0")] + public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - [Constraint(Default = "0")] - public bool IsSensitive { get; set; } - } + [Column("isSensitive")] + [Constraint(Default = "0")] + public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index 621aba121a..d11ebc96ce 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -1,68 +1,67 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +public class NodeDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - public class NodeDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Node; - public const int NodeIdSeed = 1060; - private int? _userId; + public const string TableName = Constants.DatabaseSchema.Tables.Node; + public const int NodeIdSeed = 1060; + private int? _userId; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] - public int NodeId { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] + public int NodeId { get; set; } - [Column("uniqueId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [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; } + [Column("uniqueId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [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; } - [Column("parentId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")] - public int ParentId { get; set; } + [Column("parentId")] + [ForeignKey(typeof(NodeDto))] + [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; } + // 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")] - [Length(150)] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")] - public string Path { get; set; } = null!; + [Column("path")] + [Length(150)] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")] + public string Path { get; set; } = null!; - [Column("sortOrder")] - public int SortOrder { get; set; } + [Column("sortOrder")] + public int SortOrder { get; set; } - [Column("trashed")] - [Constraint(Default = "0")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] - public bool Trashed { get; set; } + [Column("trashed")] + [Constraint(Default = "0")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] + public bool Trashed { get; set; } - [Column("nodeUser")] // TODO: db rename to 'createUserId' - [ForeignKey(typeof(UserDto))] - [NullSetting(NullSetting = NullSettings.Null)] - public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero + [Column("nodeUser")] // TODO: db rename to 'createUserId' + [ForeignKey(typeof(UserDto))] + [NullSetting(NullSetting = NullSettings.Null)] + public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } // return null if zero - [Column("text")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Text { get; set; } + [Column("text")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Text { get; set; } - [Column("nodeObjectType")] // TODO: db rename to '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("nodeObjectType")] // TODO: db rename to '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")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } - } + [Column("createDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs index bd0c63a412..f0c57e0d18 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyDataDto.cs @@ -1,136 +1,136 @@ -using System; -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyDataDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyDataDto + public const string TableName = Constants.DatabaseSchema.Tables.PropertyData; + public const int VarcharLength = 512; + public const int SegmentLength = 256; + + private decimal? _decimalValue; + + // pk, not used at the moment (never updating) + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("versionId")] + [ForeignKey(typeof(ContentVersionDto))] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] + public int VersionId { get; set; } + + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] + public int PropertyTypeId { get; set; } + + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } + + [Column("segment")] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(SegmentLength)] + public string? Segment { get; set; } + + [Column("intValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? IntegerValue { get; set; } + + [Column("decimalValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? DecimalValue { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.PropertyData; - public const int VarcharLength = 512; - public const int SegmentLength = 256; + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } - private decimal? _decimalValue; + [Column("dateValue")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? DateValue { get; set; } - // pk, not used at the moment (never updating) - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("varcharValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(VarcharLength)] + public string? VarcharValue { get; set; } - [Column("versionId")] - [ForeignKey(typeof(ContentVersionDto))] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_VersionId", ForColumns = "versionId,propertyTypeId,languageId,segment")] - public int VersionId { get; set; } + [Column("textValue")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TextValue { get; set; } - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_PropertyTypeId")] - public int PropertyTypeId { get; set; } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] + public PropertyTypeDto? PropertyTypeDto { get; set; } - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get; set; } - - [Column("segment")] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Segment")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(SegmentLength)] - public string? Segment { get; set; } - - [Column("intValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? IntegerValue { get; set; } - - [Column("decimalValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public decimal? DecimalValue + [Ignore] + public object? Value + { + get { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } - - [Column("dateValue")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? DateValue { get; set; } - - [Column("varcharValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(VarcharLength)] - public string? VarcharValue { get; set; } - - [Column("textValue")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TextValue { get; set; } - - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "PropertyTypeId")] - public PropertyTypeDto? PropertyTypeDto { get; set; } - - [Ignore] - public object? Value - { - get + if (IntegerValue.HasValue) { - if (IntegerValue.HasValue) - return IntegerValue.Value; - - if (DecimalValue.HasValue) - return DecimalValue.Value; - - if (DateValue.HasValue) - return DateValue.Value; - - if (!string.IsNullOrEmpty(VarcharValue)) - return VarcharValue; - - if (!string.IsNullOrEmpty(TextValue)) - return TextValue; - - return null; + return IntegerValue.Value; } - } - public PropertyDataDto Clone(int versionId) - { - return new PropertyDataDto + if (DecimalValue.HasValue) { - VersionId = versionId, - PropertyTypeId = PropertyTypeId, - LanguageId = LanguageId, - Segment = Segment, - IntegerValue = IntegerValue, - DecimalValue = DecimalValue, - DateValue = DateValue, - VarcharValue = VarcharValue, - TextValue = TextValue, - PropertyTypeDto = PropertyTypeDto - }; - } + return DecimalValue.Value; + } - protected bool Equals(PropertyDataDto other) - { - return Id == other.Id; - } + if (DateValue.HasValue) + { + return DateValue.Value; + } - public override bool Equals(object? other) - { - return - !ReferenceEquals(null, other) // other is not null - && (ReferenceEquals(this, other) // and either ref-equals, or same id - || other is PropertyDataDto pdata && pdata.Id == Id); - } + if (!string.IsNullOrEmpty(VarcharValue)) + { + return VarcharValue; + } - public override int GetHashCode() - { - // ReSharper disable once NonReadonlyMemberInGetHashCode - return Id; + if (!string.IsNullOrEmpty(TextValue)) + { + return TextValue; + } + + return null; } } + + public PropertyDataDto Clone(int versionId) => + new PropertyDataDto + { + VersionId = versionId, + PropertyTypeId = PropertyTypeId, + LanguageId = LanguageId, + Segment = Segment, + IntegerValue = IntegerValue, + DecimalValue = DecimalValue, + DateValue = DateValue, + VarcharValue = VarcharValue, + TextValue = TextValue, + PropertyTypeDto = PropertyTypeDto, + }; + + protected bool Equals(PropertyDataDto other) => Id == other.Id; + + public override bool Equals(object? other) => + !ReferenceEquals(null, other) // other is not null + && (ReferenceEquals(this, other) // and either ref-equals, or same id + || (other is PropertyDataDto pdata && pdata.Id == Id)); + + public override int GetHashCode() => + + // ReSharper disable once NonReadonlyMemberInGetHashCode + Id; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs index 8e321fa962..2645735b82 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeCommonDto.cs @@ -1,18 +1,17 @@ -using NPoco; +using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +// this is PropertyTypeDto + the special property type fields for members +// it is used for querying everything needed for a property type, at once +internal class PropertyTypeCommonDto : PropertyTypeDto { - // this is PropertyTypeDto + the special property type fields for members - // it is used for querying everything needed for a property type, at once - internal class PropertyTypeCommonDto : PropertyTypeDto - { - [Column("memberCanEdit")] - public bool CanEdit { get; set; } + [Column("memberCanEdit")] + public bool CanEdit { get; set; } - [Column("viewOnProfile")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] + public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - public bool IsSensitive { get; set; } - } + [Column("isSensitive")] + public bool IsSensitive { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs index dd4652f366..721ea6c507 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeDto.cs @@ -1,85 +1,84 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeDto - { - private string? _alias; + private string? _alias; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 100)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 100)] + public int Id { get; set; } - [Column("dataTypeId")] - [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] + [ForeignKey(typeof(DataTypeDto), Column = "nodeId")] + public int DataTypeId { get; set; } - [Column("contentTypeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeId { get; set; } - [Column("propertyTypeGroupId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(PropertyTypeGroupDto))] - public int? PropertyTypeGroupId { get; set; } + [Column("propertyTypeGroupId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(PropertyTypeGroupDto))] + public int? PropertyTypeGroupId { get; set; } - [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] - [Column("Alias")] - public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } + [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyTypeAlias")] + [Column("Alias")] + public string? Alias { get => _alias; set => _alias = value == null ? null : string.Intern(value); } - [Column("Name")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Name { get; set; } + [Column("Name")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Name { get; set; } - [Column("sortOrder")] - [Constraint(Default = "0")] - public int SortOrder { get; set; } + [Column("sortOrder")] + [Constraint(Default = "0")] + public int SortOrder { get; set; } - [Column("mandatory")] - [Constraint(Default = "0")] - public bool Mandatory { get; set; } + [Column("mandatory")] + [Constraint(Default = "0")] + public bool Mandatory { get; set; } - [Column("mandatoryMessage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? MandatoryMessage { get; set; } + [Column("mandatoryMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? MandatoryMessage { get; set; } - [Column("validationRegExp")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? ValidationRegExp { get; set; } - [Column("validationRegExpMessage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? ValidationRegExpMessage { get; set; } + [Column("validationRegExpMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? ValidationRegExpMessage { get; set; } - [Column("Description")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(2000)] - public string? Description { get; set; } + [Column("Description")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(2000)] + public string? Description { get; set; } - [Column("labelOnTop")] - [Constraint(Default = "0")] - public bool LabelOnTop { get; set; } + [Column("labelOnTop")] + [Constraint(Default = "0")] + public bool LabelOnTop { get; set; } - [Column("variations")] - [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] - public byte Variations { get; set; } + [Column("variations")] + [Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)] + public byte Variations { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] - public DataTypeDto DataTypeDto { get; set; } = null!; + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "DataTypeId")] + public DataTypeDto DataTypeDto { get; set; } = null!; - [Column("UniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs index 489cb7fcb5..9910caaa7b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs @@ -1,47 +1,45 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +internal class PropertyTypeGroupDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - internal class PropertyTypeGroupDto - { - public const string TableName = Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup; + public const string TableName = Constants.DatabaseSchema.Tables.PropertyTypeGroup; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 56)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 56)] + public int Id { get; set; } - [Column("uniqueID")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.NewGuid)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")] - public Guid UniqueId { get; set; } + [Column("uniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")] + public Guid UniqueId { get; set; } - [Column("contenttypeNodeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - public int ContentTypeNodeId { get; set; } + [Column("contenttypeNodeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + public int ContentTypeNodeId { get; set; } - [Column("type")] - [Constraint(Default = 0)] - public short Type { get; set; } + [Column("type")] + [Constraint(Default = 0)] + public short Type { get; set; } - [Column("text")] - public string? Text { get; set; } + [Column("text")] + public string? Text { get; set; } - [Column("alias")] - public string Alias { get; set; } = null!; + [Column("alias")] + public string Alias { get; set; } = null!; - [Column("sortorder")] - public int SortOrder { get; set; } + [Column("sortorder")] + public int SortOrder { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "PropertyTypeGroupId")] - public List? PropertyTypeDtos { get; set; } - } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "PropertyTypeGroupId")] + public List? PropertyTypeDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs index f93b9b602a..9829604193 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupReadOnlyDto.cs @@ -1,26 +1,25 @@ -using System; using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyTypeGroup)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +internal class PropertyTypeGroupReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - internal class PropertyTypeGroupReadOnlyDto - { - [Column("PropertyTypeGroupId")] - public int? Id { get; set; } + [Column("PropertyTypeGroupId")] + public int? Id { get; set; } - [Column("PropertyGroupName")] - public string? Text { get; set; } + [Column("PropertyGroupName")] + public string? Text { get; set; } - [Column("PropertyGroupSortOrder")] - public int SortOrder { get; set; } + [Column("PropertyGroupSortOrder")] + public int SortOrder { get; set; } - [Column("contenttypeNodeId")] - public int ContentTypeNodeId { get; set; } + [Column("contenttypeNodeId")] + public int ContentTypeNodeId { get; set; } - [Column("PropertyGroupUniqueID")] - public Guid UniqueId { get; set; } - } + [Column("PropertyGroupUniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs index ae1358b5cd..94d8436401 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeReadOnlyDto.cs @@ -1,70 +1,69 @@ -using System; using NPoco; +using Umbraco.Cms.Core; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.PropertyType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class PropertyTypeReadOnlyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class PropertyTypeReadOnlyDto - { - [Column("PropertyTypeId")] - public int? Id { get; set; } + [Column("PropertyTypeId")] + public int? Id { get; set; } - [Column("dataTypeId")] - public int DataTypeId { get; set; } + [Column("dataTypeId")] + public int DataTypeId { get; set; } - [Column("contentTypeId")] - public int ContentTypeId { get; set; } + [Column("contentTypeId")] + public int ContentTypeId { get; set; } - [Column("PropertyTypesGroupId")] - public int? PropertyTypeGroupId { get; set; } + [Column("PropertyTypesGroupId")] + public int? PropertyTypeGroupId { get; set; } - [Column("Alias")] - public string? Alias { get; set; } + [Column("Alias")] + public string? Alias { get; set; } - [Column("Name")] - public string? Name { get; set; } + [Column("Name")] + public string? Name { get; set; } - [Column("PropertyTypeSortOrder")] - public int SortOrder { get; set; } + [Column("PropertyTypeSortOrder")] + public int SortOrder { get; set; } - [Column("mandatory")] - public bool Mandatory { get; set; } + [Column("mandatory")] + public bool Mandatory { get; set; } - [Column("mandatoryMessage")] - public string? MandatoryMessage { get; set; } + [Column("mandatoryMessage")] + public string? MandatoryMessage { get; set; } - [Column("validationRegExp")] - public string? ValidationRegExp { get; set; } + [Column("validationRegExp")] + public string? ValidationRegExp { get; set; } - [Column("validationRegExpMessage")] - public string? ValidationRegExpMessage { get; set; } + [Column("validationRegExpMessage")] + public string? ValidationRegExpMessage { get; set; } - [Column("Description")] - public string? Description { get; set; } + [Column("Description")] + public string? Description { get; set; } - [Column("labelOnTop")] - public bool LabelOnTop { get; set; } + [Column("labelOnTop")] + public bool LabelOnTop { get; set; } - /* cmsMemberType */ - [Column("memberCanEdit")] - public bool CanEdit { get; set; } + /* cmsMemberType */ + [Column("memberCanEdit")] + public bool CanEdit { get; set; } - [Column("viewOnProfile")] - public bool ViewOnProfile { get; set; } + [Column("viewOnProfile")] + public bool ViewOnProfile { get; set; } - [Column("isSensitive")] - public bool IsSensitive { get; set; } + [Column("isSensitive")] + public bool IsSensitive { get; set; } - /* DataType */ - [Column("propertyEditorAlias")] - public string? PropertyEditorAlias { get; set; } + /* DataType */ + [Column("propertyEditorAlias")] + public string? PropertyEditorAlias { get; set; } - [Column("dbType")] - public string? DbType { get; set; } + [Column("dbType")] + public string? DbType { get; set; } - [Column("UniqueID")] - public Guid UniqueId { get; set; } - } + [Column("UniqueID")] + public Guid UniqueId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs index 435e072307..b377b49177 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RedirectUrlDto.cs @@ -1,54 +1,49 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.RedirectUrl)] +[PrimaryKey("id", AutoIncrement = false)] +[ExplicitColumns] +internal class RedirectUrlDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.RedirectUrl)] - [PrimaryKey("id", AutoIncrement = false)] - [ExplicitColumns] - class RedirectUrlDto - { - public RedirectUrlDto() - { - CreateDateUtc = DateTime.UtcNow; - } + public RedirectUrlDto() => CreateDateUtc = DateTime.UtcNow; - // notes - // - // we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the - // problem is that the index key must be 900 bytes max. should we run without an index? done - // some perfs comparisons, and running with an index on a hash is only slightly slower on - // inserts, and much faster on reads, so... we have an index on a hash. + // notes + // + // we want a unique, non-clustered index on (url ASC, contentId ASC, culture ASC, createDate DESC) but the + // problem is that the index key must be 900 bytes max. should we run without an index? done + // some perfs comparisons, and running with an index on a hash is only slightly slower on + // inserts, and much faster on reads, so... we have an index on a hash. + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoRedirectUrl", AutoIncrement = false)] + public Guid Id { get; set; } - [Column("id")] - [PrimaryKeyColumn(Name = "PK_umbracoRedirectUrl", AutoIncrement = false)] - public Guid Id { get; set; } + [ResultColumn] + public int ContentId { get; set; } - [ResultColumn] - public int ContentId { get; set; } + [Column("contentKey")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto), Column = "uniqueID")] + public Guid ContentKey { get; set; } - [Column("contentKey")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(NodeDto), Column = "uniqueID")] - public Guid ContentKey { get; set; } + [Column("createDateUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime CreateDateUtc { get; set; } - [Column("createDateUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime CreateDateUtc { get; set; } + [Column("url")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Url { get; set; } = null!; - [Column("url")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Url { get; set; } = null!; + [Column("culture")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Culture { get; set; } - [Column("culture")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Culture { get; set; } - - [Column("urlHash")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, culture, createDateUtc")] - [Length(40)] - public string UrlHash { get; set; } = null!; - } + [Column("urlHash")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRedirectUrl", ForColumns = "urlHash, contentKey, culture, createDateUtc")] + [Length(40)] + public string UrlHash { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs index b197f12692..59484734dc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs @@ -1,46 +1,45 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Relation)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class RelationDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Relation)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class RelationDto - { - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("parentId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] - public int ParentId { get; set; } + [Column("parentId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] + public int ParentId { get; set; } - [Column("childId")] - [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode1")] - public int ChildId { get; set; } + [Column("childId")] + [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode1")] + public int ChildId { get; set; } - [Column("relType")] - [ForeignKey(typeof(RelationTypeDto))] - public int RelationType { get; set; } + [Column("relType")] + [ForeignKey(typeof(RelationTypeDto))] + public int RelationType { get; set; } - [Column("datetime")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime Datetime { get; set; } + [Column("datetime")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime Datetime { get; set; } - [Column("comment")] - [Length(1000)] - public string? Comment { get; set; } + [Column("comment")] + [Length(1000)] + public string? Comment { get; set; } - [ResultColumn] - [Column("parentObjectType")] - public Guid ParentObjectType { get; set; } + [ResultColumn] + [Column("parentObjectType")] + public Guid ParentObjectType { get; set; } - [ResultColumn] - [Column("childObjectType")] - public Guid ChildObjectType { get; set; } - } + [ResultColumn] + [Column("childObjectType")] + public Guid ChildObjectType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs index d1cb5cc278..adefd7ae38 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs @@ -1,48 +1,47 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.RelationType)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class RelationTypeDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.RelationType)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class RelationTypeDto - { - public const int NodeIdSeed = 10; + public const int NodeIdSeed = 10; - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] + public int Id { get; set; } - [Column("typeUniqueId")] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] - public Guid UniqueId { get; set; } + [Column("typeUniqueId")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_UniqueId")] + public Guid UniqueId { get; set; } - [Column("dual")] - public bool Dual { get; set; } + [Column("dual")] + public bool Dual { get; set; } - [Column("parentObjectType")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid? ParentObjectType { get; set; } + [Column("parentObjectType")] + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ParentObjectType { get; set; } - [Column("childObjectType")] - [NullSetting(NullSetting = NullSettings.Null)] - public Guid? ChildObjectType { get; set; } + [Column("childObjectType")] + [NullSetting(NullSetting = NullSettings.Null)] + public Guid? ChildObjectType { get; set; } - [Column("name")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] - public string Name { get; set; } = null!; + [Column("name")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] + public string Name { get; set; } = null!; - [Column("alias")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Length(100)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] - public string Alias { get; set; } = null!; + [Column("alias")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] + public string Alias { get; set; } = null!; - [Constraint(Default = "0")] - [Column("isDependency")] - public bool IsDependency { get; set; } - } + [Constraint(Default = "0")] + [Column("isDependency")] + public bool IsDependency { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs index 89ef0039ab..66a8c2bd07 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs @@ -1,40 +1,39 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Server)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class ServerRegistrationDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Server)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class ServerRegistrationDto - { - [Column("id")] - [PrimaryKeyColumn(AutoIncrement = true)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(AutoIncrement = true)] + public int Id { get; set; } - [Column("address")] - [Length(500)] - public string? ServerAddress { get; set; } + [Column("address")] + [Length(500)] + public string? ServerAddress { get; set; } - [Column("computerName")] - [Length(255)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique - public string? ServerIdentity { get; set; } + [Column("computerName")] + [Length(255)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique + public string? ServerIdentity { get; set; } - [Column("registeredDate")] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime DateRegistered { get; set; } + [Column("registeredDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime DateRegistered { get; set; } - [Column("lastNotifiedDate")] - public DateTime DateAccessed { get; set; } + [Column("lastNotifiedDate")] + public DateTime DateAccessed { get; set; } - [Column("isActive")] - [Index(IndexTypes.NonClustered)] - public bool IsActive { get; set; } + [Column("isActive")] + [Index(IndexTypes.NonClustered)] + public bool IsActive { get; set; } - [Column("isSchedulingPublisher")] - public bool IsSchedulingPublisher { get; set; } - } + [Column("isSchedulingPublisher")] + public bool IsSchedulingPublisher { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs index 8c032660df..cc8b80c777 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TagDto.cs @@ -1,40 +1,40 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id")] +[ExplicitColumns] +internal class TagDto { - [TableName(TableName)] - [PrimaryKey("id")] - [ExplicitColumns] - internal class TagDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.Tag; + public const string TableName = Constants.DatabaseSchema.Tables.Tag; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("group")] - [Length(100)] - public string Group { get; set; } = null!; + [Column("group")] + [Length(100)] + public string Group { get; set; } = null!; - [Column("languageId")] - [ForeignKey(typeof(LanguageDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? LanguageId { get;set; } + [Column("languageId")] + [ForeignKey(typeof(LanguageDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_LanguageId")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? LanguageId { get; set; } - [Column("tag")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] - public string Text { get; set; } = null!; + [Column("tag")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "group,tag,languageId", Name = "IX_cmsTags")] + public string Text { get; set; } = null!; - //[Column("key")] - //[Length(301)] // de-normalized "{group}/{tag}" - //public string Key { get; set; } + // [Column("key")] + // [Length(301)] // de-normalized "{group}/{tag}" + // public string Key { get; set; } - // queries result column - [ResultColumn("NodeCount")] - public int NodeCount { get; set; } - } + // queries result column + [ResultColumn("NodeCount")] + public int NodeCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs index 2cc287ac92..3799679e4d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TagRelationshipDto.cs @@ -1,26 +1,26 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("nodeId", AutoIncrement = false)] +[ExplicitColumns] +internal class TagRelationshipDto { - [TableName(TableName)] - [PrimaryKey("nodeId", AutoIncrement = false)] - [ExplicitColumns] - internal class TagRelationshipDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship; + public const string TableName = Constants.DatabaseSchema.Tables.TagRelationship; - [Column("nodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] - [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsTagRelationship", OnColumns = "nodeId, propertyTypeId, tagId")] + [ForeignKey(typeof(ContentDto), Name = "FK_cmsTagRelationship_cmsContent", Column = "nodeId")] + public int NodeId { get; set; } - [Column("tagId")] - [ForeignKey(typeof(TagDto))] - public int TagId { get; set; } + [Column("tagId")] + [ForeignKey(typeof(TagDto))] + public int TagId { get; set; } - [Column("propertyTypeId")] - [ForeignKey(typeof(PropertyTypeDto), Name = "FK_cmsTagRelationship_cmsPropertyType")] - public int PropertyTypeId { get; set; } - } + [Column("propertyTypeId")] + [ForeignKey(typeof(PropertyTypeDto), Name = "FK_cmsTagRelationship_cmsPropertyType")] + public int PropertyTypeId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs index 9a80cdd8e2..4355a3c983 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TemplateDto.cs @@ -1,29 +1,29 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.Template)] +[PrimaryKey("pk")] +[ExplicitColumns] +internal class TemplateDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.Template)] - [PrimaryKey("pk")] - [ExplicitColumns] - internal class TemplateDto - { - [Column("pk")] - [PrimaryKeyColumn] - public int PrimaryKey { get; set; } + [Column("pk")] + [PrimaryKeyColumn] + public int PrimaryKey { get; set; } - [Column("nodeId")] - [Index(IndexTypes.UniqueNonClustered)] - [ForeignKey(typeof(NodeDto), Name = "FK_cmsTemplate_umbracoNode")] - public int NodeId { get; set; } + [Column("nodeId")] + [Index(IndexTypes.UniqueNonClustered)] + [ForeignKey(typeof(NodeDto), Name = "FK_cmsTemplate_umbracoNode")] + public int NodeId { get; set; } - [Column("alias")] - [Length(100)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Alias { get; set; } + [Column("alias")] + [Length(100)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Alias { get; set; } - [ResultColumn] - [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] - public NodeDto NodeDto { get; set; } = null!; - } + [ResultColumn] + [Reference(ReferenceType.OneToOne, ColumnName = "NodeId")] + public NodeDto NodeDto { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs index 09f6647bfe..760419a307 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs @@ -1,34 +1,32 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +[PrimaryKey("Id")] +internal class TwoFactorLoginDto { - [TableName(TableName)] - [ExplicitColumns] - [PrimaryKey("Id")] - internal class TwoFactorLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TwoFactorLogin; + public const string TableName = Constants.DatabaseSchema.Tables.TwoFactorLogin; - [Column("id")] - [PrimaryKeyColumn] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } - [Column("userOrMemberKey")] - [Index(IndexTypes.NonClustered)] - public Guid UserOrMemberKey { get; set; } + [Column("userOrMemberKey")] + [Index(IndexTypes.NonClustered)] + public Guid UserOrMemberKey { get; set; } - [Column("providerName")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", - Name = "IX_" + TableName + "_ProviderName")] - public string ProviderName { get; set; } = null!; + [Column("providerName")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", Name = "IX_" + TableName + "_ProviderName")] + public string ProviderName { get; set; } = null!; - [Column("secret")] - [Length(400)] - [NullSetting(NullSetting = NullSettings.NotNull)] - public string Secret { get; set; } = null!; - } + [Column("secret")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Secret { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs index fd8806124e..aca7d3682e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/User2NodeNotifyDto.cs @@ -1,25 +1,25 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.User2NodeNotify)] +[PrimaryKey("userId", AutoIncrement = false)] +[ExplicitColumns] +internal class User2NodeNotifyDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify)] - [PrimaryKey("userId", AutoIncrement = false)] - [ExplicitColumns] - internal class User2NodeNotifyDto - { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodeNotify", OnColumns = "userId, nodeId, action")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUser2NodeNotify", OnColumns = "userId, nodeId, action")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + public int NodeId { get; set; } - [Column("action")] - [SpecialDbType(SpecialDbTypes.NCHAR)] - [Length(1)] - public string? Action { get; set; } - } + [Column("action")] + [SpecialDbType(SpecialDbTypes.NCHAR)] + [Length(1)] + public string? Action { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs index db3d5b4e74..3bc059ff21 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/User2UserGroupDto.cs @@ -1,19 +1,19 @@ using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.User2UserGroup)] - [ExplicitColumns] - public class User2UserGroupDto - { - [Column("userId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("userGroupId")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } - } +[TableName(Constants.DatabaseSchema.Tables.User2UserGroup)] +[ExplicitColumns] +public class User2UserGroupDto +{ + [Column("userId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_user2userGroup", OnColumns = "userId, userGroupId")] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("userGroupId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index 20768eed65..16db4a10ad 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -1,127 +1,124 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +public class UserDto { - [TableName(TableName)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - public class UserDto + public const string TableName = Constants.DatabaseSchema.Tables.User; + + public UserDto() { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.User; - - public UserDto() - { - UserGroupDtos = new List(); - UserStartNodeDtos = new HashSet(); - } - - // TODO: We need to add a GUID for users and track external logins with that instead of the INT - - [Column("id")] - [PrimaryKeyColumn(Name = "PK_user")] - public int Id { get; set; } - - [Column("userDisabled")] - [Constraint(Default = "0")] - public bool Disabled { get; set; } - - [Column("userNoConsole")] - [Constraint(Default = "0")] - public bool NoConsole { get; set; } - - [Column("userName")] - public string UserName { get; set; } = null!; - - [Column("userLogin")] - [Length(125)] - [Index(IndexTypes.NonClustered)] - public string? Login { get; set; } - - [Column("userPassword")] - [Length(500)] - public string? Password { get; set; } - - /// - /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) - /// - [Column("passwordConfig")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? PasswordConfig { get; set; } - - [Column("userEmail")] - public string Email { get; set; } = null!; - - [Column("userLanguage")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(10)] - public string? UserLanguage { get; set; } - - [Column("securityStampToken")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(255)] - public string? SecurityStampToken { get; set; } - - [Column("failedLoginAttempts")] - [NullSetting(NullSetting = NullSettings.Null)] - public int? FailedLoginAttempts { get; set; } - - [Column("lastLockoutDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLockoutDate { get; set; } - - [Column("lastPasswordChangeDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastPasswordChangeDate { get; set; } - - [Column("lastLoginDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LastLoginDate { get; set; } - - [Column("emailConfirmedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? EmailConfirmedDate { get; set; } - - [Column("invitedDate")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? InvitedDate { get; set; } - - [Column("createDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } = DateTime.Now; - - [Column("updateDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } = DateTime.Now; - - /// - /// Will hold the media file system relative path of the users custom avatar if they uploaded one - /// - [Column("avatar")] - [NullSetting(NullSetting = NullSettings.Null)] - [Length(500)] - public string? Avatar { get; set; } - - /// - /// A Json blob stored for recording tour data for a user - /// - [Column("tourData")] - [NullSetting(NullSetting = NullSettings.Null)] - [SpecialDbType(SpecialDbTypes.NTEXT)] - public string? TourData { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] - public List UserGroupDtos { get; set; } - - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] - public HashSet UserStartNodeDtos { get; set; } + UserGroupDtos = new List(); + UserStartNodeDtos = new HashSet(); } + + // TODO: We need to add a GUID for users and track external logins with that instead of the INT + [Column("id")] + [PrimaryKeyColumn(Name = "PK_user")] + public int Id { get; set; } + + [Column("userDisabled")] + [Constraint(Default = "0")] + public bool Disabled { get; set; } + + [Column("userNoConsole")] + [Constraint(Default = "0")] + public bool NoConsole { get; set; } + + [Column("userName")] + public string UserName { get; set; } = null!; + + [Column("userLogin")] + [Length(125)] + [Index(IndexTypes.NonClustered)] + public string? Login { get; set; } + + [Column("userPassword")] + [Length(500)] + public string? Password { get; set; } + + /// + /// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations) + /// + [Column("passwordConfig")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? PasswordConfig { get; set; } + + [Column("userEmail")] + public string Email { get; set; } = null!; + + [Column("userLanguage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(10)] + public string? UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string? SecurityStampToken { get; set; } + + [Column("failedLoginAttempts")] + [NullSetting(NullSetting = NullSettings.Null)] + public int? FailedLoginAttempts { get; set; } + + [Column("lastLockoutDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLockoutDate { get; set; } + + [Column("lastPasswordChangeDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastPasswordChangeDate { get; set; } + + [Column("lastLoginDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LastLoginDate { get; set; } + + [Column("emailConfirmedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? EmailConfirmedDate { get; set; } + + [Column("invitedDate")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? InvitedDate { get; set; } + + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } = DateTime.Now; + + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } = DateTime.Now; + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string? Avatar { get; set; } + + /// + /// A Json blob stored for recording tour data for a user + /// + [Column("tourData")] + [NullSetting(NullSetting = NullSettings.Null)] + [SpecialDbType(SpecialDbTypes.NTEXT)] + public string? TourData { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public List UserGroupDtos { get; set; } + + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserId")] + public HashSet UserStartNodeDtos { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs index b5719c1c63..7e2099ae44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2AppDto.cs @@ -1,19 +1,19 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos -{ - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2App)] - [ExplicitColumns] - public class UserGroup2AppDto - { - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; - [Column("app")] - [Length(50)] - public string AppAlias { get; set; } = null!; - } +[TableName(Constants.DatabaseSchema.Tables.UserGroup2App)] +[ExplicitColumns] +public class UserGroup2AppDto +{ + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_userGroup2App", OnColumns = "userGroupId, app")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } + + [Column("app")] + [Length(50)] + public string AppAlias { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs index ad172c846c..54b66b8e22 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodeDto.cs @@ -2,22 +2,21 @@ using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[ExplicitColumns] +internal class UserGroup2NodeDto { - [TableName(TableName)] - [ExplicitColumns] - internal class UserGroup2NodeDto - { - public const string TableName = Constants.DatabaseSchema.Tables.UserGroup2Node; + public const string TableName = Constants.DatabaseSchema.Tables.UserGroup2Node; - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_" + TableName, OnColumns = "userGroupId, nodeId")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_" + TableName, OnColumns = "userGroupId, nodeId")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_nodeId")] - public int NodeId { get; set; } - } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_nodeId")] + public int NodeId { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs index 4461089f96..94a8fd4361 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroup2NodePermissionDto.cs @@ -1,23 +1,23 @@ -using NPoco; +using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup2NodePermission)] +[ExplicitColumns] +internal class UserGroup2NodePermissionDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission)] - [ExplicitColumns] - internal class UserGroup2NodePermissionDto - { - [Column("userGroupId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", OnColumns = "userGroupId, nodeId, permission")] - [ForeignKey(typeof(UserGroupDto))] - public int UserGroupId { get; set; } + [Column("userGroupId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoUserGroup2NodePermission", OnColumns = "userGroupId, nodeId, permission")] + [ForeignKey(typeof(UserGroupDto))] + public int UserGroupId { get; set; } - [Column("nodeId")] - [ForeignKey(typeof(NodeDto))] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] + public int NodeId { get; set; } - [Column("permission")] - public string? Permission { get; set; } - } + [Column("permission")] + public string? Permission { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs index afbda3cc9a..cd868f5956 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs @@ -1,72 +1,67 @@ -using System; -using System.Collections.Generic; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserGroup)] +[PrimaryKey("id")] +[ExplicitColumns] +public class UserGroupDto { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserGroup)] - [PrimaryKey("id")] - [ExplicitColumns] - public class UserGroupDto - { - public UserGroupDto() - { - UserGroup2AppDtos = new List(); - } + public UserGroupDto() => UserGroup2AppDtos = new List(); - [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 6)] - public int Id { get; set; } + [Column("id")] + [PrimaryKeyColumn(IdentitySeed = 6)] + public int Id { get; set; } - [Column("userGroupAlias")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")] - public string? Alias { get; set; } + [Column("userGroupAlias")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupAlias")] + public string? Alias { get; set; } - [Column("userGroupName")] - [Length(200)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] - public string? Name { get; set; } + [Column("userGroupName")] + [Length(200)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] + public string? Name { get; set; } - [Column("userGroupDefaultPermissions")] - [Length(50)] - [NullSetting(NullSetting = NullSettings.Null)] - public string? DefaultPermissions { get; set; } + [Column("userGroupDefaultPermissions")] + [Length(50)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? DefaultPermissions { get; set; } - [Column("createDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime CreateDate { get; set; } + [Column("createDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime CreateDate { get; set; } - [Column("updateDate")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Constraint(Default = SystemMethods.CurrentDateTime)] - public DateTime UpdateDate { get; set; } + [Column("updateDate")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } - [Column("icon")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? Icon { get; set; } + [Column("icon")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Icon { get; set; } - [Column("startContentId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] - public int? StartContentId { get; set; } + [Column("startContentId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startContentId_umbracoNode_id")] + public int? StartContentId { get; set; } - [Column("startMediaId")] - [NullSetting(NullSetting = NullSettings.Null)] - [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] - public int? StartMediaId { get; set; } + [Column("startMediaId")] + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(NodeDto), Name = "FK_startMediaId_umbracoNode_id")] + public int? StartMediaId { get; set; } - [ResultColumn] - [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] - public List UserGroup2AppDtos { get; set; } + [ResultColumn] + [Reference(ReferenceType.Many, ReferenceMemberName = "UserGroupId")] + public List UserGroup2AppDtos { get; set; } - /// - /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) - /// - [ResultColumn] - public int UserCount { get; set; } - } + /// + /// This is only relevant when this column is included in the results (i.e. GetUserGroupsWithUserCounts) + /// + [ResultColumn] + public int UserCount { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs index 4d18a39557..bf52e3fd9c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserLoginDto.cs @@ -1,57 +1,60 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(TableName)] +[PrimaryKey("sessionId", AutoIncrement = false)] +[ExplicitColumns] +internal class UserLoginDto { - [TableName(TableName)] - [PrimaryKey("sessionId", AutoIncrement = false)] - [ExplicitColumns] - internal class UserLoginDto - { - public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.UserLogin; + public const string TableName = Constants.DatabaseSchema.Tables.UserLogin; - [Column("sessionId")] - [PrimaryKeyColumn(AutoIncrement = false)] - public Guid SessionId { get; set; } + [Column("sessionId")] + [PrimaryKeyColumn(AutoIncrement = false)] + public Guid SessionId { get; set; } - [Column("userId")] - [ForeignKey(typeof(UserDto), Name = "FK_" + TableName + "_umbracoUser_id")] - public int? UserId { get; set; } + [Column("userId")] + [ForeignKey(typeof(UserDto), Name = "FK_" + TableName + "_umbracoUser_id")] + public int? UserId { get; set; } - /// - /// Tracks when the session is created - /// - [Column("loggedInUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - public DateTime LoggedInUtc { get; set; } + /// + /// Tracks when the session is created + /// + [Column("loggedInUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime LoggedInUtc { get; set; } - /// - /// Updated every time a user's session is validated - /// - /// - /// This allows us to guess if a session is timed out if a user doesn't actively - /// log out and also allows us to trim the data in the table. - /// The index is IMPORTANT as it prevents deadlocks during deletion of - /// old sessions (DELETE ... WHERE lastValidatedUtc < date). - /// - [Column("lastValidatedUtc")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserLogin_lastValidatedUtc")] - public DateTime LastValidatedUtc { get; set; } + /// + /// Updated every time a user's session is validated + /// + /// + /// + /// This allows us to guess if a session is timed out if a user doesn't actively + /// log out and also allows us to trim the data in the table. + /// + /// + /// The index is IMPORTANT as it prevents deadlocks during deletion of + /// old sessions (DELETE ... WHERE lastValidatedUtc < date). + /// + /// + [Column("lastValidatedUtc")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUserLogin_lastValidatedUtc")] + public DateTime LastValidatedUtc { get; set; } - /// - /// Tracks when the session is removed when the user's account is logged out - /// - [Column("loggedOutUtc")] - [NullSetting(NullSetting = NullSettings.Null)] - public DateTime? LoggedOutUtc { get; set; } + /// + /// Tracks when the session is removed when the user's account is logged out + /// + [Column("loggedOutUtc")] + [NullSetting(NullSetting = NullSettings.Null)] + public DateTime? LoggedOutUtc { get; set; } - /// - /// Logs the IP address of the session if available - /// - [Column("ipAddress")] - [NullSetting(NullSetting = NullSettings.Null)] - public string? IpAddress { get; set; } - } + /// + /// Logs the IP address of the session if available + /// + [Column("ipAddress")] + [NullSetting(NullSetting = NullSettings.Null)] + public string? IpAddress { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs index c6116648c7..327bb69b63 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs @@ -1,20 +1,18 @@ -using System; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +internal class UserNotificationDto { - internal class UserNotificationDto - { - [Column("nodeId")] - public int NodeId { get; set; } + [Column("nodeId")] + public int NodeId { get; set; } - [Column("userId")] - public int UserId { get; set; } + [Column("userId")] + public int UserId { get; set; } - [Column("nodeObjectType")] - public Guid NodeObjectType { get; set; } + [Column("nodeObjectType")] + public Guid NodeObjectType { get; set; } - [Column("action")] - public string Action { get; set; } = null!; - } + [Column("action")] + public string Action { get; set; } = null!; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs index 44e6379007..4d54752e75 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserStartNodeDto.cs @@ -1,67 +1,77 @@ -using System; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; -namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +[TableName(Constants.DatabaseSchema.Tables.UserStartNode)] +[PrimaryKey("id", AutoIncrement = true)] +[ExplicitColumns] +public class UserStartNodeDto : IEquatable { - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode)] - [PrimaryKey("id", AutoIncrement = true)] - [ExplicitColumns] - public class UserStartNodeDto : IEquatable + public enum StartNodeTypeValue { - [Column("id")] - [PrimaryKeyColumn(Name = "PK_userStartNode")] - public int Id { get; set; } - - [Column("userId")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(UserDto))] - public int UserId { get; set; } - - [Column("startNode")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [ForeignKey(typeof(NodeDto))] - public int StartNode { get; set; } - - [Column("startNodeType")] - [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", Name = "IX_umbracoUserStartNode_startNodeType")] - public int StartNodeType { get; set; } - - public enum StartNodeTypeValue - { - Content = 1, - Media = 2 - } - - public bool Equals(UserStartNodeDto? other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id == other.Id; - } - - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals((UserStartNodeDto) obj); - } - - public override int GetHashCode() - { - return Id; - } - - public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) - { - return Equals(left, right); - } - - public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) - { - return !Equals(left, right); - } + Content = 1, + Media = 2, } + + [Column("id")] + [PrimaryKeyColumn(Name = "PK_userStartNode")] + public int Id { get; set; } + + [Column("userId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(UserDto))] + public int UserId { get; set; } + + [Column("startNode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [ForeignKey(typeof(NodeDto))] + public int StartNode { get; set; } + + [Column("startNodeType")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "startNodeType, startNode, userId", Name = "IX_umbracoUserStartNode_startNodeType")] + public int StartNodeType { get; set; } + + public static bool operator ==(UserStartNodeDto left, UserStartNodeDto right) => Equals(left, right); + + public static bool operator !=(UserStartNodeDto left, UserStartNodeDto right) => !Equals(left, right); + + public bool Equals(UserStartNodeDto? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((UserStartNodeDto)obj); + } + + public override int GetHashCode() => Id; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs index 67c60ffb4d..297cea9025 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/AuditEntryFactory.cs @@ -1,52 +1,45 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class AuditEntryFactory { - internal static class AuditEntryFactory + public static IEnumerable BuildEntities(IEnumerable dtos) => + dtos.Select(BuildEntity).ToList(); + + public static IAuditEntry BuildEntity(AuditEntryDto dto) { - public static IEnumerable BuildEntities(IEnumerable dtos) + var entity = new AuditEntry { - return dtos.Select(BuildEntity).ToList(); - } + Id = dto.Id, + PerformingUserId = dto.PerformingUserId, + PerformingDetails = dto.PerformingDetails, + PerformingIp = dto.PerformingIp, + EventDateUtc = dto.EventDateUtc, + AffectedUserId = dto.AffectedUserId, + AffectedDetails = dto.AffectedDetails, + EventType = dto.EventType, + EventDetails = dto.EventDetails, + }; - public static IAuditEntry BuildEntity(AuditEntryDto dto) - { - var entity = new AuditEntry - { - Id = dto.Id, - PerformingUserId = dto.PerformingUserId, - PerformingDetails = dto.PerformingDetails, - PerformingIp = dto.PerformingIp, - EventDateUtc = dto.EventDateUtc, - AffectedUserId = dto.AffectedUserId, - AffectedDetails = dto.AffectedDetails, - EventType = dto.EventType, - EventDetails = dto.EventDetails - }; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - entity.ResetDirtyProperties(false); - return entity; - } - - public static AuditEntryDto BuildDto(IAuditEntry entity) - { - return new AuditEntryDto - { - Id = entity.Id, - PerformingUserId = entity.PerformingUserId, - PerformingDetails = entity.PerformingDetails, - PerformingIp = entity.PerformingIp, - EventDateUtc = entity.EventDateUtc, - AffectedUserId = entity.AffectedUserId, - AffectedDetails = entity.AffectedDetails, - EventType = entity.EventType, - EventDetails = entity.EventDetails - }; - } + // on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + entity.ResetDirtyProperties(false); + return entity; } + + public static AuditEntryDto BuildDto(IAuditEntry entity) => + new AuditEntryDto + { + Id = entity.Id, + PerformingUserId = entity.PerformingUserId, + PerformingDetails = entity.PerformingDetails, + PerformingIp = entity.PerformingIp, + EventDateUtc = entity.EventDateUtc, + AffectedUserId = entity.AffectedUserId, + AffectedDetails = entity.AffectedDetails, + EventType = entity.EventType, + EventDetails = entity.EventDetails, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs index 1a38348acf..bc33b15fd5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/CacheInstructionFactory.cs @@ -1,25 +1,23 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class CacheInstructionFactory { - internal static class CacheInstructionFactory - { - public static IEnumerable BuildEntities(IEnumerable dtos) => dtos.Select(BuildEntity).ToList(); + public static IEnumerable BuildEntities(IEnumerable dtos) => + dtos.Select(BuildEntity).ToList(); - public static CacheInstruction BuildEntity(CacheInstructionDto dto) => - new CacheInstruction(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount); + public static CacheInstruction BuildEntity(CacheInstructionDto dto) => + new(dto.Id, dto.UtcStamp, dto.Instructions, dto.OriginIdentity, dto.InstructionCount); - public static CacheInstructionDto BuildDto(CacheInstruction entity) => - new CacheInstructionDto - { - Id = entity.Id, - UtcStamp = entity.UtcStamp, - Instructions = entity.Instructions, - OriginIdentity = entity.OriginIdentity, - InstructionCount = entity.InstructionCount, - }; - } + public static CacheInstructionDto BuildDto(CacheInstruction entity) => + new() + { + Id = entity.Id, + UtcStamp = entity.UtcStamp, + Instructions = entity.Instructions, + OriginIdentity = entity.OriginIdentity, + InstructionCount = entity.InstructionCount, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs index 33f348a644..5e4035b0b8 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ConsentFactory.cs @@ -1,65 +1,64 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ConsentFactory { - internal static class ConsentFactory + public static IEnumerable BuildEntities(IEnumerable dtos) { - public static IEnumerable BuildEntities(IEnumerable dtos) + var ix = new Dictionary(); + var output = new List(); + + foreach (ConsentDto dto in dtos) { - var ix = new Dictionary(); - var output = new List(); + var k = dto.Source + "::" + dto.Context + "::" + dto.Action; - foreach (var dto in dtos) + var consent = new Consent { - var k = dto.Source + "::" + dto.Context + "::" + dto.Action; - - var consent = new Consent - { - Id = dto.Id, - Current = dto.Current, - CreateDate = dto.CreateDate, - Source = dto.Source, - Context = dto.Context, - Action = dto.Action, - State = (ConsentState) dto.State, // assume value is valid - Comment = dto.Comment - }; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - consent.ResetDirtyProperties(false); - - if (ix.TryGetValue(k, out var current)) - { - if (current.HistoryInternal == null) - current.HistoryInternal = new List(); - current.HistoryInternal.Add(consent); - } - else - { - ix[k] = consent; - output.Add(consent); - } - } - - return output; - } - - public static ConsentDto BuildDto(IConsent entity) - { - return new ConsentDto - { - Id = entity.Id, - Current = entity.Current, - CreateDate = entity.CreateDate, - Source = entity.Source, - Context = entity.Context, - Action = entity.Action, - State = (int) entity.State, - Comment = entity.Comment + Id = dto.Id, + Current = dto.Current, + CreateDate = dto.CreateDate, + Source = dto.Source, + Context = dto.Context, + Action = dto.Action, + State = (ConsentState)dto.State, // assume value is valid + Comment = dto.Comment, }; + + // on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + consent.ResetDirtyProperties(false); + + if (ix.TryGetValue(k, out Consent? current)) + { + if (current.HistoryInternal == null) + { + current.HistoryInternal = new List(); + } + + current.HistoryInternal.Add(consent); + } + else + { + ix[k] = consent; + output.Add(consent); + } } + + return output; } + + public static ConsentDto BuildDto(IConsent entity) => + new ConsentDto + { + Id = entity.Id, + Current = entity.Current, + CreateDate = entity.CreateDate, + Source = entity.Source, + Context = entity.Context, + Action = entity.Action, + State = (int)entity.State, + Comment = entity.Comment, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs index 5048474ee7..ad295505c4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs @@ -1,331 +1,320 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal class ContentBaseFactory { - internal class ContentBaseFactory + /// + /// Builds an IContent item from a dto and content type. + /// + public static Content BuildEntity(DocumentDto dto, IContentType? contentType) { - /// - /// Builds an IContent item from a dto and content type. - /// - public static Content BuildEntity(DocumentDto dto, IContentType? contentType) + ContentDto contentDto = dto.ContentDto; + NodeDto nodeDto = contentDto.NodeDto; + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; + ContentVersionDto contentVersionDto = documentVersionDto.ContentVersionDto; + DocumentVersionDto? publishedVersionDto = dto.PublishedVersionDto; + + var content = new Content(nodeDto.Text, nodeDto.ParentId, contentType); + + try { - var contentDto = dto.ContentDto; - var nodeDto = contentDto.NodeDto; - var documentVersionDto = dto.DocumentVersionDto; - var contentVersionDto = documentVersionDto.ContentVersionDto; - var publishedVersionDto = dto.PublishedVersionDto; + content.DisableChangeTracking(); - var content = new Content(nodeDto.Text, nodeDto.ParentId, contentType); + content.Id = dto.NodeId; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; - try + content.Name = contentVersionDto.Text; + + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; + + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; + + content.Published = dto.Published; + content.Edited = dto.Edited; + + // TODO: shall we get published infos or not? + // if (dto.Published) + if (publishedVersionDto != null) { - content.DisableChangeTracking(); - - content.Id = dto.NodeId; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; - - content.Name = contentVersionDto.Text; - - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; - - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; - - content.Published = dto.Published; - content.Edited = dto.Edited; - - // TODO: shall we get published infos or not? - //if (dto.Published) - if (publishedVersionDto != null) - { - content.PublishedVersionId = publishedVersionDto.Id; - content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; - content.PublishName = publishedVersionDto.ContentVersionDto.Text; - content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; - } - - // templates = ignored, managed by the repository - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); + content.PublishedVersionId = publishedVersionDto.Id; + content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; + content.PublishName = publishedVersionDto.ContentVersionDto.Text; + content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; } + + // templates = ignored, managed by the repository + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; } - - /// - /// Builds an IMedia item from a dto and content type. - /// - public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) + finally { - var nodeDto = dto.NodeDto; - var contentVersionDto = dto.ContentVersionDto; - - var content = new Core.Models.Media(nodeDto.Text, nodeDto.ParentId, contentType); - - try - { - content.DisableChangeTracking(); - - content.Id = dto.NodeId; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; - - // TODO: missing names? - - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; - - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); - } + content.EnableChangeTracking(); } + } - /// - /// Builds an IMedia item from a dto and content type. - /// - public static Member BuildEntity(MemberDto dto, IMemberType? contentType) + /// + /// Builds an IMedia item from a dto and content type. + /// + public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) + { + NodeDto nodeDto = dto.NodeDto; + ContentVersionDto contentVersionDto = dto.ContentVersionDto; + + var content = new Core.Models.Media(nodeDto.Text, nodeDto.ParentId, contentType); + + try { - var nodeDto = dto.ContentDto.NodeDto; - var contentVersionDto = dto.ContentVersionDto; + content.DisableChangeTracking(); - var content = new Member(nodeDto.Text, dto.Email, dto.LoginName, dto.Password, contentType); + content.Id = dto.NodeId; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; - try - { - content.DisableChangeTracking(); + // TODO: missing names? + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; - content.Id = dto.NodeId; - content.SecurityStamp = dto.SecurityStampToken; - content.EmailConfirmedDate = dto.EmailConfirmedDate; - content.PasswordConfiguration = dto.PasswordConfig; - content.Key = nodeDto.UniqueId; - content.VersionId = contentVersionDto.Id; + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; - // TODO: missing names? - - content.Path = nodeDto.Path; - content.Level = nodeDto.Level; - content.ParentId = nodeDto.ParentId; - content.SortOrder = nodeDto.SortOrder; - content.Trashed = nodeDto.Trashed; - - content.CreatorId = nodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; - content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; - content.IsLockedOut = dto.IsLockedOut; - content.IsApproved = dto.IsApproved; - content.LastLoginDate = dto.LastLoginDate; - content.LastLockoutDate = dto.LastLockoutDate; - content.LastPasswordChangeDate = dto.LastPasswordChangeDate; - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; } - - /// - /// Builds a dto from an IContent item. - /// - public static DocumentDto BuildDto(IContent entity, Guid objectType) + finally { - var contentDto = BuildContentDto(entity, objectType); - - var dto = new DocumentDto - { - NodeId = entity.Id, - Published = entity.Published, - ContentDto = contentDto, - DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) - }; - - return dto; + content.EnableChangeTracking(); } + } - public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ContentScheduleCollection contentSchedule, ILanguageRepository languageRepository) + /// + /// Builds an IMedia item from a dto and content type. + /// + public static Member BuildEntity(MemberDto dto, IMemberType? contentType) + { + NodeDto nodeDto = dto.ContentDto.NodeDto; + ContentVersionDto contentVersionDto = dto.ContentVersionDto; + + var content = new Member(nodeDto.Text, dto.Email, dto.LoginName, dto.Password, contentType); + + try { - return contentSchedule.FullSchedule.Select(x => - (x, new ContentScheduleDto + content.DisableChangeTracking(); + + content.Id = dto.NodeId; + content.SecurityStamp = dto.SecurityStampToken; + content.EmailConfirmedDate = dto.EmailConfirmedDate; + content.PasswordConfiguration = dto.PasswordConfig; + content.Key = nodeDto.UniqueId; + content.VersionId = contentVersionDto.Id; + + // TODO: missing names? + content.Path = nodeDto.Path; + content.Level = nodeDto.Level; + content.ParentId = nodeDto.ParentId; + content.SortOrder = nodeDto.SortOrder; + content.Trashed = nodeDto.Trashed; + + content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; + content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; + content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; + content.IsLockedOut = dto.IsLockedOut; + content.IsApproved = dto.IsApproved; + content.LastLoginDate = dto.LastLoginDate; + content.LastLockoutDate = dto.LastLockoutDate; + content.LastPasswordChangeDate = dto.LastPasswordChangeDate; + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally + { + content.EnableChangeTracking(); + } + } + + /// + /// Builds a dto from an IContent item. + /// + public static DocumentDto BuildDto(IContent entity, Guid objectType) + { + ContentDto contentDto = BuildContentDto(entity, objectType); + + var dto = new DocumentDto + { + NodeId = entity.Id, + Published = entity.Published, + ContentDto = contentDto, + DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto), + }; + + return dto; + } + + public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto( + IContent entity, + ContentScheduleCollection contentSchedule, + ILanguageRepository languageRepository) => + contentSchedule.FullSchedule.Select(x => + (x, + new ContentScheduleDto { Action = x.Action.ToString(), Date = x.Date, NodeId = entity.Id, LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), - Id = x.Id + Id = x.Id, })); - } - /// - /// Builds a dto from an IMedia item. - /// - public static MediaDto BuildDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity) + /// + /// Builds a dto from an IMedia item. + /// + public static MediaDto BuildDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity) + { + ContentDto contentDto = BuildContentDto(entity, Constants.ObjectTypes.Media); + + var dto = new MediaDto { - var contentDto = BuildContentDto(entity, Cms.Core.Constants.ObjectTypes.Media); + NodeId = entity.Id, + ContentDto = contentDto, + MediaVersionDto = BuildMediaVersionDto(mediaUrlGenerators, entity, contentDto), + }; - var dto = new MediaDto - { - NodeId = entity.Id, - ContentDto = contentDto, - MediaVersionDto = BuildMediaVersionDto(mediaUrlGenerators, entity, contentDto) - }; + return dto; + } - return dto; - } + /// + /// Builds a dto from an IMember item. + /// + public static MemberDto BuildDto(IMember entity) + { + ContentDto contentDto = BuildContentDto(entity, Constants.ObjectTypes.Member); - /// - /// Builds a dto from an IMember item. - /// - public static MemberDto BuildDto(IMember entity) + var dto = new MemberDto { - var contentDto = BuildContentDto(entity, Cms.Core.Constants.ObjectTypes.Member); + Email = entity.Email, + LoginName = entity.Username, + NodeId = entity.Id, + Password = entity.RawPasswordValue, + SecurityStampToken = entity.SecurityStamp, + EmailConfirmedDate = entity.EmailConfirmedDate, + ContentDto = contentDto, + ContentVersionDto = BuildContentVersionDto(entity, contentDto), + PasswordConfig = entity.PasswordConfiguration, + FailedPasswordAttempts = entity.FailedPasswordAttempts, + IsApproved = entity.IsApproved, + IsLockedOut = entity.IsLockedOut, + LastLockoutDate = entity.LastLockoutDate, + LastLoginDate = entity.LastLoginDate, + LastPasswordChangeDate = entity.LastPasswordChangeDate, + }; + return dto; + } - var dto = new MemberDto - { - Email = entity.Email, - LoginName = entity.Username, - NodeId = entity.Id, - Password = entity.RawPasswordValue, - SecurityStampToken = entity.SecurityStamp, - EmailConfirmedDate = entity.EmailConfirmedDate, - ContentDto = contentDto, - ContentVersionDto = BuildContentVersionDto(entity, contentDto), - PasswordConfig = entity.PasswordConfiguration, - FailedPasswordAttempts = entity.FailedPasswordAttempts, - IsApproved = entity.IsApproved, - IsLockedOut = entity.IsLockedOut, - LastLockoutDate = entity.LastLockoutDate, - LastLoginDate = entity.LastLoginDate, - LastPasswordChangeDate = entity.LastPasswordChangeDate, - }; - return dto; - } - - private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) + private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) + { + var dto = new ContentDto { - var dto = new ContentDto - { - NodeId = entity.Id, - ContentTypeId = entity.ContentTypeId, + NodeId = entity.Id, ContentTypeId = entity.ContentTypeId, NodeDto = BuildNodeDto(entity, objectType), + }; - NodeDto = BuildNodeDto(entity, objectType) - }; + return dto; + } - return dto; - } - - private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) + private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) + { + var dto = new NodeDto { - var dto = new NodeDto - { - NodeId = entity.Id, - UniqueId = entity.Key, - ParentId = entity.ParentId, - Level = Convert.ToInt16(entity.Level), - Path = entity.Path, - SortOrder = entity.SortOrder, - Trashed = entity.Trashed, - UserId = entity.CreatorId, - Text = entity.Name, - NodeObjectType = objectType, - CreateDate = entity.CreateDate - }; + NodeId = entity.Id, + UniqueId = entity.Key, + ParentId = entity.ParentId, + Level = Convert.ToInt16(entity.Level), + Path = entity.Path, + SortOrder = entity.SortOrder, + Trashed = entity.Trashed, + UserId = entity.CreatorId, + Text = entity.Name, + NodeObjectType = objectType, + CreateDate = entity.CreateDate, + }; - return dto; - } + return dto; + } - // always build the current / VersionPk dto - // we're never going to build / save old versions (which are immutable) - private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) + // always build the current / VersionPk dto + // we're never going to build / save old versions (which are immutable) + private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) + { + var dto = new ContentVersionDto { - var dto = new ContentVersionDto - { - Id = entity.VersionId, - NodeId = entity.Id, - VersionDate = entity.UpdateDate, - UserId = entity.WriterId, - Current = true, // always building the current one - Text = entity.Name, + Id = entity.VersionId, + NodeId = entity.Id, + VersionDate = entity.UpdateDate, + UserId = entity.WriterId, + Current = true, // always building the current one + Text = entity.Name, + ContentDto = contentDto, + }; - ContentDto = contentDto - }; + return dto; + } - return dto; - } - - // always build the current / VersionPk dto - // we're never going to build / save old versions (which are immutable) - private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) + // always build the current / VersionPk dto + // we're never going to build / save old versions (which are immutable) + private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) + { + var dto = new DocumentVersionDto { - var dto = new DocumentVersionDto - { - Id = entity.VersionId, - TemplateId = entity.TemplateId, - Published = false, // always building the current, unpublished one + Id = entity.VersionId, + TemplateId = entity.TemplateId, + Published = false, // always building the current, unpublished one - ContentVersionDto = BuildContentVersionDto(entity, contentDto) - }; + ContentVersionDto = BuildContentVersionDto(entity, contentDto), + }; - return dto; - } + return dto; + } - private static MediaVersionDto BuildMediaVersionDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity, ContentDto contentDto) + private static MediaVersionDto BuildMediaVersionDto(MediaUrlGeneratorCollection mediaUrlGenerators, IMedia entity, ContentDto contentDto) + { + // try to get a path from the string being stored for media + // TODO: only considering umbracoFile + string? path = null; + + if (entity.Properties.TryGetValue(Constants.Conventions.Media.File, out IProperty? property) + && mediaUrlGenerators.TryGetMediaPath(property.PropertyType.PropertyEditorAlias, property.GetValue(), out var mediaPath)) { - // try to get a path from the string being stored for media - // TODO: only considering umbracoFile - - string? path = null; - - if (entity.Properties.TryGetValue(Cms.Core.Constants.Conventions.Media.File, out var property) - && mediaUrlGenerators.TryGetMediaPath(property.PropertyType.PropertyEditorAlias, property.GetValue(), out var mediaPath)) - { - path = mediaPath; - } - - var dto = new MediaVersionDto - { - Id = entity.VersionId, - Path = path, - - ContentVersionDto = BuildContentVersionDto(entity, contentDto) - }; - - return dto; + path = mediaPath; } + + var dto = new MediaVersionDto + { + Id = entity.VersionId, Path = path, ContentVersionDto = BuildContentVersionDto(entity, contentDto), + }; + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs index a3a1deb62d..095f338399 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentTypeFactory.cs @@ -1,180 +1,188 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +// factory for +// IContentType (document types) +// IMediaType (media types) +// IMemberType (member types) +// +internal static class ContentTypeFactory { - // factory for - // IContentType (document types) - // IMediaType (media types) - // IMemberType (member types) - // - internal static class ContentTypeFactory + #region IContentType + + public static IContentType BuildContentTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) { - #region IContentType + var contentType = new ContentType(shortStringHelper, dto.NodeDto.ParentId); - public static IContentType BuildContentTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + try { - var contentType = new ContentType(shortStringHelper, dto.NodeDto.ParentId); + contentType.DisableChangeTracking(); - try - { - contentType.DisableChangeTracking(); - - BuildCommonEntity(contentType, dto); - - // reset dirty initial properties (U4-1946) - contentType.ResetDirtyProperties(false); - return contentType; - } - finally - { - contentType.EnableChangeTracking(); - } - } - - #endregion - - #region IMediaType - - public static IMediaType BuildMediaTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) - { - var contentType = new MediaType(shortStringHelper, dto.NodeDto.ParentId); - try - { - contentType.DisableChangeTracking(); - - BuildCommonEntity(contentType, dto); - - // reset dirty initial properties (U4-1946) - contentType.ResetDirtyProperties(false); - } - finally - { - contentType.EnableChangeTracking(); - } + BuildCommonEntity(contentType, dto); + // reset dirty initial properties (U4-1946) + contentType.ResetDirtyProperties(false); return contentType; } - - #endregion - - #region IMemberType - - public static IMemberType BuildMemberTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + finally { - var contentType = new MemberType(shortStringHelper, dto.NodeDto.ParentId); - try - { - contentType.DisableChangeTracking(); - BuildCommonEntity(contentType, dto, false); - contentType.ResetDirtyProperties(false); - } - finally - { - contentType.EnableChangeTracking(); - } - - return contentType; + contentType.EnableChangeTracking(); } - - public static IEnumerable BuildMemberPropertyTypeDtos(IMemberType entity) - { - var memberType = entity as MemberType; - if (memberType == null || memberType.PropertyTypes.Any() == false) - return Enumerable.Empty(); - - var dtos = memberType.PropertyTypes.Select(x => new MemberPropertyTypeDto - { - NodeId = entity.Id, - PropertyTypeId = x.Id, - CanEdit = memberType.MemberCanEditProperty(x.Alias), - ViewOnProfile = memberType.MemberCanViewProperty(x.Alias), - IsSensitive = memberType.IsSensitiveProperty(x.Alias) - }).ToList(); - return dtos; - } - - #endregion - - #region Common - - private static void BuildCommonEntity(ContentTypeBase entity, ContentTypeDto dto, bool setVariations = true) - { - entity.Id = dto.NodeDto.NodeId; - entity.Key = dto.NodeDto.UniqueId; - entity.Alias = dto.Alias ?? string.Empty; - entity.Name = dto.NodeDto.Text; - entity.Icon = dto.Icon; - entity.Thumbnail = dto.Thumbnail; - entity.SortOrder = dto.NodeDto.SortOrder; - entity.Description = dto.Description; - entity.CreateDate = dto.NodeDto.CreateDate; - entity.UpdateDate = dto.NodeDto.CreateDate; - entity.Path = dto.NodeDto.Path; - entity.Level = dto.NodeDto.Level; - entity.CreatorId = dto.NodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - entity.AllowedAsRoot = dto.AllowAtRoot; - entity.IsContainer = dto.IsContainer; - entity.IsElement = dto.IsElement; - entity.Trashed = dto.NodeDto.Trashed; - - if (setVariations) - entity.Variations = (ContentVariation) dto.Variations; - } - - public static ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) - { - Guid nodeObjectType; - if (entity is IContentType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.DocumentType; - else if (entity is IMediaType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.MediaType; - else if (entity is IMemberType) - nodeObjectType = Cms.Core.Constants.ObjectTypes.MemberType; - else - throw new Exception("Invalid entity."); - - var contentTypeDto = new ContentTypeDto - { - Alias = entity.Alias, - Description = entity.Description, - Icon = entity.Icon, - Thumbnail = entity.Thumbnail, - NodeId = entity.Id, - AllowAtRoot = entity.AllowedAsRoot, - IsContainer = entity.IsContainer, - IsElement = entity.IsElement, - Variations = (byte) entity.Variations, - NodeDto = BuildNodeDto(entity, nodeObjectType) - }; - return contentTypeDto; - } - - private static NodeDto BuildNodeDto(IUmbracoEntity entity, Guid nodeObjectType) - { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeObjectType = nodeObjectType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - return nodeDto; - } - - #endregion } + + #endregion + + #region IMediaType + + public static IMediaType BuildMediaTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + { + var contentType = new MediaType(shortStringHelper, dto.NodeDto.ParentId); + try + { + contentType.DisableChangeTracking(); + + BuildCommonEntity(contentType, dto); + + // reset dirty initial properties (U4-1946) + contentType.ResetDirtyProperties(false); + } + finally + { + contentType.EnableChangeTracking(); + } + + return contentType; + } + + #endregion + + #region IMemberType + + public static IMemberType BuildMemberTypeEntity(IShortStringHelper shortStringHelper, ContentTypeDto dto) + { + var contentType = new MemberType(shortStringHelper, dto.NodeDto.ParentId); + try + { + contentType.DisableChangeTracking(); + BuildCommonEntity(contentType, dto, false); + contentType.ResetDirtyProperties(false); + } + finally + { + contentType.EnableChangeTracking(); + } + + return contentType; + } + + public static IEnumerable BuildMemberPropertyTypeDtos(IMemberType entity) + { + if (entity is not MemberType memberType || memberType.PropertyTypes.Any() == false) + { + return Enumerable.Empty(); + } + + var dtos = memberType.PropertyTypes.Select(x => new MemberPropertyTypeDto + { + NodeId = entity.Id, + PropertyTypeId = x.Id, + CanEdit = memberType.MemberCanEditProperty(x.Alias), + ViewOnProfile = memberType.MemberCanViewProperty(x.Alias), + IsSensitive = memberType.IsSensitiveProperty(x.Alias), + }).ToList(); + return dtos; + } + + public static ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) + { + Guid nodeObjectType; + if (entity is IContentType) + { + nodeObjectType = Constants.ObjectTypes.DocumentType; + } + else if (entity is IMediaType) + { + nodeObjectType = Constants.ObjectTypes.MediaType; + } + else if (entity is IMemberType) + { + nodeObjectType = Constants.ObjectTypes.MemberType; + } + else + { + throw new Exception("Invalid entity."); + } + + var contentTypeDto = new ContentTypeDto + { + Alias = entity.Alias, + Description = entity.Description, + Icon = entity.Icon, + Thumbnail = entity.Thumbnail, + NodeId = entity.Id, + AllowAtRoot = entity.AllowedAsRoot, + IsContainer = entity.IsContainer, + IsElement = entity.IsElement, + Variations = (byte)entity.Variations, + NodeDto = BuildNodeDto(entity, nodeObjectType), + }; + return contentTypeDto; + } + + #endregion + + #region Common + + private static void BuildCommonEntity(ContentTypeBase entity, ContentTypeDto dto, bool setVariations = true) + { + entity.Id = dto.NodeDto.NodeId; + entity.Key = dto.NodeDto.UniqueId; + entity.Alias = dto.Alias ?? string.Empty; + entity.Name = dto.NodeDto.Text; + entity.Icon = dto.Icon; + entity.Thumbnail = dto.Thumbnail; + entity.SortOrder = dto.NodeDto.SortOrder; + entity.Description = dto.Description; + entity.CreateDate = dto.NodeDto.CreateDate; + entity.UpdateDate = dto.NodeDto.CreateDate; + entity.Path = dto.NodeDto.Path; + entity.Level = dto.NodeDto.Level; + entity.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; + entity.AllowedAsRoot = dto.AllowAtRoot; + entity.IsContainer = dto.IsContainer; + entity.IsElement = dto.IsElement; + entity.Trashed = dto.NodeDto.Trashed; + + if (setVariations) + { + entity.Variations = (ContentVariation)dto.Variations; + } + } + + private static NodeDto BuildNodeDto(IUmbracoEntity entity, Guid nodeObjectType) + { + var nodeDto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), + NodeObjectType = nodeObjectType, + ParentId = entity.ParentId, + Path = entity.Path, + SortOrder = entity.SortOrder, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key, + UserId = entity.CreatorId, + }; + return nodeDto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs index df655d3ade..69862364de 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DataTypeFactory.cs @@ -1,91 +1,90 @@ -using System; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DataTypeFactory { - internal static class DataTypeFactory + public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, IConfigurationEditorJsonSerializer serializer) { - public static IDataType BuildEntity(DataTypeDto dto, PropertyEditorCollection editors, ILogger logger, IConfigurationEditorJsonSerializer serializer) + // Check we have an editor for the data type. + if (!editors.TryGet(dto.EditorAlias, out IDataEditor? editor)) { - // Check we have an editor for the data type. - if (!editors.TryGet(dto.EditorAlias, out var editor)) - { - logger.LogWarning("Could not find an editor with alias {EditorAlias}, treating as Label. " + - "The site may fail to boot and/or load data types and run.", dto.EditorAlias); + logger.LogWarning( + "Could not find an editor with alias {EditorAlias}, treating as Label. " + "The site may fail to boot and/or load data types and run.", dto.EditorAlias); - // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear - // the situation to the user. - editor = new MissingPropertyEditor(); - } - - var dataType = new DataType(editor, serializer); - - try - { - dataType.DisableChangeTracking(); - - dataType.CreateDate = dto.NodeDto.CreateDate; - dataType.DatabaseType = dto.DbType.EnumParse(true); - dataType.Id = dto.NodeId; - dataType.Key = dto.NodeDto.UniqueId; - dataType.Level = dto.NodeDto.Level; - dataType.UpdateDate = dto.NodeDto.CreateDate; - dataType.Name = dto.NodeDto.Text; - dataType.ParentId = dto.NodeDto.ParentId; - dataType.Path = dto.NodeDto.Path; - dataType.SortOrder = dto.NodeDto.SortOrder; - dataType.Trashed = dto.NodeDto.Trashed; - dataType.CreatorId = dto.NodeDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - - dataType.SetLazyConfiguration(dto.Configuration); - - // reset dirty initial properties (U4-1946) - dataType.ResetDirtyProperties(false); - return dataType; - } - finally - { - dataType.EnableChangeTracking(); - } + // Create as special type, which downstream can be handled by converting to a LabelPropertyEditor to make clear + // the situation to the user. + editor = new MissingPropertyEditor(); } - public static DataTypeDto BuildDto(IDataType entity, IConfigurationEditorJsonSerializer serializer) - { - var dataTypeDto = new DataTypeDto - { - EditorAlias = entity.EditorAlias, - NodeId = entity.Id, - DbType = entity.DatabaseType.ToString(), - Configuration = ConfigurationEditor.ToDatabase(entity.Configuration, serializer), - NodeDto = BuildNodeDto(entity) - }; + var dataType = new DataType(editor, serializer); - return dataTypeDto; + try + { + dataType.DisableChangeTracking(); + + dataType.CreateDate = dto.NodeDto.CreateDate; + dataType.DatabaseType = dto.DbType.EnumParse(true); + dataType.Id = dto.NodeId; + dataType.Key = dto.NodeDto.UniqueId; + dataType.Level = dto.NodeDto.Level; + dataType.UpdateDate = dto.NodeDto.CreateDate; + dataType.Name = dto.NodeDto.Text; + dataType.ParentId = dto.NodeDto.ParentId; + dataType.Path = dto.NodeDto.Path; + dataType.SortOrder = dto.NodeDto.SortOrder; + dataType.Trashed = dto.NodeDto.Trashed; + dataType.CreatorId = dto.NodeDto.UserId ?? Constants.Security.UnknownUserId; + + dataType.SetLazyConfiguration(dto.Configuration); + + // reset dirty initial properties (U4-1946) + dataType.ResetDirtyProperties(false); + return dataType; } - - private static NodeDto BuildNodeDto(IDataType entity) + finally { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = Convert.ToInt16(entity.Level), - NodeObjectType = Cms.Core.Constants.ObjectTypes.DataType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = entity.Trashed, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - - return nodeDto; + dataType.EnableChangeTracking(); } } + + public static DataTypeDto BuildDto(IDataType entity, IConfigurationEditorJsonSerializer serializer) + { + var dataTypeDto = new DataTypeDto + { + EditorAlias = entity.EditorAlias, + NodeId = entity.Id, + DbType = entity.DatabaseType.ToString(), + Configuration = ConfigurationEditor.ToDatabase(entity.Configuration, serializer), + NodeDto = BuildNodeDto(entity), + }; + + return dataTypeDto; + } + + private static NodeDto BuildNodeDto(IDataType entity) + { + var nodeDto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = Convert.ToInt16(entity.Level), + NodeObjectType = Constants.ObjectTypes.DataType, + ParentId = entity.ParentId, + Path = entity.Path, + SortOrder = entity.SortOrder, + Text = entity.Name, + Trashed = entity.Trashed, + UniqueId = entity.Key, + UserId = entity.CreatorId, + }; + + return nodeDto; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs index 31dc7ef2ec..5a82c3be01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryItemFactory.cs @@ -1,70 +1,68 @@ -using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DictionaryItemFactory { - internal static class DictionaryItemFactory + #region Implementation of IEntityFactory + + public static IDictionaryItem BuildEntity(DictionaryDto dto) { - #region Implementation of IEntityFactory + var item = new DictionaryItem(dto.Parent, dto.Key); - public static IDictionaryItem BuildEntity(DictionaryDto dto) + try { - var item = new DictionaryItem(dto.Parent, dto.Key); + item.DisableChangeTracking(); - try - { - item.DisableChangeTracking(); + item.Id = dto.PrimaryKey; + item.Key = dto.UniqueId; - item.Id = dto.PrimaryKey; - item.Key = dto.UniqueId; - - // reset dirty initial properties (U4-1946) - item.ResetDirtyProperties(false); - return item; - } - finally - { - item.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + item.ResetDirtyProperties(false); + return item; } - - public static DictionaryDto BuildDto(IDictionaryItem entity) + finally { - return new DictionaryDto - { - UniqueId = entity.Key, - Key = entity.ItemKey, - Parent = entity.ParentId, - PrimaryKey = entity.Id, - LanguageTextDtos = BuildLanguageTextDtos(entity) - }; - } - - #endregion - - private static List BuildLanguageTextDtos(IDictionaryItem entity) - { - var list = new List(); - if (entity.Translations is not null) - { - foreach (var translation in entity.Translations) - { - var text = new LanguageTextDto - { - LanguageId = translation.LanguageId, - UniqueId = translation.Key, - Value = translation.Value!, - }; - - if (translation.HasIdentity) - text.PrimaryKey = translation.Id; - - list.Add(text); - } - } - - return list; + item.EnableChangeTracking(); } } + + private static List BuildLanguageTextDtos(IDictionaryItem entity) + { + var list = new List(); + if (entity.Translations is not null) + { + foreach (IDictionaryTranslation translation in entity.Translations) + { + var text = new LanguageTextDto + { + LanguageId = translation.LanguageId, + UniqueId = translation.Key, + Value = translation.Value, + }; + + if (translation.HasIdentity) + { + text.PrimaryKey = translation.Id; + } + + list.Add(text); + } + } + + return list; + } + + public static DictionaryDto BuildDto(IDictionaryItem entity) => + new DictionaryDto + { + UniqueId = entity.Key, + Key = entity.ItemKey, + Parent = entity.ParentId, + PrimaryKey = entity.Id, + LanguageTextDtos = BuildLanguageTextDtos(entity), + }; + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs index a53222ad5e..a06adb5c34 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/DictionaryTranslationFactory.cs @@ -1,48 +1,43 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class DictionaryTranslationFactory { - internal static class DictionaryTranslationFactory + #region Implementation of IEntityFactory + + public static IDictionaryTranslation BuildEntity(LanguageTextDto dto, Guid uniqueId) { - #region Implementation of IEntityFactory + var item = new DictionaryTranslation(dto.LanguageId, dto.Value, uniqueId); - public static IDictionaryTranslation BuildEntity(LanguageTextDto dto, Guid uniqueId) + try { - var item = new DictionaryTranslation(dto.LanguageId, dto.Value, uniqueId); + item.DisableChangeTracking(); - try - { - item.DisableChangeTracking(); + item.Id = dto.PrimaryKey; - item.Id = dto.PrimaryKey; - - // reset dirty initial properties (U4-1946) - item.ResetDirtyProperties(false); - return item; - } - finally - { - item.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + item.ResetDirtyProperties(false); + return item; } - - public static LanguageTextDto BuildDto(IDictionaryTranslation entity, Guid uniqueId) + finally { - var text = new LanguageTextDto - { - LanguageId = entity.LanguageId, - UniqueId = uniqueId, - Value = entity.Value - }; - - if (entity.HasIdentity) - text.PrimaryKey = entity.Id; - - return text; + item.EnableChangeTracking(); } - - #endregion } + + public static LanguageTextDto BuildDto(IDictionaryTranslation entity, Guid uniqueId) + { + var text = new LanguageTextDto { LanguageId = entity.LanguageId, UniqueId = uniqueId, Value = entity.Value }; + + if (entity.HasIdentity) + { + text.PrimaryKey = entity.Id; + } + + return text; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 1c74dcb8bd..77ab4ed404 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -1,81 +1,78 @@ -using System; -using System.Globalization; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ExternalLoginFactory { - internal static class ExternalLoginFactory + public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto) { - public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto) - { - var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate); + var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate); - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) - { + public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) + { + // If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state. + // At this point we have to manually set the key, to ensure external logins can be used to upgrade + var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString(); - //If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state. - //At this point we have to manually set the key, to ensure external logins can be used to upgrade - var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString(); - - var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate) + var entity = + new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate) { - UserData = dto.UserData + UserData = dto.UserData, }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) + public static ExternalLoginDto BuildDto(IIdentityUserLogin entity) + { + var dto = new ExternalLoginDto { - var dto = new ExternalLoginDto - { - Id = entity.Id, - CreateDate = entity.CreateDate, - LoginProvider = entity.LoginProvider, - ProviderKey = entity.ProviderKey, - UserOrMemberKey = entity.Key, - UserData = entity.UserData - }; + Id = entity.Id, + CreateDate = entity.CreateDate, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserOrMemberKey = entity.Key, + UserData = entity.UserData, + }; - return dto; - } + return dto; + } - public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null) + public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null) + { + var dto = new ExternalLoginDto { - var dto = new ExternalLoginDto - { - Id = id ?? default, - UserOrMemberKey = userOrMemberKey, - LoginProvider = entity.LoginProvider, - ProviderKey = entity.ProviderKey, - UserData = entity.UserData, - CreateDate = DateTime.Now - }; + Id = id ?? default, + UserOrMemberKey = userOrMemberKey, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserData = entity.UserData, + CreateDate = DateTime.Now, + }; - return dto; - } + return dto; + } - public static ExternalLoginTokenDto BuildDto(int externalLoginId, IExternalLoginToken token, int? id = null) + public static ExternalLoginTokenDto BuildDto(int externalLoginId, IExternalLoginToken token, int? id = null) + { + var dto = new ExternalLoginTokenDto { - var dto = new ExternalLoginTokenDto - { - Id = id ?? default, - ExternalLoginId = externalLoginId, - Name = token.Name, - Value = token.Value, - CreateDate = DateTime.Now - }; + Id = id ?? default, + ExternalLoginId = externalLoginId, + Name = token.Name, + Value = token.Value, + CreateDate = DateTime.Now, + }; - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs index 2c7c6c081e..7948164280 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs @@ -1,51 +1,50 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class LanguageFactory { - internal static class LanguageFactory + public static ILanguage BuildEntity(LanguageDto dto) { - public static ILanguage BuildEntity(LanguageDto dto) + ArgumentNullException.ThrowIfNull(dto); + if (dto.IsoCode == null || dto.CultureName == null) { - ArgumentNullException.ThrowIfNull(dto); - if (dto.IsoCode == null || dto.CultureName == null) - { - throw new InvalidOperationException("Language ISO code and/or culture name can't be null."); - } - - var lang = new Language(dto.IsoCode, dto.CultureName) - { - Id = dto.Id, - IsDefault = dto.IsDefault, - IsMandatory = dto.IsMandatory, - FallbackLanguageId = dto.FallbackLanguageId - }; - - // Reset dirty initial properties - lang.ResetDirtyProperties(false); - - return lang; + throw new InvalidOperationException("Language ISO code and/or culture name can't be null."); } - public static LanguageDto BuildDto(ILanguage entity) + var lang = new Language(dto.IsoCode, dto.CultureName) { - ArgumentNullException.ThrowIfNull(entity); + Id = dto.Id, + IsDefault = dto.IsDefault, + IsMandatory = dto.IsMandatory, + FallbackLanguageId = dto.FallbackLanguageId, + }; - var dto = new LanguageDto - { - IsoCode = entity.IsoCode, - CultureName = entity.CultureName, - IsDefault = entity.IsDefault, - IsMandatory = entity.IsMandatory, - FallbackLanguageId = entity.FallbackLanguageId - }; + // Reset dirty initial properties + lang.ResetDirtyProperties(false); - if (entity.HasIdentity) - { - dto.Id = (short)entity.Id; - } + return lang; + } - return dto; + public static LanguageDto BuildDto(ILanguage entity) + { + ArgumentNullException.ThrowIfNull(entity); + + var dto = new LanguageDto + { + IsoCode = entity.IsoCode, + CultureName = entity.CultureName, + IsDefault = entity.IsDefault, + IsMandatory = entity.IsMandatory, + FallbackLanguageId = entity.FallbackLanguageId, + }; + + if (entity.HasIdentity) + { + dto.Id = (short)entity.Id; } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs index 7f73abacaa..3725a2be6a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/MacroFactory.cs @@ -1,79 +1,80 @@ -using System.Collections.Generic; -using System.Globalization; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class MacroFactory { - internal static class MacroFactory + public static IMacro BuildEntity(IShortStringHelper shortStringHelper, MacroDto dto) { - public static IMacro BuildEntity(IShortStringHelper shortStringHelper, MacroDto dto) + var model = new Macro(shortStringHelper, dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, + dto.Name, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroSource); + + try { - var model = new Macro(shortStringHelper, dto.Id, dto.UniqueId, dto.UseInEditor, dto.RefreshRate, dto.Alias, dto.Name, dto.CacheByPage, dto.CachePersonalized, dto.DontRender, dto.MacroSource); + model.DisableChangeTracking(); - try + foreach (MacroPropertyDto p in dto.MacroPropertyDtos.EmptyNull()) { - model.DisableChangeTracking(); - - foreach (var p in dto.MacroPropertyDtos.EmptyNull()) - { - model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); - } - - // reset dirty initial properties (U4-1946) - model.ResetDirtyProperties(false); - return model; - } - finally - { - model.EnableChangeTracking(); + model.Properties.Add(new MacroProperty(p.Id, p.UniqueId, p.Alias, p.Name, p.SortOrder, p.EditorAlias)); } + + // reset dirty initial properties (U4-1946) + model.ResetDirtyProperties(false); + return model; } - - public static MacroDto BuildDto(IMacro entity) + finally { - var dto = new MacroDto - { - UniqueId = entity.Key, - Alias = entity.Alias, - CacheByPage = entity.CacheByPage, - CachePersonalized = entity.CacheByMember, - DontRender = entity.DontRender, - Name = entity.Name, - MacroSource = entity.MacroSource, - RefreshRate = entity.CacheDuration, - UseInEditor = entity.UseInEditor, - MacroPropertyDtos = BuildPropertyDtos(entity), - MacroType = 7 //PartialView - }; - - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; - } - - private static List BuildPropertyDtos(IMacro entity) - { - var list = new List(); - foreach (var p in entity.Properties) - { - var text = new MacroPropertyDto - { - UniqueId = p.Key, - Alias = p.Alias, - Name = p.Name, - Macro = entity.Id, - SortOrder = (byte)p.SortOrder, - EditorAlias = p.EditorAlias, - Id = p.Id - }; - - list.Add(text); - } - return list; + model.EnableChangeTracking(); } } + + public static MacroDto BuildDto(IMacro entity) + { + var dto = new MacroDto + { + UniqueId = entity.Key, + Alias = entity.Alias, + CacheByPage = entity.CacheByPage, + CachePersonalized = entity.CacheByMember, + DontRender = entity.DontRender, + Name = entity.Name, + MacroSource = entity.MacroSource, + RefreshRate = entity.CacheDuration, + UseInEditor = entity.UseInEditor, + MacroPropertyDtos = BuildPropertyDtos(entity), + MacroType = 7, // PartialView + }; + + if (entity.HasIdentity) + { + dto.Id = entity.Id; + } + + return dto; + } + + private static List BuildPropertyDtos(IMacro entity) + { + var list = new List(); + foreach (IMacroProperty p in entity.Properties) + { + var text = new MacroPropertyDto + { + UniqueId = p.Key, + Alias = p.Alias, + Name = p.Name, + Macro = entity.Id, + SortOrder = (byte)p.SortOrder, + EditorAlias = p.EditorAlias, + Id = p.Id, + }; + + list.Add(text); + } + + return list; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs index d3ddf40ce3..b6ecbe3b6f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/MemberGroupFactory.cs @@ -1,71 +1,65 @@ -using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class MemberGroupFactory { - internal static class MemberGroupFactory + private static readonly Guid _nodeObjectTypeId; + + static MemberGroupFactory() => _nodeObjectTypeId = Constants.ObjectTypes.MemberGroup; + + #region Implementation of IEntityFactory + + public static IMemberGroup BuildEntity(NodeDto dto) { + var group = new MemberGroup(); - private static readonly Guid _nodeObjectTypeId; - - static MemberGroupFactory() + try { - _nodeObjectTypeId = Cms.Core.Constants.ObjectTypes.MemberGroup; + group.DisableChangeTracking(); + + group.CreateDate = dto.CreateDate; + group.Id = dto.NodeId; + group.Key = dto.UniqueId; + group.Name = dto.Text; + + // reset dirty initial properties (U4-1946) + group.ResetDirtyProperties(false); + return group; } - - #region Implementation of IEntityFactory - - public static IMemberGroup BuildEntity(NodeDto dto) + finally { - var group = new MemberGroup(); - - try - { - group.DisableChangeTracking(); - - group.CreateDate = dto.CreateDate; - group.Id = dto.NodeId; - group.Key = dto.UniqueId; - group.Name = dto.Text; - - // reset dirty initial properties (U4-1946) - group.ResetDirtyProperties(false); - return group; - } - finally - { - group.EnableChangeTracking(); - } + group.EnableChangeTracking(); } - - public static NodeDto BuildDto(IMemberGroup entity) - { - var dto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = 0, - NodeObjectType = _nodeObjectTypeId, - ParentId = -1, - Path = "", - SortOrder = 0, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - - if (entity.HasIdentity) - { - dto.NodeId = entity.Id; - dto.Path = "-1," + entity.Id; - } - - return dto; - } - - #endregion - } + + public static NodeDto BuildDto(IMemberGroup entity) + { + var dto = new NodeDto + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = 0, + NodeObjectType = _nodeObjectTypeId, + ParentId = -1, + Path = string.Empty, + SortOrder = 0, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key, + UserId = entity.CreatorId, + }; + + if (entity.HasIdentity) + { + dto.NodeId = entity.Id; + dto.Path = "-1," + entity.Id; + } + + return dto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs index 1b8708c640..73e6de29a6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyFactory.cs @@ -1,193 +1,220 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PropertyFactory { - internal static class PropertyFactory + public static IEnumerable BuildEntities(IPropertyType[]? propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) { - public static IEnumerable BuildEntities(IPropertyType[]? propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) + var properties = new List(); + var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); + + if (propertyTypes is null) { - var properties = new List(); - var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); - - if (propertyTypes is null) - { - return properties; - } - foreach (var propertyType in propertyTypes) - { - var values = new List(); - int propertyId = default; - - // see notes in BuildDtos - we always have edit+published dtos - if (xdtos.TryGetValue(propertyType.Id, out var propDtos)) - { - foreach (var propDto in propDtos) - { - propertyId = propDto.Id; - values.Add(new Property.InitialPropertyValue(languageRepository.GetIsoCodeById(propDto.LanguageId), propDto.Segment, propDto.VersionId == publishedVersionId, propDto.Value)); - } - } - - var property = Property.CreateWithValues(propertyId, propertyType, values.ToArray()); - properties.Add(property); - } - return properties; } - private static PropertyDataDto BuildDto(int versionId, IProperty property, int? languageId, string? segment, object? value) + foreach (IPropertyType propertyType in propertyTypes) { - var dto = new PropertyDataDto { VersionId = versionId, PropertyTypeId = property.PropertyTypeId }; + var values = new List(); + int propertyId = default; - if (languageId.HasValue) - dto.LanguageId = languageId; - - if (segment != null) - dto.Segment = segment; - - if (property.ValueStorageType == ValueStorageType.Integer) + // see notes in BuildDtos - we always have edit+published dtos + if (xdtos.TryGetValue(propertyType.Id, out IEnumerable? propDtos)) { - if (value is bool || property.PropertyType.PropertyEditorAlias == Cms.Core.Constants.PropertyEditors.Aliases.Boolean) + foreach (PropertyDataDto propDto in propDtos) { - dto.IntegerValue = value != null && string.IsNullOrEmpty(value.ToString()) ? 0 : Convert.ToInt32(value); - } - else if (value != null && string.IsNullOrWhiteSpace(value.ToString()) == false && int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) - { - dto.IntegerValue = val; + propertyId = propDto.Id; + values.Add(new Property.InitialPropertyValue( + languageRepository.GetIsoCodeById(propDto.LanguageId), + propDto.Segment, + propDto.VersionId == publishedVersionId, + propDto.Value)); } } - else if (property.ValueStorageType == ValueStorageType.Decimal && value != null) - { - if (decimal.TryParse(value.ToString(), out var val)) - { - dto.DecimalValue = val; // property value should be normalized already - } - } - else if (property.ValueStorageType == ValueStorageType.Date && value != null && string.IsNullOrWhiteSpace(value.ToString()) == false) - { - if (DateTime.TryParse(value.ToString(), out var date)) - { - dto.DateValue = date; - } - } - else if (property.ValueStorageType == ValueStorageType.Ntext && value != null) - { - dto.TextValue = value.ToString(); - } - else if (property.ValueStorageType == ValueStorageType.Nvarchar && value != null) - { - dto.VarcharValue = value.ToString(); - } - return dto; + var property = Property.CreateWithValues(propertyId, propertyType, values.ToArray()); + properties.Add(property); } - /// - /// Creates a collection of from a collection of - /// - /// - /// The of the entity containing the collection of - /// - /// - /// - /// The properties to map - /// - /// out parameter indicating that one or more properties have been edited - /// - /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. - /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. - /// - /// - public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, - ILanguageRepository languageRepository, out bool edited, - out HashSet? editedCultures) + return properties; + } + + /// + /// Creates a collection of from a collection of + /// + /// + /// The of the entity containing the collection of + /// + /// + /// + /// The properties to map + /// + /// out parameter indicating that one or more properties have been edited + /// + /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. + /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. + /// + /// + public static IEnumerable BuildDtos( + ContentVariation contentVariation, + int currentVersionId, + int publishedVersionId, + IEnumerable properties, + ILanguageRepository languageRepository, + out bool edited, + out HashSet? editedCultures) + { + var propertyDataDtos = new List(); + edited = false; + editedCultures = null; // don't allocate unless necessary + string? defaultCulture = null; // don't allocate unless necessary + + var entityVariesByCulture = contentVariation.VariesByCulture(); + + // create dtos for each property values, but only for values that do actually exist + // ie have a non-null value, everything else is just ignored and won't have a db row + foreach (IProperty property in properties) { - var propertyDataDtos = new List(); - edited = false; - editedCultures = null; // don't allocate unless necessary - string? defaultCulture = null; //don't allocate unless necessary - - var entityVariesByCulture = contentVariation.VariesByCulture(); - - // create dtos for each property values, but only for values that do actually exist - // ie have a non-null value, everything else is just ignored and won't have a db row - - foreach (var property in properties) + if (property.PropertyType.SupportsPublishing) { - if (property.PropertyType.SupportsPublishing) + // create the resulting hashset if it's not created and the entity varies by culture + if (entityVariesByCulture && editedCultures == null) { - //create the resulting hashset if it's not created and the entity varies by culture - if (entityVariesByCulture && editedCultures == null) - editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + } - // publishing = deal with edit and published values - foreach (var propertyValue in property.Values) + // publishing = deal with edit and published values + foreach (IPropertyValue propertyValue in property.Values) + { + var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; + var isCultureValue = propertyValue.Culture != null; + var isSegmentValue = propertyValue.Segment != null; + + // deal with published value + if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) { - var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; - var isCultureValue = propertyValue.Culture != null; - var isSegmentValue = propertyValue.Segment != null; + propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); + } - // deal with published value - if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) - propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue?.Segment, propertyValue?.PublishedValue)); + // deal with edit value + if (propertyValue.EditedValue != null || isSegmentValue) + { + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + } - // deal with edit value - if (propertyValue?.EditedValue != null || isSegmentValue) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue?.Culture), propertyValue?.Segment, propertyValue?.EditedValue)); + // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the + // administrator has previously changed the property type to be variant vs invariant. + // We need to check for this scenario here because otherwise the editedCultures and edited flags + // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to + // only process edited cultures based on the current value type and how the property varies. + // The above logic will still persist the currently saved property value for each culture in case the admin + // decides to swap the property's variance again, in which case the edited flag will be recalculated. + if ((property.PropertyType.VariesByCulture() && isInvariantValue) || + (!property.PropertyType.VariesByCulture() && isCultureValue)) + { + continue; + } - // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the - // administrator has previously changed the property type to be variant vs invariant. - // We need to check for this scenario here because otherwise the editedCultures and edited flags - // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to - // only process edited cultures based on the current value type and how the property varies. - // The above logic will still persist the currently saved property value for each culture in case the admin - // decides to swap the property's variance again, in which case the edited flag will be recalculated. + // use explicit equals here, else object comparison fails at comparing eg strings + var sameValues = propertyValue?.PublishedValue == null + ? propertyValue?.EditedValue == null + : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); - if (property.PropertyType.VariesByCulture() && isInvariantValue || !property.PropertyType.VariesByCulture() && isCultureValue) - continue; + edited |= !sameValues; - // use explicit equals here, else object comparison fails at comparing eg strings - var sameValues = propertyValue?.PublishedValue == null ? propertyValue?.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); - - edited |= !sameValues; - - if (entityVariesByCulture && !sameValues) + if (entityVariesByCulture && !sameValues) + { + if (isCultureValue && propertyValue?.Culture is not null) { - if (isCultureValue && propertyValue?.Culture is not null) + editedCultures?.Add(propertyValue.Culture); // report culture as edited + } + else if (isInvariantValue) + { + // flag culture as edited if it contains an edited invariant property + if (defaultCulture == null) { - editedCultures?.Add(propertyValue.Culture); // report culture as edited + defaultCulture = languageRepository.GetDefaultIsoCode(); } - else if (isInvariantValue) - { - // flag culture as edited if it contains an edited invariant property - if (defaultCulture == null) - defaultCulture = languageRepository.GetDefaultIsoCode(); - editedCultures?.Add(defaultCulture); - } + editedCultures?.Add(defaultCulture); } } } - else - { - foreach (var propertyValue in property.Values) - { - // not publishing = only deal with edit values - if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); - } - edited = true; - } } + else + { + foreach (IPropertyValue propertyValue in property.Values) + { + // not publishing = only deal with edit values + if (propertyValue.EditedValue != null) + { + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + } + } - return propertyDataDtos; + edited = true; + } } + + return propertyDataDtos; + } + + private static PropertyDataDto BuildDto(int versionId, IProperty property, int? languageId, string? segment, object? value) + { + var dto = new PropertyDataDto { VersionId = versionId, PropertyTypeId = property.PropertyTypeId }; + + if (languageId.HasValue) + { + dto.LanguageId = languageId; + } + + if (segment != null) + { + dto.Segment = segment; + } + + if (property.ValueStorageType == ValueStorageType.Integer) + { + if (value is bool || property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.Boolean) + { + dto.IntegerValue = value != null && string.IsNullOrEmpty(value.ToString()) ? 0 : Convert.ToInt32(value); + } + else if (value != null && string.IsNullOrWhiteSpace(value.ToString()) == false && + int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + dto.IntegerValue = val; + } + } + else if (property.ValueStorageType == ValueStorageType.Decimal && value != null) + { + if (decimal.TryParse(value.ToString(), out var val)) + { + dto.DecimalValue = val; // property value should be normalized already + } + } + else if (property.ValueStorageType == ValueStorageType.Date && value != null && + string.IsNullOrWhiteSpace(value.ToString()) == false) + { + if (DateTime.TryParse(value.ToString(), out DateTime date)) + { + dto.DateValue = date; + } + } + else if (property.ValueStorageType == ValueStorageType.Ntext && value != null) + { + dto.TextValue = value.ToString(); + } + else if (property.ValueStorageType == ValueStorageType.Nvarchar && value != null) + { + dto.VarcharValue = value.ToString(); + } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs index 333c0176c8..65dc528c17 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs @@ -1,160 +1,163 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PropertyGroupFactory { - internal static class PropertyGroupFactory + internal static PropertyTypeGroupDto BuildGroupDto(PropertyGroup propertyGroup, int contentTypeId) { - - #region Implementation of IEntityFactory,IEnumerable> - - public static IEnumerable BuildEntity(IEnumerable groupDtos, - bool isPublishing, - int contentTypeId, - DateTime createDate, - DateTime updateDate, - Func propertyTypeCtor) + var dto = new PropertyTypeGroupDto { - // groupDtos contains all the groups, those that are defined on the current - // content type, and those that are inherited from composition content types - var propertyGroups = new PropertyGroupCollection(); - foreach (var groupDto in groupDtos) - { - var group = new PropertyGroup(isPublishing); + UniqueId = propertyGroup.Key, + Type = (short)propertyGroup.Type, + ContentTypeNodeId = contentTypeId, + Text = propertyGroup.Name, + Alias = propertyGroup.Alias, + SortOrder = propertyGroup.SortOrder, + }; - try - { - group.DisableChangeTracking(); - - // if the group is defined on the current content type, - // assign its identifier, else it will be zero - if (groupDto.ContentTypeNodeId == contentTypeId) - group.Id = groupDto.Id; - - group.Key = groupDto.UniqueId; - group.Type = (PropertyGroupType)groupDto.Type; - group.Name = groupDto.Text; - group.Alias = groupDto.Alias; - group.SortOrder = groupDto.SortOrder; - - group.PropertyTypes = new PropertyTypeCollection(isPublishing); - - //Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded - var typeDtos = groupDto.PropertyTypeDtos?.Where(x => x.Id > 0) ?? Enumerable.Empty(); - foreach (var typeDto in typeDtos) - { - var tempGroupDto = groupDto; - var propertyType = propertyTypeCtor(typeDto.DataTypeDto.EditorAlias, - typeDto.DataTypeDto.DbType.EnumParse(true), - typeDto.Alias); - - try - { - propertyType.DisableChangeTracking(); - - propertyType.Alias = typeDto.Alias ?? string.Empty; - propertyType.DataTypeId = typeDto.DataTypeId; - propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; - propertyType.Description = typeDto.Description; - propertyType.Id = typeDto.Id; - propertyType.Key = typeDto.UniqueId; - propertyType.Name = typeDto.Name ?? string.Empty; - propertyType.Mandatory = typeDto.Mandatory; - propertyType.MandatoryMessage = typeDto.MandatoryMessage; - propertyType.SortOrder = typeDto.SortOrder; - propertyType.ValidationRegExp = typeDto.ValidationRegExp; - propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage; - propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id); - propertyType.CreateDate = createDate; - propertyType.UpdateDate = updateDate; - propertyType.Variations = (ContentVariation)typeDto.Variations; - - // reset dirty initial properties (U4-1946) - propertyType.ResetDirtyProperties(false); - group.PropertyTypes.Add(propertyType); - } - finally - { - propertyType.EnableChangeTracking(); - } - } - - // reset dirty initial properties (U4-1946) - group.ResetDirtyProperties(false); - propertyGroups.Add(group); - } - finally - { - group.EnableChangeTracking(); - } - } - - return propertyGroups; + if (propertyGroup.HasIdentity) + { + dto.Id = propertyGroup.Id; } - public static IEnumerable BuildDto(IEnumerable entity) - { - return entity.Select(BuildGroupDto).ToList(); - } + dto.PropertyTypeDtos = propertyGroup.PropertyTypes + ?.Select(propertyType => BuildPropertyTypeDto(propertyGroup.Id, propertyType, contentTypeId)).ToList(); - #endregion - - internal static PropertyTypeGroupDto BuildGroupDto(PropertyGroup propertyGroup, int contentTypeId) - { - var dto = new PropertyTypeGroupDto - { - UniqueId = propertyGroup.Key, - Type = (short)propertyGroup.Type, - ContentTypeNodeId = contentTypeId, - Text = propertyGroup.Name, - Alias = propertyGroup.Alias, - SortOrder = propertyGroup.SortOrder - }; - - if (propertyGroup.HasIdentity) - dto.Id = propertyGroup.Id; - - dto.PropertyTypeDtos = propertyGroup.PropertyTypes?.Select(propertyType => BuildPropertyTypeDto(propertyGroup.Id, propertyType, contentTypeId)).ToList(); - - return dto; - } - - internal static PropertyTypeDto BuildPropertyTypeDto(int groupId, IPropertyType propertyType, int contentTypeId) - { - var propertyTypeDto = new PropertyTypeDto - { - Alias = propertyType.Alias, - ContentTypeId = contentTypeId, - DataTypeId = propertyType.DataTypeId, - Description = propertyType.Description, - Mandatory = propertyType.Mandatory, - MandatoryMessage = propertyType.MandatoryMessage, - Name = propertyType.Name, - SortOrder = propertyType.SortOrder, - ValidationRegExp = propertyType.ValidationRegExp, - ValidationRegExpMessage = propertyType.ValidationRegExpMessage, - UniqueId = propertyType.Key, - Variations = (byte)propertyType.Variations, - LabelOnTop = propertyType.LabelOnTop - }; - - if (groupId != default) - { - propertyTypeDto.PropertyTypeGroupId = groupId; - } - else - { - propertyTypeDto.PropertyTypeGroupId = null; - } - - if (propertyType.HasIdentity) - propertyTypeDto.Id = propertyType.Id; - - return propertyTypeDto; - } + return dto; } + + internal static PropertyTypeDto BuildPropertyTypeDto(int groupId, IPropertyType propertyType, int contentTypeId) + { + var propertyTypeDto = new PropertyTypeDto + { + Alias = propertyType.Alias, + ContentTypeId = contentTypeId, + DataTypeId = propertyType.DataTypeId, + Description = propertyType.Description, + Mandatory = propertyType.Mandatory, + MandatoryMessage = propertyType.MandatoryMessage, + Name = propertyType.Name, + SortOrder = propertyType.SortOrder, + ValidationRegExp = propertyType.ValidationRegExp, + ValidationRegExpMessage = propertyType.ValidationRegExpMessage, + UniqueId = propertyType.Key, + Variations = (byte)propertyType.Variations, + LabelOnTop = propertyType.LabelOnTop, + }; + + if (groupId != default) + { + propertyTypeDto.PropertyTypeGroupId = groupId; + } + else + { + propertyTypeDto.PropertyTypeGroupId = null; + } + + if (propertyType.HasIdentity) + { + propertyTypeDto.Id = propertyType.Id; + } + + return propertyTypeDto; + } + + #region Implementation of IEntityFactory,IEnumerable> + + public static IEnumerable BuildEntity( + IEnumerable groupDtos, + bool isPublishing, + int contentTypeId, + DateTime createDate, + DateTime updateDate, + Func propertyTypeCtor) + { + // groupDtos contains all the groups, those that are defined on the current + // content type, and those that are inherited from composition content types + var propertyGroups = new PropertyGroupCollection(); + foreach (PropertyTypeGroupDto groupDto in groupDtos) + { + var group = new PropertyGroup(isPublishing); + + try + { + group.DisableChangeTracking(); + + // if the group is defined on the current content type, + // assign its identifier, else it will be zero + if (groupDto.ContentTypeNodeId == contentTypeId) + { + group.Id = groupDto.Id; + } + + group.Key = groupDto.UniqueId; + group.Type = (PropertyGroupType)groupDto.Type; + group.Name = groupDto.Text; + group.Alias = groupDto.Alias; + group.SortOrder = groupDto.SortOrder; + + group.PropertyTypes = new PropertyTypeCollection(isPublishing); + + // Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded + IEnumerable typeDtos = groupDto.PropertyTypeDtos?.Where(x => x.Id > 0) ?? + Enumerable.Empty(); + foreach (PropertyTypeDto typeDto in typeDtos) + { + PropertyTypeGroupDto tempGroupDto = groupDto; + PropertyType propertyType = propertyTypeCtor( + typeDto.DataTypeDto.EditorAlias, + typeDto.DataTypeDto.DbType.EnumParse(true), + typeDto.Alias); + + try + { + propertyType.DisableChangeTracking(); + + propertyType.Alias = typeDto.Alias ?? string.Empty; + propertyType.DataTypeId = typeDto.DataTypeId; + propertyType.DataTypeKey = typeDto.DataTypeDto.NodeDto.UniqueId; + propertyType.Description = typeDto.Description; + propertyType.Id = typeDto.Id; + propertyType.Key = typeDto.UniqueId; + propertyType.Name = typeDto.Name ?? string.Empty; + propertyType.Mandatory = typeDto.Mandatory; + propertyType.MandatoryMessage = typeDto.MandatoryMessage; + propertyType.SortOrder = typeDto.SortOrder; + propertyType.ValidationRegExp = typeDto.ValidationRegExp; + propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage; + propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id); + propertyType.CreateDate = createDate; + propertyType.UpdateDate = updateDate; + propertyType.Variations = (ContentVariation)typeDto.Variations; + + // reset dirty initial properties (U4-1946) + propertyType.ResetDirtyProperties(false); + group.PropertyTypes.Add(propertyType); + } + finally + { + propertyType.EnableChangeTracking(); + } + } + + // reset dirty initial properties (U4-1946) + group.ResetDirtyProperties(false); + propertyGroups.Add(group); + } + finally + { + group.EnableChangeTracking(); + } + } + + return propertyGroups; + } + + public static IEnumerable BuildDto(IEnumerable entity) => + entity.Select(BuildGroupDto).ToList(); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs index 0ed16d80da..25232b4f9f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/PublicAccessEntryFactory.cs @@ -1,53 +1,52 @@ -using System.Linq; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class PublicAccessEntryFactory { - internal static class PublicAccessEntryFactory + public static PublicAccessEntry BuildEntity(AccessDto dto) { - public static PublicAccessEntry BuildEntity(AccessDto dto) - { - var entity = new PublicAccessEntry(dto.Id, dto.NodeId, dto.LoginNodeId, dto.NoAccessNodeId, + var entity = new PublicAccessEntry( + dto.Id, + dto.NodeId, + dto.LoginNodeId, + dto.NoAccessNodeId, dto.Rules.Select(x => new PublicAccessRule(x.Id, x.AccessId) - { - RuleValue = x.RuleValue, - RuleType = x.RuleType, - CreateDate = x.CreateDate, - UpdateDate = x.UpdateDate - })) { - CreateDate = dto.CreateDate, - UpdateDate = dto.UpdateDate - }; + RuleValue = x.RuleValue, + RuleType = x.RuleType, + CreateDate = x.CreateDate, + UpdateDate = x.UpdateDate, + })) + { CreateDate = dto.CreateDate, UpdateDate = dto.UpdateDate }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } - public static AccessDto BuildDto(PublicAccessEntry entity) + public static AccessDto BuildDto(PublicAccessEntry entity) + { + var dto = new AccessDto { - var dto = new AccessDto + Id = entity.Key, + NoAccessNodeId = entity.NoAccessNodeId, + LoginNodeId = entity.LoginNodeId, + NodeId = entity.ProtectedNodeId, + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Rules = entity.Rules.Select(x => new AccessRuleDto { - Id = entity.Key, - NoAccessNodeId = entity.NoAccessNodeId, - LoginNodeId = entity.LoginNodeId, - NodeId = entity.ProtectedNodeId, - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Rules = entity.Rules.Select(x => new AccessRuleDto - { - AccessId = x.AccessEntryId, - Id = x.Key, - RuleValue = x.RuleValue, - RuleType = x.RuleType, - CreateDate = x.CreateDate, - UpdateDate = x.UpdateDate - }).ToList() - }; + AccessId = x.AccessEntryId, + Id = x.Key, + RuleValue = x.RuleValue, + RuleType = x.RuleType, + CreateDate = x.CreateDate, + UpdateDate = x.UpdateDate, + }).ToList(), + }; - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs index 63d3292160..872810ddd6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationFactory.cs @@ -1,66 +1,68 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class RelationFactory { - internal static class RelationFactory + public static IRelation BuildEntity(RelationDto dto, IRelationType relationType) { - public static IRelation BuildEntity(RelationDto dto, IRelationType relationType) + var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType); + + try { - var entity = new Relation(dto.ParentId, dto.ChildId, dto.ParentObjectType, dto.ChildObjectType, relationType); + entity.DisableChangeTracking(); - try - { - entity.DisableChangeTracking(); + entity.Comment = dto.Comment; + entity.CreateDate = dto.Datetime; + entity.Id = dto.Id; + entity.UpdateDate = dto.Datetime; - entity.Comment = dto.Comment; - entity.CreateDate = dto.Datetime; - entity.Id = dto.Id; - entity.UpdateDate = dto.Datetime; + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; + } + finally + { + entity.EnableChangeTracking(); + } + } - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } - finally - { - entity.EnableChangeTracking(); - } + public static RelationDto BuildDto(IRelation entity) + { + var dto = new RelationDto + { + ChildId = entity.ChildId, + Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, + Datetime = entity.CreateDate, + ParentId = entity.ParentId, + RelationType = entity.RelationType.Id, + }; + + if (entity.HasIdentity) + { + dto.Id = entity.Id; } - public static RelationDto BuildDto(IRelation entity) + return dto; + } + + public static RelationDto BuildDto(ReadOnlyRelation entity) + { + var dto = new RelationDto { - var dto = new RelationDto - { - ChildId = entity.ChildId, - Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, - Datetime = entity.CreateDate, - ParentId = entity.ParentId, - RelationType = entity.RelationType.Id - }; + ChildId = entity.ChildId, + Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, + Datetime = entity.CreateDate, + ParentId = entity.ParentId, + RelationType = entity.RelationTypeId, + }; - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; - } - - public static RelationDto BuildDto(ReadOnlyRelation entity) - { - var dto = new RelationDto - { - ChildId = entity.ChildId, - Comment = string.IsNullOrEmpty(entity.Comment) ? string.Empty : entity.Comment, - Datetime = entity.CreateDate, - ParentId = entity.ParentId, - RelationType = entity.RelationTypeId - }; - - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; + if (entity.HasIdentity) + { + dto.Id = entity.Id; } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs index 57b1831c9d..3fbc91f51e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs @@ -1,60 +1,58 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class RelationTypeFactory { - internal static class RelationTypeFactory + #region Implementation of IEntityFactory + + public static IRelationType BuildEntity(RelationTypeDto dto) { - #region Implementation of IEntityFactory + var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency); - public static IRelationType BuildEntity(RelationTypeDto dto) + try { - var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency); + entity.DisableChangeTracking(); - try - { - entity.DisableChangeTracking(); + entity.Id = dto.Id; + entity.Key = dto.UniqueId; - entity.Id = dto.Id; - entity.Key = dto.UniqueId; - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } - finally - { - entity.EnableChangeTracking(); - } + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; } - - public static RelationTypeDto BuildDto(IRelationType entity) + finally { - var isDependency = false; - if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) - { - isDependency = relationTypeWithIsDependency.IsDependency; - } - var dto = new RelationTypeDto - { - Alias = entity.Alias, - ChildObjectType = entity.ChildObjectType, - Dual = entity.IsBidirectional, - IsDependency = isDependency, - Name = entity.Name ?? string.Empty, - ParentObjectType = entity.ParentObjectType, - UniqueId = entity.Key - }; - if (entity.HasIdentity) - { - dto.Id = entity.Id; - } - - return dto; + entity.EnableChangeTracking(); } - - - - #endregion } + + public static RelationTypeDto BuildDto(IRelationType entity) + { + var isDependency = false; + if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) + { + isDependency = relationTypeWithIsDependency.IsDependency; + } + + var dto = new RelationTypeDto + { + Alias = entity.Alias, + ChildObjectType = entity.ChildObjectType, + Dual = entity.IsBidirectional, + IsDependency = isDependency, + Name = entity.Name ?? string.Empty, + ParentObjectType = entity.ParentObjectType, + UniqueId = entity.Key, + }; + if (entity.HasIdentity) + { + dto.Id = entity.Id; + } + + return dto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs index f662faf561..cfbb27bd44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ServerRegistrationFactory.cs @@ -1,34 +1,36 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class ServerRegistrationFactory { - internal static class ServerRegistrationFactory + public static ServerRegistration BuildEntity(ServerRegistrationDto dto) { - public static ServerRegistration BuildEntity(ServerRegistrationDto dto) + var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive, dto.IsSchedulingPublisher); + + // reset dirty initial properties (U4-1946) + model.ResetDirtyProperties(false); + return model; + } + + public static ServerRegistrationDto BuildDto(IServerRegistration entity) + { + var dto = new ServerRegistrationDto { - var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive, dto.IsSchedulingPublisher); - // reset dirty initial properties (U4-1946) - model.ResetDirtyProperties(false); - return model; + ServerAddress = entity.ServerAddress, + DateRegistered = entity.CreateDate, + IsActive = entity.IsActive, + IsSchedulingPublisher = ((ServerRegistration)entity).IsSchedulingPublisher, + DateAccessed = entity.UpdateDate, + ServerIdentity = entity.ServerIdentity, + }; + + if (entity.HasIdentity) + { + dto.Id = entity.Id; } - public static ServerRegistrationDto BuildDto(IServerRegistration entity) - { - var dto = new ServerRegistrationDto - { - ServerAddress = entity.ServerAddress, - DateRegistered = entity.CreateDate, - IsActive = entity.IsActive, - IsSchedulingPublisher = ((ServerRegistration) entity).IsSchedulingPublisher, - DateAccessed = entity.UpdateDate, - ServerIdentity = entity.ServerIdentity - }; - - if (entity.HasIdentity) - dto.Id = entity.Id; - - return dto; - } + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs index e666e53658..1774bb854c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/TagFactory.cs @@ -1,28 +1,27 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories -{ - internal static class TagFactory - { - public static ITag BuildEntity(TagDto dto) - { - var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - return entity; - } +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; - public static TagDto BuildDto(ITag entity) - { - return new TagDto - { - Id = entity.Id, - Group = entity.Group, - Text = entity.Text, - LanguageId = entity.LanguageId - //Key = entity.Group + "/" + entity.Text // de-normalize - }; - } +internal static class TagFactory +{ + public static ITag BuildEntity(TagDto dto) + { + var entity = new Tag(dto.Id, dto.Group, dto.Text, dto.LanguageId) { NodeCount = dto.NodeCount }; + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + return entity; } + + public static TagDto BuildDto(ITag entity) => + new TagDto + { + Id = entity.Id, + Group = entity.Group, + Text = entity.Text, + LanguageId = entity.LanguageId, + + // Key = entity.Group + "/" + entity.Text // de-normalize + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs index eb84d46f68..3028d1a509 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/TemplateFactory.cs @@ -1,87 +1,81 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using File = Umbraco.Cms.Core.Models.File; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class TemplateFactory { - internal static class TemplateFactory + private static NodeDto BuildNodeDto(Template entity, Guid? nodeObjectTypeId) { - - #region Implementation of IEntityFactory - - public static Template BuildEntity(IShortStringHelper shortStringHelper, TemplateDto dto, IEnumerable childDefinitions, Func getFileContent) + var nodeDto = new NodeDto { - var template = new Template(shortStringHelper, dto.NodeDto.Text, dto.Alias, getFileContent); + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = 1, + NodeObjectType = nodeObjectTypeId, + ParentId = entity.MasterTemplateId?.Value ?? 0, + Path = entity.Path, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key + }; - try + return nodeDto; + } + + #region Implementation of IEntityFactory + + public static Template BuildEntity(IShortStringHelper shortStringHelper, TemplateDto dto, + IEnumerable childDefinitions, Func getFileContent) + { + var template = new Template(shortStringHelper, dto.NodeDto.Text, dto.Alias, getFileContent); + + try + { + template.DisableChangeTracking(); + + template.CreateDate = dto.NodeDto.CreateDate; + template.Id = dto.NodeId; + template.Key = dto.NodeDto.UniqueId; + template.Path = dto.NodeDto.Path; + + template.IsMasterTemplate = childDefinitions.Any(x => x.ParentId == dto.NodeId); + + if (dto.NodeDto.ParentId > 0) { - template.DisableChangeTracking(); - - template.CreateDate = dto.NodeDto.CreateDate; - template.Id = dto.NodeId; - template.Key = dto.NodeDto.UniqueId; - template.Path = dto.NodeDto.Path; - - template.IsMasterTemplate = childDefinitions.Any(x => x.ParentId == dto.NodeId); - - if (dto.NodeDto.ParentId > 0) - template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); - - // reset dirty initial properties (U4-1946) - template.ResetDirtyProperties(false); - return template; - } - finally - { - template.EnableChangeTracking(); + template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); } + + // reset dirty initial properties (U4-1946) + template.ResetDirtyProperties(false); + return template; } - - public static TemplateDto BuildDto(Template entity, Guid? nodeObjectTypeId,int primaryKey) + finally { - var dto = new TemplateDto - { - Alias = entity.Alias, - NodeDto = BuildNodeDto(entity, nodeObjectTypeId) - }; - - if (entity.MasterTemplateId != null && entity.MasterTemplateId.Value > 0) - { - dto.NodeDto.ParentId = entity.MasterTemplateId.Value; - } - - if (entity.HasIdentity) - { - dto.NodeId = entity.Id; - dto.PrimaryKey = primaryKey; - } - - return dto; - } - - #endregion - - private static NodeDto BuildNodeDto(Template entity,Guid? nodeObjectTypeId) - { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = 1, - NodeObjectType = nodeObjectTypeId, - ParentId = entity.MasterTemplateId?.Value ?? 0, - Path = entity.Path, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key - }; - - return nodeDto; + template.EnableChangeTracking(); } } + + public static TemplateDto BuildDto(Template entity, Guid? nodeObjectTypeId, int primaryKey) + { + var dto = new TemplateDto {Alias = entity.Alias, NodeDto = BuildNodeDto(entity, nodeObjectTypeId)}; + + if (entity.MasterTemplateId != null && entity.MasterTemplateId.Value > 0) + { + dto.NodeDto.ParentId = entity.MasterTemplateId.Value; + } + + if (entity.HasIdentity) + { + dto.NodeId = entity.Id; + dto.PrimaryKey = primaryKey; + } + + return dto; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 8ee2bcfec0..721a504a20 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -1,119 +1,120 @@ -using System; -using System.Linq; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class UserFactory { - internal static class UserFactory + public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) { - public static IUser BuildEntity(GlobalSettings globalSettings, UserDto dto) + var guidId = dto.Id.ToGuid(); + + var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, + dto.PasswordConfig, + dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content) + .Select(x => x.StartNode).ToArray(), + dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media) + .Select(x => x.StartNode).ToArray()); + + try { - var guidId = dto.Id.ToGuid(); + user.DisableChangeTracking(); - var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, dto.PasswordConfig, - dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(), - dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.StartNode).ToArray(), - dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.StartNode).ToArray()); + user.Key = guidId; + user.IsLockedOut = dto.NoConsole; + user.IsApproved = dto.Disabled == false; + user.Language = dto.UserLanguage; + user.SecurityStamp = dto.SecurityStampToken; + user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; + user.LastLockoutDate = dto.LastLockoutDate; + user.LastLoginDate = dto.LastLoginDate; + user.LastPasswordChangeDate = dto.LastPasswordChangeDate; + user.CreateDate = dto.CreateDate; + user.UpdateDate = dto.UpdateDate; + user.Avatar = dto.Avatar; + user.EmailConfirmedDate = dto.EmailConfirmedDate; + user.InvitedDate = dto.InvitedDate; + user.TourData = dto.TourData; - try - { - user.DisableChangeTracking(); + // reset dirty initial properties (U4-1946) + user.ResetDirtyProperties(false); - user.Key = guidId; - user.IsLockedOut = dto.NoConsole; - user.IsApproved = dto.Disabled == false; - user.Language = dto.UserLanguage; - user.SecurityStamp = dto.SecurityStampToken; - user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; - user.LastLockoutDate = dto.LastLockoutDate; - user.LastLoginDate = dto.LastLoginDate; - user.LastPasswordChangeDate = dto.LastPasswordChangeDate; - user.CreateDate = dto.CreateDate; - user.UpdateDate = dto.UpdateDate; - user.Avatar = dto.Avatar; - user.EmailConfirmedDate = dto.EmailConfirmedDate; - user.InvitedDate = dto.InvitedDate; - user.TourData = dto.TourData; - - // reset dirty initial properties (U4-1946) - user.ResetDirtyProperties(false); - - return user; - } - finally - { - user.EnableChangeTracking(); - } + return user; } - - public static UserDto BuildDto(IUser entity) + finally { - var dto = new UserDto - { - Disabled = entity.IsApproved == false, - Email = entity.Email, - Login = entity.Username, - NoConsole = entity.IsLockedOut, - Password = entity.RawPasswordValue, - PasswordConfig = entity.PasswordConfiguration, - UserLanguage = entity.Language, - UserName = entity.Name!, - SecurityStampToken = entity.SecurityStamp, - FailedLoginAttempts = entity.FailedPasswordAttempts, - LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? (DateTime?)null : entity.LastLockoutDate, - LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, - LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Avatar = entity.Avatar, - EmailConfirmedDate = entity.EmailConfirmedDate, - InvitedDate = entity.InvitedDate, - TourData = entity.TourData - }; - - if (entity.StartContentIds is not null) - { - foreach (var startNodeId in entity.StartContentIds) - { - dto.UserStartNodeDtos.Add(new UserStartNodeDto - { - StartNode = startNodeId, - StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Content, - UserId = entity.Id - }); - } - } - - if (entity.StartMediaIds is not null) - { - foreach (var startNodeId in entity.StartMediaIds) - { - dto.UserStartNodeDtos.Add(new UserStartNodeDto - { - StartNode = startNodeId, - StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Media, - UserId = entity.Id - }); - } - } - - if (entity.HasIdentity) - { - dto.Id = entity.Id.SafeCast(); - } - - return dto; - } - - private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) - { - return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, - group.StartContentId, group.StartMediaId, group.Alias, - group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), - group.DefaultPermissions == null ? Enumerable.Empty() : group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); + user.EnableChangeTracking(); } } + + public static UserDto BuildDto(IUser entity) + { + var dto = new UserDto + { + Disabled = entity.IsApproved == false, + Email = entity.Email, + Login = entity.Username, + NoConsole = entity.IsLockedOut, + Password = entity.RawPasswordValue, + PasswordConfig = entity.PasswordConfiguration, + UserLanguage = entity.Language, + UserName = entity.Name!, + SecurityStampToken = entity.SecurityStamp, + FailedLoginAttempts = entity.FailedPasswordAttempts, + LastLockoutDate = entity.LastLockoutDate == DateTime.MinValue ? null : entity.LastLockoutDate, + LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? null : entity.LastLoginDate, + LastPasswordChangeDate = + entity.LastPasswordChangeDate == DateTime.MinValue ? null : entity.LastPasswordChangeDate, + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Avatar = entity.Avatar, + EmailConfirmedDate = entity.EmailConfirmedDate, + InvitedDate = entity.InvitedDate, + TourData = entity.TourData, + }; + + if (entity.StartContentIds is not null) + { + foreach (var startNodeId in entity.StartContentIds) + { + dto.UserStartNodeDtos.Add(new UserStartNodeDto + { + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Content, + UserId = entity.Id, + }); + } + } + + if (entity.StartMediaIds is not null) + { + foreach (var startNodeId in entity.StartMediaIds) + { + dto.UserStartNodeDtos.Add(new UserStartNodeDto + { + StartNode = startNodeId, + StartNodeType = (int)UserStartNodeDto.StartNodeTypeValue.Media, + UserId = entity.Id, + }); + } + } + + if (entity.HasIdentity) + { + dto.Id = entity.Id.SafeCast(); + } + + return dto; + } + + private static IReadOnlyUserGroup ToReadOnlyGroup(UserGroupDto group) => + new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, + group.StartContentId, group.StartMediaId, group.Alias, + group.UserGroup2AppDtos.Select(x => x.AppAlias).WhereNotNull().ToArray(), + group.DefaultPermissions == null + ? Enumerable.Empty() + : group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs index 9672e0e3a9..6807403fca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs @@ -1,82 +1,79 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Factories +namespace Umbraco.Cms.Infrastructure.Persistence.Factories; + +internal static class UserGroupFactory { - internal static class UserGroupFactory + public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto) { - public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserGroupDto dto) + var userGroup = new UserGroup( + shortStringHelper, + dto.UserCount, + dto.Alias, + dto.Name, + dto.DefaultPermissions.IsNullOrWhiteSpace() ? Enumerable.Empty() : dto.DefaultPermissions!.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), + dto.Icon); + + try { - var userGroup = new UserGroup(shortStringHelper, dto.UserCount, dto.Alias, dto.Name, - dto.DefaultPermissions.IsNullOrWhiteSpace() - ? Enumerable.Empty() - : dto.DefaultPermissions!.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToList(), - dto.Icon); - - try + userGroup.DisableChangeTracking(); + userGroup.Id = dto.Id; + userGroup.CreateDate = dto.CreateDate; + userGroup.UpdateDate = dto.UpdateDate; + userGroup.StartContentId = dto.StartContentId; + userGroup.StartMediaId = dto.StartMediaId; + if (dto.UserGroup2AppDtos != null) { - userGroup.DisableChangeTracking(); - userGroup.Id = dto.Id; - userGroup.CreateDate = dto.CreateDate; - userGroup.UpdateDate = dto.UpdateDate; - userGroup.StartContentId = dto.StartContentId; - userGroup.StartMediaId = dto.StartMediaId; - if (dto.UserGroup2AppDtos != null) + foreach (UserGroup2AppDto app in dto.UserGroup2AppDtos) { - foreach (var app in dto.UserGroup2AppDtos) - { - userGroup.AddAllowedSection(app.AppAlias); - } + userGroup.AddAllowedSection(app.AppAlias); } + } - userGroup.ResetDirtyProperties(false); - return userGroup; - } - finally - { - userGroup.EnableChangeTracking(); - } + userGroup.ResetDirtyProperties(false); + return userGroup; } - - public static UserGroupDto BuildDto(IUserGroup entity) + finally { - var dto = new UserGroupDto - { - Alias = entity.Alias, - DefaultPermissions = entity.Permissions == null ? "" : string.Join("", entity.Permissions), - Name = entity.Name, - UserGroup2AppDtos = new List(), - CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate, - Icon = entity.Icon, - StartMediaId = entity.StartMediaId, - StartContentId = entity.StartContentId - }; + userGroup.EnableChangeTracking(); + } + } - foreach (var app in entity.AllowedSections) - { - var appDto = new UserGroup2AppDto - { - AppAlias = app - }; - if (entity.HasIdentity) - { - appDto.UserGroupId = entity.Id; - } - - dto.UserGroup2AppDtos.Add(appDto); - } + public static UserGroupDto BuildDto(IUserGroup entity) + { + var dto = new UserGroupDto + { + Alias = entity.Alias, + DefaultPermissions = entity.Permissions == null ? string.Empty : string.Join(string.Empty, entity.Permissions), + Name = entity.Name, + UserGroup2AppDtos = new List(), + CreateDate = entity.CreateDate, + UpdateDate = entity.UpdateDate, + Icon = entity.Icon, + StartMediaId = entity.StartMediaId, + StartContentId = entity.StartContentId, + }; + foreach (var app in entity.AllowedSections) + { + var appDto = new UserGroup2AppDto { AppAlias = app }; if (entity.HasIdentity) - dto.Id = short.Parse(entity.Id.ToString()); + { + appDto.UserGroupId = entity.Id; + } - return dto; + dto.UserGroup2AppDtos.Add(appDto); } + if (entity.HasIdentity) + { + dto.Id = short.Parse(entity.Id.ToString()); + } + + return dto; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs index 59d4f1c0b7..1b0b3e8f82 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ITransientErrorDetectionStrategy.cs @@ -1,17 +1,15 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +/// +/// Defines an interface which must be implemented by custom components responsible for detecting specific transient +/// conditions. +/// +public interface ITransientErrorDetectionStrategy { /// - /// Defines an interface which must be implemented by custom components responsible for detecting specific transient conditions. + /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// - public interface ITransientErrorDetectionStrategy - { - /// - /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. - /// - /// The exception object to be verified. - /// True if the specified exception is considered as transient, otherwise false. - bool IsTransient(Exception ex); - } + /// The exception object to be verified. + /// True if the specified exception is considered as transient, otherwise false. + bool IsTransient(Exception ex); } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs index 57b14bf0a9..c6a3976637 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryDbConnection.cs @@ -1,239 +1,195 @@ -using System; using System.Data; using System.Data.Common; using System.Diagnostics.CodeAnalysis; -using Transaction = System.Transactions.Transaction; +using System.Transactions; +using IsolationLevel = System.Data.IsolationLevel; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +public class RetryDbConnection : DbConnection { - public class RetryDbConnection : DbConnection + private readonly RetryPolicy? _cmdRetryPolicy; + private readonly RetryPolicy _conRetryPolicy; + + public RetryDbConnection(DbConnection connection, RetryPolicy? conRetryPolicy, RetryPolicy? cmdRetryPolicy) { - private DbConnection _inner; - private readonly RetryPolicy _conRetryPolicy; - private readonly RetryPolicy? _cmdRetryPolicy; + Inner = connection; + Inner.StateChange += StateChangeHandler; - public RetryDbConnection(DbConnection connection, RetryPolicy? conRetryPolicy, RetryPolicy? cmdRetryPolicy) + _conRetryPolicy = conRetryPolicy ?? RetryPolicy.NoRetry; + _cmdRetryPolicy = cmdRetryPolicy; + } + + public DbConnection Inner { get; } + + [AllowNull] + public override string ConnectionString + { + get => Inner.ConnectionString; + set => Inner.ConnectionString = value; + } + + public override int ConnectionTimeout => Inner.ConnectionTimeout; + + protected override bool CanRaiseEvents => true; + + public override string DataSource => Inner.DataSource; + + public override string Database => Inner.Database; + + public override string ServerVersion => Inner.ServerVersion; + + public override ConnectionState State => Inner.State; + + public override void ChangeDatabase(string databaseName) => Inner.ChangeDatabase(databaseName); + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => + Inner.BeginTransaction(isolationLevel); + + public override void Close() => Inner.Close(); + + public override void EnlistTransaction(Transaction? transaction) => Inner.EnlistTransaction(transaction); + + protected override DbCommand CreateDbCommand() => + new FaultHandlingDbCommand(this, Inner.CreateCommand(), _cmdRetryPolicy); + + protected override void Dispose(bool disposing) + { + if (disposing && Inner != null) { - _inner = connection; - _inner.StateChange += StateChangeHandler; - - _conRetryPolicy = conRetryPolicy ?? RetryPolicy.NoRetry; - _cmdRetryPolicy = cmdRetryPolicy; + Inner.StateChange -= StateChangeHandler; + Inner.Dispose(); } - public DbConnection Inner { get { return _inner; } } + base.Dispose(disposing); + } - [AllowNull] - public override string ConnectionString { get { return _inner.ConnectionString; } set { _inner.ConnectionString = value; } } + public override DataTable GetSchema() => Inner.GetSchema(); - protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + public override DataTable GetSchema(string collectionName) => Inner.GetSchema(collectionName); + + public override DataTable GetSchema(string collectionName, string?[] restrictionValues) => + Inner.GetSchema(collectionName, restrictionValues); + + public override void Open() => _conRetryPolicy.ExecuteAction(Inner.Open); + + public void Ensure() + { + // verify whether or not the connection is valid and is open. This code may be retried therefore + // it is important to ensure that a connection is re-established should it have previously failed + if (State != ConnectionState.Open) { - return _inner.BeginTransaction(isolationLevel); - } - - protected override bool CanRaiseEvents - { - get { return true; } - } - - public override void ChangeDatabase(string databaseName) - { - _inner.ChangeDatabase(databaseName); - } - - public override void Close() - { - _inner.Close(); - } - - public override int ConnectionTimeout - { - get { return _inner.ConnectionTimeout; } - } - - protected override DbCommand CreateDbCommand() - { - return new FaultHandlingDbCommand(this, _inner.CreateCommand(), _cmdRetryPolicy); - } - - public override string DataSource - { - get { return _inner.DataSource; } - } - - public override string Database - { - get { return _inner.Database; } - } - - protected override void Dispose(bool disposing) - { - if (disposing && _inner != null) - { - _inner.StateChange -= StateChangeHandler; - _inner.Dispose(); - } - base.Dispose(disposing); - } - - public override void EnlistTransaction(Transaction? transaction) - { - _inner.EnlistTransaction(transaction); - } - - public override DataTable GetSchema() - { - return _inner.GetSchema(); - } - - public override DataTable GetSchema(string collectionName) - { - return _inner.GetSchema(collectionName); - } - - public override DataTable GetSchema(string collectionName, string?[] restrictionValues) - { - return _inner.GetSchema(collectionName, restrictionValues); - } - - public override void Open() - { - _conRetryPolicy.ExecuteAction(_inner.Open); - } - - public override string ServerVersion - { - get { return _inner.ServerVersion; } - } - - public override ConnectionState State - { - get { return _inner.State; } - } - - private void StateChangeHandler(object sender, StateChangeEventArgs stateChangeEventArguments) - { - OnStateChange(stateChangeEventArguments); - } - - public void Ensure() - { - // verify whether or not the connection is valid and is open. This code may be retried therefore - // it is important to ensure that a connection is re-established should it have previously failed - if (State != ConnectionState.Open) - Open(); + Open(); } } - class FaultHandlingDbCommand : DbCommand - { - private RetryDbConnection _connection; - private DbCommand _inner; - private readonly RetryPolicy _cmdRetryPolicy; - - public FaultHandlingDbCommand(RetryDbConnection connection, DbCommand command, RetryPolicy? cmdRetryPolicy) - { - _connection = connection; - _inner = command; - _cmdRetryPolicy = cmdRetryPolicy ?? RetryPolicy.NoRetry; - } - - public DbCommand Inner => _inner; - - protected override void Dispose(bool disposing) - { - if (disposing) - _inner.Dispose(); - _inner = null!; - base.Dispose(disposing); - } - - public override void Cancel() - { - _inner.Cancel(); - } - - [AllowNull] - public override string CommandText - { - get => _inner.CommandText; - set => _inner.CommandText = value; - } - - public override int CommandTimeout - { - get => _inner.CommandTimeout; - set => _inner.CommandTimeout = value; - } - - public override CommandType CommandType - { - get => _inner.CommandType; - set => _inner.CommandType = value; - } - - [AllowNull] - protected override DbConnection DbConnection - { - get => _connection; - set - { - if (value == null) throw new ArgumentNullException(nameof(value)); - if (!(value is RetryDbConnection connection)) throw new ArgumentException("Value is not a FaultHandlingDbConnection instance."); - if (_connection != null && _connection != connection) throw new Exception("Value is another FaultHandlingDbConnection instance."); - _connection = connection; - _inner.Connection = connection.Inner; - } - } - - protected override DbParameter CreateDbParameter() - { - return _inner.CreateParameter(); - } - - protected override DbParameterCollection DbParameterCollection => _inner.Parameters; - - protected override DbTransaction? DbTransaction - { - get => _inner.Transaction; - set => _inner.Transaction = value; - } - - public override bool DesignTimeVisible { get; set; } - - protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) - { - return Execute(() => _inner.ExecuteReader(behavior)); - } - - public override int ExecuteNonQuery() - { - return Execute(() => _inner.ExecuteNonQuery()); - } - - public override object? ExecuteScalar() - { - return Execute(() => _inner.ExecuteScalar()); - } - - private T Execute(Func f) - { - return _cmdRetryPolicy.ExecuteAction(() => - { - _connection.Ensure(); - return f(); - })!; - } - - public override void Prepare() - { - _inner.Prepare(); - } - - public override UpdateRowSource UpdatedRowSource - { - get => _inner.UpdatedRowSource; - set => _inner.UpdatedRowSource = value; - } - } + private void StateChangeHandler(object sender, StateChangeEventArgs stateChangeEventArguments) => + OnStateChange(stateChangeEventArguments); +} + +internal class FaultHandlingDbCommand : DbCommand +{ + private readonly RetryPolicy _cmdRetryPolicy; + private RetryDbConnection _connection; + + public FaultHandlingDbCommand(RetryDbConnection connection, DbCommand command, RetryPolicy? cmdRetryPolicy) + { + _connection = connection; + Inner = command; + _cmdRetryPolicy = cmdRetryPolicy ?? RetryPolicy.NoRetry; + } + + public DbCommand Inner { get; private set; } + + [AllowNull] + public override string CommandText + { + get => Inner.CommandText; + set => Inner.CommandText = value; + } + + public override int CommandTimeout + { + get => Inner.CommandTimeout; + set => Inner.CommandTimeout = value; + } + + public override CommandType CommandType + { + get => Inner.CommandType; + set => Inner.CommandType = value; + } + + public override bool DesignTimeVisible { get; set; } + + [AllowNull] + protected override DbConnection DbConnection + { + get => _connection; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (!(value is RetryDbConnection connection)) + { + throw new ArgumentException("Value is not a FaultHandlingDbConnection instance."); + } + + if (_connection != null && _connection != connection) + { + throw new Exception("Value is another FaultHandlingDbConnection instance."); + } + + _connection = connection; + Inner.Connection = connection.Inner; + } + } + + protected override DbParameterCollection DbParameterCollection => Inner.Parameters; + + protected override DbTransaction? DbTransaction + { + get => Inner.Transaction; + set => Inner.Transaction = value; + } + + public override UpdateRowSource UpdatedRowSource + { + get => Inner.UpdatedRowSource; + set => Inner.UpdatedRowSource = value; + } + + public override void Cancel() => Inner.Cancel(); + + public override int ExecuteNonQuery() => Execute(() => Inner.ExecuteNonQuery()); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Inner.Dispose(); + } + + Inner = null!; + base.Dispose(disposing); + } + + protected override DbParameter CreateDbParameter() => Inner.CreateParameter(); + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => + Execute(() => Inner.ExecuteReader(behavior)); + + public override object? ExecuteScalar() => Execute(() => Inner.ExecuteScalar()); + + public override void Prepare() => Inner.Prepare(); + + private T Execute(Func f) => + _cmdRetryPolicy.ExecuteAction(() => + { + _connection.Ensure(); + return f(); + })!; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs index 78c8ab9c25..3d8021a660 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryLimitExceededException.cs @@ -1,54 +1,64 @@ -using System; using System.Runtime.Serialization; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// The special type of exception that provides managed exit from a retry loop. The user code can use this exception to +/// notify the retry policy that no further retry attempts are required. +/// +/// +[Serializable] +public sealed class RetryLimitExceededException : Exception { /// - /// The special type of exception that provides managed exit from a retry loop. The user code can use this exception to notify the retry policy that no further retry attempts are required. + /// Initializes a new instance of the class with a default error message. /// - /// - [Serializable] - public sealed class RetryLimitExceededException : Exception + public RetryLimitExceededException() { - /// - /// Initializes a new instance of the class with a default error message. - /// - public RetryLimitExceededException() - : base() - { } + } - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public RetryLimitExceededException(string message) - : base(message) - { } + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public RetryLimitExceededException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a reference to the inner exception that is the cause of this exception. - /// - /// The exception that is the cause of the current exception. - public RetryLimitExceededException(Exception innerException) - : base(null, innerException) - { } + /// + /// Initializes a new instance of the class with a reference to the inner + /// exception that is the cause of this exception. + /// + /// The exception that is the cause of the current exception. + public RetryLimitExceededException(Exception innerException) + : base(null, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - /// The exception that is the cause of the current exception. - public RetryLimitExceededException(string message, Exception innerException) - : base(message, innerException) - { } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public RetryLimitExceededException(string message, Exception innerException) + : base(message, innerException) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - private RetryLimitExceededException(SerializationInfo info, StreamingContext context) - : base(info, context) - { } + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized object + /// data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + private RetryLimitExceededException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs index 82e4f20c50..716906f5b8 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicy.cs @@ -1,239 +1,262 @@ -using System; -using System.Threading; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// Provides the base implementation of the retry mechanism for unreliable actions and transient conditions. +/// +public class RetryPolicy { /// - /// Provides the base implementation of the retry mechanism for unreliable actions and transient conditions. + /// Returns a default policy that does no retries, it just invokes action exactly once. /// - public class RetryPolicy + public static readonly RetryPolicy NoRetry = new(new TransientErrorIgnoreStrategy(), 0); + + /// + /// Returns a default policy that implements a fixed retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy DefaultFixed = new(new TransientErrorCatchAllStrategy(), new FixedInterval()); + + /// + /// Returns a default policy that implements a progressive retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy + DefaultProgressive = new(new TransientErrorCatchAllStrategy(), new Incremental()); + + /// + /// Returns a default policy that implements a random exponential retry interval configured with the default + /// retry strategy. + /// The default retry policy treats all caught exceptions as transient errors. + /// + public static readonly RetryPolicy DefaultExponential = + new(new TransientErrorCatchAllStrategy(), new ExponentialBackoff()); + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters + /// defining the progressive delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The retry strategy to use for this retry policy. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, RetryStrategy retryStrategy) + { + // Guard.ArgumentNotNull(errorDetectionStrategy, "errorDetectionStrategy"); + // Guard.ArgumentNotNull(retryStrategy, "retryPolicy"); + ErrorDetectionStrategy = errorDetectionStrategy; + + if (errorDetectionStrategy == null) + { + throw new InvalidOperationException( + "The error detection strategy type must implement the ITransientErrorDetectionStrategy interface."); + } + + RetryStrategy = retryStrategy; + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and default fixed + /// time interval between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount) + : this(errorDetectionStrategy, new FixedInterval(retryCount)) + { + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and fixed time + /// interval between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The interval between retries. + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan retryInterval) + : this(errorDetectionStrategy, new FixedInterval(retryCount, retryInterval)) + { + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and back-off + /// parameters for calculating the exponential delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The minimum back-off time. + /// The maximum back-off time. + /// + /// The time value that will be used for calculating a random delta in the exponential delay + /// between retries. + /// + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(errorDetectionStrategy, new ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff)) + { + } + + /// + /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters + /// defining the progressive delay between retries. + /// + /// + /// The that is responsible for + /// detecting transient conditions. + /// + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(errorDetectionStrategy, new Incremental(retryCount, initialInterval, increment)) + { + } + + /// + /// An instance of a callback delegate that will be invoked whenever a retry condition is encountered. + /// + public event EventHandler? Retrying; + + /// + /// Gets the retry strategy. + /// + public RetryStrategy RetryStrategy { get; } + + /// + /// Gets the instance of the error detection strategy. + /// + public ITransientErrorDetectionStrategy ErrorDetectionStrategy { get; } + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// A delegate representing the executable action which doesn't return any results. + public virtual void ExecuteAction(Action action) => + + // Guard.ArgumentNotNull(action, "action"); + ExecuteAction(() => + { + action(); + return default(object); + }); + + /// + /// Repetitively executes the specified action while it satisfies the current retry policy. + /// + /// The type of result expected from the executable action. + /// A delegate representing the executable action which returns the result of type R. + /// The result from the action. + public virtual TResult? ExecuteAction(Func func) + { + // Guard.ArgumentNotNull(func, "func"); + var retryCount = 0; + TimeSpan delay = TimeSpan.Zero; + Exception? lastError; + + ShouldRetry shouldRetry = RetryStrategy.GetShouldRetry(); + + for (; ;) + { + lastError = null; + + try + { + return func(); + } + catch (RetryLimitExceededException limitExceededEx) + { + // The user code can throw a RetryLimitExceededException to force the exit from the retry loop. + // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception + // which we will have to throw up the stack so that callers can handle it. + if (limitExceededEx.InnerException != null) + { + throw limitExceededEx.InnerException; + } + + return default; + } + catch (Exception ex) + { + lastError = ex; + + if (!(ErrorDetectionStrategy.IsTransient(lastError) && shouldRetry(retryCount++, lastError, out delay))) + { + throw; + } + } + + // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 that will block a thread indefinitely. + // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault that will be thrown by Thread.Sleep. + if (delay.TotalMilliseconds < 0) + { + delay = TimeSpan.Zero; + } + + OnRetrying(retryCount, lastError, delay); + + if (retryCount > 1 || !RetryStrategy.FastFirstRetry) + { + Thread.Sleep(delay); + } + } + } + + /// + /// Notifies the subscribers whenever a retry condition is encountered. + /// + /// The current retry attempt count. + /// The exception which caused the retry conditions to occur. + /// + /// The delay indicating how long the current thread will be suspended for before the next iteration + /// will be invoked. + /// + protected virtual void OnRetrying(int retryCount, Exception lastError, TimeSpan delay) + { + Retrying?.Invoke(this, new RetryingEventArgs(retryCount, delay, lastError)); + } + + #region Private classes + + /// + /// Implements a strategy that ignores any transient errors. + /// + private sealed class TransientErrorIgnoreStrategy : ITransientErrorDetectionStrategy { /// - /// Returns a default policy that does no retries, it just invokes action exactly once. + /// Always return false. /// - public static readonly RetryPolicy NoRetry = new RetryPolicy(new TransientErrorIgnoreStrategy(), 0); - - /// - /// Returns a default policy that implements a fixed retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultFixed = new RetryPolicy(new TransientErrorCatchAllStrategy(), new FixedInterval()); - - /// - /// Returns a default policy that implements a progressive retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultProgressive = new RetryPolicy(new TransientErrorCatchAllStrategy(), new Incremental()); - - /// - /// Returns a default policy that implements a random exponential retry interval configured with the default retry strategy. - /// The default retry policy treats all caught exceptions as transient errors. - /// - public static readonly RetryPolicy DefaultExponential = new RetryPolicy(new TransientErrorCatchAllStrategy(), new ExponentialBackoff()); - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The retry strategy to use for this retry policy. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, RetryStrategy retryStrategy) - { - //Guard.ArgumentNotNull(errorDetectionStrategy, "errorDetectionStrategy"); - //Guard.ArgumentNotNull(retryStrategy, "retryPolicy"); - - this.ErrorDetectionStrategy = errorDetectionStrategy; - - if (errorDetectionStrategy == null) - { - throw new InvalidOperationException("The error detection strategy type must implement the ITransientErrorDetectionStrategy interface."); - } - - this.RetryStrategy = retryStrategy; - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and default fixed time interval between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount) - : this(errorDetectionStrategy, new FixedInterval(retryCount)) - { - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and fixed time interval between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The interval between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan retryInterval) - : this(errorDetectionStrategy, new FixedInterval(retryCount, retryInterval)) - { - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and back-off parameters for calculating the exponential delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The minimum back-off time. - /// The maximum back-off time. - /// The time value that will be used for calculating a random delta in the exponential delay between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(errorDetectionStrategy, new ExponentialBackoff(retryCount, minBackoff, maxBackoff, deltaBackoff)) - { - } - - /// - /// Initializes a new instance of the RetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries. - /// - /// The that is responsible for detecting transient conditions. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public RetryPolicy(ITransientErrorDetectionStrategy errorDetectionStrategy, int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(errorDetectionStrategy, new Incremental(retryCount, initialInterval, increment)) - { - } - - /// - /// An instance of a callback delegate that will be invoked whenever a retry condition is encountered. - /// - public event EventHandler? Retrying; - - /// - /// Gets the retry strategy. - /// - public RetryStrategy RetryStrategy { get; private set; } - - /// - /// Gets the instance of the error detection strategy. - /// - public ITransientErrorDetectionStrategy ErrorDetectionStrategy { get; private set; } - - /// - /// Repetitively executes the specified action while it satisfies the current retry policy. - /// - /// A delegate representing the executable action which doesn't return any results. - public virtual void ExecuteAction(Action action) - { - //Guard.ArgumentNotNull(action, "action"); - - this.ExecuteAction(() => { action(); return default(object); }); - } - - /// - /// Repetitively executes the specified action while it satisfies the current retry policy. - /// - /// The type of result expected from the executable action. - /// A delegate representing the executable action which returns the result of type R. - /// The result from the action. - public virtual TResult? ExecuteAction(Func func) - { - //Guard.ArgumentNotNull(func, "func"); - - int retryCount = 0; - TimeSpan delay = TimeSpan.Zero; - Exception? lastError; - - var shouldRetry = this.RetryStrategy.GetShouldRetry(); - - for (; ; ) - { - lastError = null; - - try - { - return func(); - } - catch (RetryLimitExceededException limitExceededEx) - { - // The user code can throw a RetryLimitExceededException to force the exit from the retry loop. - // The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception - // which we will have to throw up the stack so that callers can handle it. - if (limitExceededEx.InnerException != null) - { - throw limitExceededEx.InnerException; - } - else - { - return default(TResult); - } - } - catch (Exception ex) - { - lastError = ex; - - if (!(this.ErrorDetectionStrategy.IsTransient(lastError) && shouldRetry(retryCount++, lastError, out delay))) - { - throw; - } - } - - // Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 that will block a thread indefinitely. - // In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault that will be thrown by Thread.Sleep. - if (delay.TotalMilliseconds < 0) - { - delay = TimeSpan.Zero; - } - - this.OnRetrying(retryCount, lastError, delay); - - if (retryCount > 1 || !this.RetryStrategy.FastFirstRetry) - { - Thread.Sleep(delay); - } - } - } - - /// - /// Notifies the subscribers whenever a retry condition is encountered. - /// - /// The current retry attempt count. - /// The exception which caused the retry conditions to occur. - /// The delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - protected virtual void OnRetrying(int retryCount, Exception lastError, TimeSpan delay) - { - if (this.Retrying != null) - { - this.Retrying(this, new RetryingEventArgs(retryCount, delay, lastError)); - } - } - - #region Private classes - /// - /// Implements a strategy that ignores any transient errors. - /// - private sealed class TransientErrorIgnoreStrategy : ITransientErrorDetectionStrategy - { - /// - /// Always return false. - /// - /// The exception. - /// Returns false. - public bool IsTransient(Exception ex) - { - return false; - } - } - - /// - /// Implements a strategy that treats all exceptions as transient errors. - /// - private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy - { - /// - /// Always return true. - /// - /// The exception. - /// Returns true. - public bool IsTransient(Exception ex) - { - return true; - } - } - #endregion + /// The exception. + /// Returns false. + public bool IsTransient(Exception ex) => false; } + + /// + /// Implements a strategy that treats all exceptions as transient errors. + /// + private sealed class TransientErrorCatchAllStrategy : ITransientErrorDetectionStrategy + { + /// + /// Always return true. + /// + /// The exception. + /// Returns true. + public bool IsTransient(Exception ex) => true; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs index 41f2337644..785c6cebe5 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryPolicyFactory.cs @@ -1,59 +1,56 @@ -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +// TODO: These should move to Persistence.SqlServer + +/// +/// Provides a factory class for instantiating application-specific retry policies. +/// +public static class RetryPolicyFactory { - // TODO: These should move to Persistence.SqlServer + public static RetryPolicy GetDefaultSqlConnectionRetryPolicyByConnectionString(string? connectionString) => - /// - /// Provides a factory class for instantiating application-specific retry policies. - /// - public static class RetryPolicyFactory + // Is this really the best way to determine if the database is an Azure database? + connectionString?.Contains("database.windows.net") ?? false + ? GetDefaultSqlAzureConnectionRetryPolicy() + : GetDefaultSqlConnectionRetryPolicy(); + + public static RetryPolicy GetDefaultSqlConnectionRetryPolicy() { - public static RetryPolicy GetDefaultSqlConnectionRetryPolicyByConnectionString(string? connectionString) - { - //Is this really the best way to determine if the database is an Azure database? - return connectionString?.Contains("database.windows.net") ?? false - ? GetDefaultSqlAzureConnectionRetryPolicy() - : GetDefaultSqlConnectionRetryPolicy(); - } + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); - public static RetryPolicy GetDefaultSqlConnectionRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultExponential; - var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); + return retryPolicy; + } - return retryPolicy; - } + public static RetryPolicy GetDefaultSqlAzureConnectionRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultExponential; + var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); + return retryPolicy; + } - public static RetryPolicy GetDefaultSqlAzureConnectionRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultExponential; - var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); - return retryPolicy; - } + public static RetryPolicy GetDefaultSqlCommandRetryPolicyByConnectionString(string? connectionString) => - public static RetryPolicy GetDefaultSqlCommandRetryPolicyByConnectionString(string? connectionString) - { - //Is this really the best way to determine if the database is an Azure database? - return connectionString?.Contains("database.windows.net") ?? false - ? GetDefaultSqlAzureCommandRetryPolicy() - : GetDefaultSqlCommandRetryPolicy(); - } + // Is this really the best way to determine if the database is an Azure database? + connectionString?.Contains("database.windows.net") ?? false + ? GetDefaultSqlAzureCommandRetryPolicy() + : GetDefaultSqlCommandRetryPolicy(); - public static RetryPolicy GetDefaultSqlCommandRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultFixed; - var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); + public static RetryPolicy GetDefaultSqlCommandRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultFixed; + var retryPolicy = new RetryPolicy(new NetworkConnectivityErrorDetectionStrategy(), retryStrategy); - return retryPolicy; - } + return retryPolicy; + } - public static RetryPolicy GetDefaultSqlAzureCommandRetryPolicy() - { - var retryStrategy = RetryStrategy.DefaultFixed; - var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); + public static RetryPolicy GetDefaultSqlAzureCommandRetryPolicy() + { + RetryStrategy retryStrategy = RetryStrategy.DefaultFixed; + var retryPolicy = new RetryPolicy(new SqlAzureTransientErrorDetectionStrategy(), retryStrategy); - return retryPolicy; - } + return retryPolicy; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs index 3f120261d7..81ad045641 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryStrategy.cs @@ -1,5 +1,4 @@ -using System; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; +using Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling { @@ -17,7 +16,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling /// public abstract class RetryStrategy { - #region Public members /// /// The default number of retry attempts. /// @@ -54,8 +52,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling /// public static readonly bool DefaultFirstFastRetry = true; - #endregion - /// /// Returns a default policy that does no retries, it just invokes action exactly once. /// diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs index 456dc87391..4dd473d97d 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/RetryingEventArgs.cs @@ -1,40 +1,40 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +/// +/// Contains information required for the event. +/// +public class RetryingEventArgs : EventArgs { /// - /// Contains information required for the event. + /// Initializes a new instance of the class. /// - public class RetryingEventArgs : EventArgs + /// The current retry attempt count. + /// + /// The delay indicating how long the current thread will be suspended for before the next iteration + /// will be invoked. + /// + /// The exception which caused the retry conditions to occur. + public RetryingEventArgs(int currentRetryCount, TimeSpan delay, Exception lastException) { - /// - /// Initializes a new instance of the class. - /// - /// The current retry attempt count. - /// The delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - /// The exception which caused the retry conditions to occur. - public RetryingEventArgs(int currentRetryCount, TimeSpan delay, Exception lastException) - { - //Guard.ArgumentNotNull(lastException, "lastException"); - - this.CurrentRetryCount = currentRetryCount; - this.Delay = delay; - this.LastException = lastException; - } - - /// - /// Gets the current retry count. - /// - public int CurrentRetryCount { get; private set; } - - /// - /// Gets the delay indicating how long the current thread will be suspended for before the next iteration will be invoked. - /// - public TimeSpan Delay { get; private set; } - - /// - /// Gets the exception which caused the retry conditions to occur. - /// - public Exception LastException { get; private set; } + // Guard.ArgumentNotNull(lastException, "lastException"); + CurrentRetryCount = currentRetryCount; + Delay = delay; + LastException = lastException; } + + /// + /// Gets the current retry count. + /// + public int CurrentRetryCount { get; } + + /// + /// Gets the delay indicating how long the current thread will be suspended for before the next iteration will be + /// invoked. + /// + public TimeSpan Delay { get; } + + /// + /// Gets the exception which caused the retry conditions to occur. + /// + public Exception LastException { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs index 91dcaf9feb..ba19fee617 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/ExponentialBackoff.cs @@ -1,100 +1,104 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with back-off parameters for calculating the exponential delay between retries. +/// +public class ExponentialBackoff : RetryStrategy { + private readonly TimeSpan _deltaBackoff; + private readonly TimeSpan _maxBackoff; + private readonly TimeSpan _minBackoff; + private readonly int _retryCount; + /// - /// A retry strategy with back-off parameters for calculating the exponential delay between retries. + /// Initializes a new instance of the class. /// - public class ExponentialBackoff : RetryStrategy + public ExponentialBackoff() + : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) { - private readonly int retryCount; - private readonly TimeSpan minBackoff; - private readonly TimeSpan maxBackoff; - private readonly TimeSpan deltaBackoff; - - /// - /// Initializes a new instance of the class. - /// - public ExponentialBackoff() - : this(DefaultClientRetryCount, DefaultMinBackoff, DefaultMaxBackoff, DefaultClientBackoff) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) - : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the retry strategy. - /// The maximum number of retry attempts. - /// The minimum back-off time - /// The maximum back-off time. - /// The value that will be used for calculating a random delta in the exponential delay between retries. - /// - /// Indicates whether or not the very first retry attempt will be made immediately - /// whereas the subsequent retries will remain subject to retry interval. - /// - public ExponentialBackoff(string? name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff"); - //Guard.ArgumentNotNegativeValue(maxBackoff.Ticks, "maxBackoff"); - //Guard.ArgumentNotNegativeValue(deltaBackoff.Ticks, "deltaBackoff"); - //Guard.ArgumentNotGreaterThan(minBackoff.TotalMilliseconds, maxBackoff.TotalMilliseconds, "minBackoff"); - - this.retryCount = retryCount; - this.minBackoff = minBackoff; - this.maxBackoff = maxBackoff; - this.deltaBackoff = deltaBackoff; - } - - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() - { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) - { - if (currentRetryCount < this.retryCount) - { - var random = new Random(); - - var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next((int)(this.deltaBackoff.TotalMilliseconds * 0.8), (int)(this.deltaBackoff.TotalMilliseconds * 1.2))); - var interval = (int)Math.Min(checked(this.minBackoff.TotalMilliseconds + delta), this.maxBackoff.TotalMilliseconds); - - retryInterval = TimeSpan.FromMilliseconds(interval); - - return true; - } - - retryInterval = TimeSpan.Zero; - return false; - }; - } } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + public ExponentialBackoff(int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(null, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + public ExponentialBackoff(string name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff) + : this(name, retryCount, minBackoff, maxBackoff, deltaBackoff, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the retry strategy. + /// The maximum number of retry attempts. + /// The minimum back-off time + /// The maximum back-off time. + /// + /// The value that will be used for calculating a random delta in the exponential delay between + /// retries. + /// + /// + /// Indicates whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public ExponentialBackoff(string? name, int retryCount, TimeSpan minBackoff, TimeSpan maxBackoff, TimeSpan deltaBackoff, bool firstFastRetry) + : base(name, firstFastRetry) + { + // Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + // Guard.ArgumentNotNegativeValue(minBackoff.Ticks, "minBackoff"); + // Guard.ArgumentNotNegativeValue(maxBackoff.Ticks, "maxBackoff"); + // Guard.ArgumentNotNegativeValue(deltaBackoff.Ticks, "deltaBackoff"); + // Guard.ArgumentNotGreaterThan(minBackoff.TotalMilliseconds, maxBackoff.TotalMilliseconds, "minBackoff"); + this._retryCount = retryCount; + this._minBackoff = minBackoff; + this._maxBackoff = maxBackoff; + this._deltaBackoff = deltaBackoff; + } + + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() => + delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) + { + if (currentRetryCount < _retryCount) + { + var random = new Random(); + + var delta = (int)((Math.Pow(2.0, currentRetryCount) - 1.0) * random.Next( + (int)(_deltaBackoff.TotalMilliseconds * 0.8), (int)(_deltaBackoff.TotalMilliseconds * 1.2))); + var interval = (int)Math.Min(_minBackoff.TotalMilliseconds + delta, _maxBackoff.TotalMilliseconds); + + retryInterval = TimeSpan.FromMilliseconds(interval); + + return true; + } + + retryInterval = TimeSpan.Zero; + return false; + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs index 546b10b55a..a798a5866a 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/FixedInterval.cs @@ -1,96 +1,95 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with a specified number of retry attempts and a default fixed time interval between retries. +/// +public class FixedInterval : RetryStrategy { + private readonly int _retryCount; + private readonly TimeSpan _retryInterval; + /// - /// A retry strategy with a specified number of retry attempts and a default fixed time interval between retries. + /// Initializes a new instance of the class. /// - public class FixedInterval : RetryStrategy + public FixedInterval() + : this(DefaultClientRetryCount) { - private readonly int retryCount; - private readonly TimeSpan retryInterval; + } - /// - /// Initializes a new instance of the class. - /// - public FixedInterval() - : this(DefaultClientRetryCount) + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + public FixedInterval(int retryCount) + : this(retryCount, DefaultRetryInterval) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + /// The time interval between retries. + public FixedInterval(int retryCount, TimeSpan retryInterval) + : this(null, retryCount, retryInterval, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The time interval between retries. + public FixedInterval(string name, int retryCount, TimeSpan retryInterval) + : this(name, retryCount, retryInterval, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The time interval between retries. + /// + /// a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public FixedInterval(string? name, int retryCount, TimeSpan retryInterval, bool firstFastRetry) + : base(name, firstFastRetry) + { + // Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + // Guard.ArgumentNotNegativeValue(retryInterval.Ticks, "retryInterval"); + this._retryCount = retryCount; + this._retryInterval = retryInterval; + } + + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() + { + if (_retryCount == 0) { - } - - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - public FixedInterval(int retryCount) - : this(retryCount, DefaultRetryInterval) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - /// The time interval between retries. - public FixedInterval(int retryCount, TimeSpan retryInterval) - : this(null, retryCount, retryInterval, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The time interval between retries. - public FixedInterval(string name, int retryCount, TimeSpan retryInterval) - : this(name, retryCount, retryInterval, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The time interval between retries. - /// a value indicating whether or not the very first retry attempt will be made immediately whereas the subsequent retries will remain subject to retry interval. - public FixedInterval(string? name, int retryCount, TimeSpan retryInterval, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(retryInterval.Ticks, "retryInterval"); - - this.retryCount = retryCount; - this.retryInterval = retryInterval; - } - - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() - { - if (this.retryCount == 0) - { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) - { - interval = TimeSpan.Zero; - return false; - }; - } - return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) { - if (currentRetryCount < this.retryCount) - { - interval = this.retryInterval; - return true; - } - interval = TimeSpan.Zero; return false; }; } + + return delegate(int currentRetryCount, Exception lastException, out TimeSpan interval) + { + if (currentRetryCount < _retryCount) + { + interval = _retryInterval; + return true; + } + + interval = TimeSpan.Zero; + return false; + }; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs index 1848436ae1..91fda41d00 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/Incremental.cs @@ -1,86 +1,93 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +/// +/// A retry strategy with a specified number of retry attempts and an incremental time interval between retries. +/// +public class Incremental : RetryStrategy { + private readonly TimeSpan _increment; + private readonly TimeSpan _initialInterval; + private readonly int _retryCount; + /// - /// A retry strategy with a specified number of retry attempts and an incremental time interval between retries. + /// Initializes a new instance of the class. /// - public class Incremental : RetryStrategy + public Incremental() + : this(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement) { - private readonly int retryCount; - private readonly TimeSpan initialInterval; - private readonly TimeSpan increment; - - /// - /// Initializes a new instance of the class. - /// - public Incremental() - : this(DefaultClientRetryCount, DefaultRetryInterval, DefaultRetryIncrement) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public Incremental(int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(null, retryCount, initialInterval, increment) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment) - : this(name, retryCount, initialInterval, increment, DefaultFirstFastRetry) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The retry strategy name. - /// The number of retry attempts. - /// The initial interval that will apply for the first retry. - /// The incremental time value that will be used for calculating the progressive delay between retries. - /// a value indicating whether or not the very first retry attempt will be made immediately whereas the subsequent retries will remain subject to retry interval. - public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment, bool firstFastRetry) - : base(name, firstFastRetry) - { - //Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); - //Guard.ArgumentNotNegativeValue(initialInterval.Ticks, "initialInterval"); - //Guard.ArgumentNotNegativeValue(increment.Ticks, "increment"); - - this.retryCount = retryCount; - this.initialInterval = initialInterval; - this.increment = increment; - } - - /// - /// Returns the corresponding ShouldRetry delegate. - /// - /// The ShouldRetry delegate. - public override ShouldRetry GetShouldRetry() - { - return delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) - { - if (currentRetryCount < this.retryCount) - { - retryInterval = TimeSpan.FromMilliseconds(this.initialInterval.TotalMilliseconds + (this.increment.TotalMilliseconds * currentRetryCount)); - - return true; - } - - retryInterval = TimeSpan.Zero; - - return false; - }; - } } + + /// + /// Initializes a new instance of the class. + /// + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public Incremental(int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(null, retryCount, initialInterval, increment) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment) + : this(name, retryCount, initialInterval, increment, DefaultFirstFastRetry) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The retry strategy name. + /// The number of retry attempts. + /// The initial interval that will apply for the first retry. + /// + /// The incremental time value that will be used for calculating the progressive delay between + /// retries. + /// + /// + /// a value indicating whether or not the very first retry attempt will be made immediately + /// whereas the subsequent retries will remain subject to retry interval. + /// + public Incremental(string? name, int retryCount, TimeSpan initialInterval, TimeSpan increment, bool firstFastRetry) + : base(name, firstFastRetry) + { + // Guard.ArgumentNotNegativeValue(retryCount, "retryCount"); + // Guard.ArgumentNotNegativeValue(initialInterval.Ticks, "initialInterval"); + // Guard.ArgumentNotNegativeValue(increment.Ticks, "increment"); + this._retryCount = retryCount; + this._initialInterval = initialInterval; + this._increment = increment; + } + + /// + /// Returns the corresponding ShouldRetry delegate. + /// + /// The ShouldRetry delegate. + public override ShouldRetry GetShouldRetry() => + delegate(int currentRetryCount, Exception lastException, out TimeSpan retryInterval) + { + if (currentRetryCount < _retryCount) + { + retryInterval = TimeSpan.FromMilliseconds(_initialInterval.TotalMilliseconds + + (_increment.TotalMilliseconds * currentRetryCount)); + + return true; + } + + retryInterval = TimeSpan.Zero; + + return false; + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs index fc7bb72b6b..e046f8592d 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs @@ -1,31 +1,27 @@ -using System; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; + +/// +/// Implements a strategy that detects network connectivity errors such as host not found. +/// +public class NetworkConnectivityErrorDetectionStrategy : ITransientErrorDetectionStrategy { - /// - /// Implements a strategy that detects network connectivity errors such as host not found. - /// - public class NetworkConnectivityErrorDetectionStrategy : ITransientErrorDetectionStrategy + public bool IsTransient(Exception? ex) { - public bool IsTransient(Exception ex) + if (ex != null && ex is SqlException sqlException) { - SqlException? sqlException; - - if (ex != null && (sqlException = ex as SqlException) != null) + switch (sqlException.Number) { - switch (sqlException.Number) - { - // SQL Error Code: 11001 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL - // Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) - case 11001: - return true; - } + // SQL Error Code: 11001 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL + // Server is configured to allow remote connections. (provider: TCP Provider, error: 0 - No such host is known.) + case 11001: + return true; } - - return false; } + + return false; } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs index 2711ce4714..b4ae18d55a 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs @@ -1,174 +1,165 @@ -using System; -using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies; + +// See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues +// Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? +// but i guess that's not netcore so we'll just leave it. + +/// +/// Provides the transient error detection logic for transient faults that are specific to SQL Azure. +/// +public class SqlAzureTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy { - // See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues - // Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? - // but i guess that's not netcore so we'll just leave it. - /// - /// Provides the transient error detection logic for transient faults that are specific to SQL Azure. + /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// - public class SqlAzureTransientErrorDetectionStrategy : ITransientErrorDetectionStrategy + /// The exception object to be verified. + /// true if the specified exception is considered as transient; otherwise, false. + public bool IsTransient(Exception? ex) { - #region ProcessNetLibErrorCode enumeration - - /// - /// Error codes reported by the DBNETLIB module. - /// - private enum ProcessNetLibErrorCode + if (ex != null) { - ZeroBytes = -3, - - Timeout = -2, /* Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. */ - - Unknown = -1, - - InsufficientMemory = 1, - - AccessDenied = 2, - - ConnectionBusy = 3, - - ConnectionBroken = 4, - - ConnectionLimit = 5, - - ServerNotFound = 6, - - NetworkNotFound = 7, - - InsufficientResources = 8, - - NetworkBusy = 9, - - NetworkAccessDenied = 10, - - GeneralError = 11, - - IncorrectMode = 12, - - NameNotFound = 13, - - InvalidConnection = 14, - - ReadWriteError = 15, - - TooManyHandles = 16, - - ServerError = 17, - - SSLError = 18, - - EncryptionError = 19, - - EncryptionNotSupported = 20 - } - - #endregion - - #region ITransientErrorDetectionStrategy implementation - - /// - /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. - /// - /// The exception object to be verified. - /// true if the specified exception is considered as transient; otherwise, false. - public bool IsTransient(Exception ex) - { - if (ex != null) + SqlException? sqlException; + if ((sqlException = ex as SqlException) != null) { - SqlException? sqlException; - if ((sqlException = ex as SqlException) != null) + // Enumerate through all errors found in the exception. + foreach (SqlError err in sqlException.Errors) { - // Enumerate through all errors found in the exception. - foreach (SqlError err in sqlException.Errors) + switch (err.Number) { - switch (err.Number) - { - // SQL Error Code: 40501 - // The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). - case ThrottlingCondition.ThrottlingErrorNumber: - // Decode the reason code from the error message to determine the grounds for throttling. - var condition = ThrottlingCondition.FromError(err); + // SQL Error Code: 40501 + // The service is currently busy. Retry the request after 10 seconds. Code: (reason code to be decoded). + case ThrottlingCondition.ThrottlingErrorNumber: + // Decode the reason code from the error message to determine the grounds for throttling. + var condition = ThrottlingCondition.FromError(err); - // Attach the decoded values as additional attributes to the original SQL exception. - sqlException.Data[condition.ThrottlingMode.GetType().Name] = - condition.ThrottlingMode.ToString(); - sqlException.Data[condition.GetType().Name] = condition; + // Attach the decoded values as additional attributes to the original SQL exception. + sqlException.Data[condition.ThrottlingMode.GetType().Name] = + condition.ThrottlingMode.ToString(); + sqlException.Data[condition.GetType().Name] = condition; - return true; + return true; - // SQL Error Code: 10928 - // Resource ID: %d. The %s limit for the database is %d and has been reached. - case 10928: - // SQL Error Code: 10929 - // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. - // However, the server is currently too busy to support requests greater than %d for this database. - case 10929: - // SQL Error Code: 10053 - // A transport-level error has occurred when receiving results from the server. - // An established connection was aborted by the software in your host machine. - case 10053: - // SQL Error Code: 10054 - // A transport-level error has occurred when sending the request to the server. - // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) - case 10054: - // SQL Error Code: 10060 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server - // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed - // because the connected party did not properly respond after a period of time, or established connection failed - // because connected host has failed to respond.)"} - case 10060: - // SQL Error Code: 40197 - // The service has encountered an error processing your request. Please try again. - case 40197: - // SQL Error Code: 40540 - // The service has encountered an error processing your request. Please try again. - case 40540: - // SQL Error Code: 40613 - // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer - // support, and provide them the session tracing ID of ZZZZZ. - case 40613: - // SQL Error Code: 40143 - // The service has encountered an error processing your request. Please try again. - case 40143: - // SQL Error Code: 233 - // The client was unable to establish a connection because of an error during connection initialization process before login. - // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy - // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. - // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) - case 233: - // SQL Error Code: 64 - // A connection was successfully established with the server, but then an error occurred during the login process. - // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) - case 64: - // DBNETLIB Error Code: 20 - // The instance of SQL Server you attempted to connect to does not support encryption. - case (int)ProcessNetLibErrorCode.EncryptionNotSupported: - return true; - } + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + case 10928: + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // However, the server is currently too busy to support requests greater than %d for this database. + case 10929: + // SQL Error Code: 10053 + // A transport-level error has occurred when receiving results from the server. + // An established connection was aborted by the software in your host machine. + case 10053: + // SQL Error Code: 10054 + // A transport-level error has occurred when sending the request to the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 10054: + // SQL Error Code: 10060 + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed + // because connected host has failed to respond.)"} + case 10060: + // SQL Error Code: 40197 + // The service has encountered an error processing your request. Please try again. + case 40197: + // SQL Error Code: 40540 + // The service has encountered an error processing your request. Please try again. + case 40540: + // SQL Error Code: 40613 + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // support, and provide them the session tracing ID of ZZZZZ. + case 40613: + // SQL Error Code: 40143 + // The service has encountered an error processing your request. Please try again. + case 40143: + // SQL Error Code: 233 + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) + case 233: + // SQL Error Code: 64 + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + case 64: + // DBNETLIB Error Code: 20 + // The instance of SQL Server you attempted to connect to does not support encryption. + case (int)ProcessNetLibErrorCode.EncryptionNotSupported: + return true; } } - else if (ex is TimeoutException) - { - return true; - } - // else - // { - // EntityException entityException; - // if ((entityException = ex as EntityException) != null) - // { - // return this.IsTransient(entityException.InnerException); - // } - // } + } + else if (ex is TimeoutException) + { + return true; } - return false; + // else + // { + // EntityException entityException; + // if ((entityException = ex as EntityException) != null) + // { + // return this.IsTransient(entityException.InnerException); + // } + // } } - #endregion + return false; + } + + /// + /// Error codes reported by the DBNETLIB module. + /// + private enum ProcessNetLibErrorCode + { + ZeroBytes = -3, + + Timeout = -2, /* Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding. */ + + Unknown = -1, + + InsufficientMemory = 1, + + AccessDenied = 2, + + ConnectionBusy = 3, + + ConnectionBroken = 4, + + ConnectionLimit = 5, + + ServerNotFound = 6, + + NetworkNotFound = 7, + + InsufficientResources = 8, + + NetworkBusy = 9, + + NetworkAccessDenied = 10, + + GeneralError = 11, + + IncorrectMode = 12, + + NameNotFound = 13, + + InvalidConnection = 14, + + ReadWriteError = 15, + + TooManyHandles = 16, + + ServerError = 17, + + SSLError = 18, + + EncryptionError = 19, + + EncryptionNotSupported = 20, } } diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs index 96d42a9481..24ba741c06 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs @@ -1,330 +1,331 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling +namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling; + +/// +/// Defines the possible throttling modes in SQL Azure. +/// +public enum ThrottlingMode { /// - /// Defines the possible throttling modes in SQL Azure. + /// Corresponds to "No Throttling" throttling mode whereby all SQL statements can be processed. /// - public enum ThrottlingMode - { - /// - /// Corresponds to "No Throttling" throttling mode whereby all SQL statements can be processed. - /// - NoThrottling = 0, - - /// - /// Corresponds to "Reject Update / Insert" throttling mode whereby SQL statements such as INSERT, UPDATE, CREATE TABLE and CREATE INDEX are rejected. - /// - RejectUpdateInsert = 1, - - /// - /// Corresponds to "Reject All Writes" throttling mode whereby SQL statements such as INSERT, UPDATE, DELETE, CREATE, DROP are rejected. - /// - RejectAllWrites = 2, - - /// - /// Corresponds to "Reject All" throttling mode whereby all SQL statements are rejected. - /// - RejectAll = 3, - - /// - /// Corresponds to an unknown throttling mode whereby throttling mode cannot be determined with certainty. - /// - Unknown = -1 - } + NoThrottling = 0, /// - /// Defines the possible throttling types in SQL Azure. + /// Corresponds to "Reject Update / Insert" throttling mode whereby SQL statements such as INSERT, UPDATE, CREATE TABLE + /// and CREATE INDEX are rejected. /// - public enum ThrottlingType - { - /// - /// Indicates that no throttling was applied to a given resource. - /// - None = 0, - - /// - /// Corresponds to a Soft throttling type. Soft throttling is applied when machine resources such as, CPU, IO, storage, and worker threads exceed - /// predefined safety thresholds despite the load balancer’s best efforts. - /// - Soft = 1, - - /// - /// Corresponds to a Hard throttling type. Hard throttling is applied when the machine is out of resources, for example storage space. - /// With hard throttling, no new connections are allowed to the databases hosted on the machine until resources are freed up. - /// - Hard = 2, - - /// - /// Corresponds to an unknown throttling type in the event when the throttling type cannot be determined with certainty. - /// - Unknown = 3 - } + RejectUpdateInsert = 1, /// - /// Defines the types of resources in SQL Azure which may be subject to throttling conditions. + /// Corresponds to "Reject All Writes" throttling mode whereby SQL statements such as INSERT, UPDATE, DELETE, CREATE, + /// DROP are rejected. /// - public enum ThrottledResourceType - { - /// - /// Corresponds to "Physical Database Space" resource which may be subject to throttling. - /// - PhysicalDatabaseSpace = 0, - - /// - /// Corresponds to "Physical Log File Space" resource which may be subject to throttling. - /// - PhysicalLogSpace = 1, - - /// - /// Corresponds to "Transaction Log Write IO Delay" resource which may be subject to throttling. - /// - LogWriteIoDelay = 2, - - /// - /// Corresponds to "Database Read IO Delay" resource which may be subject to throttling. - /// - DataReadIoDelay = 3, - - /// - /// Corresponds to "CPU" resource which may be subject to throttling. - /// - Cpu = 4, - - /// - /// Corresponds to "Database Size" resource which may be subject to throttling. - /// - DatabaseSize = 5, - - /// - /// Corresponds to "SQL Worker Thread Pool" resource which may be subject to throttling. - /// - WorkerThreads = 7, - - /// - /// Corresponds to an internal resource which may be subject to throttling. - /// - Internal = 6, - - /// - /// Corresponds to an unknown resource type in the event when the actual resource cannot be determined with certainty. - /// - Unknown = -1 - } + RejectAllWrites = 2, /// - /// Implements an object holding the decoded reason code returned from SQL Azure when encountering throttling conditions. + /// Corresponds to "Reject All" throttling mode whereby all SQL statements are rejected. /// - [Serializable] - public class ThrottlingCondition + RejectAll = 3, + + /// + /// Corresponds to an unknown throttling mode whereby throttling mode cannot be determined with certainty. + /// + Unknown = -1, +} + +/// +/// Defines the possible throttling types in SQL Azure. +/// +public enum ThrottlingType +{ + /// + /// Indicates that no throttling was applied to a given resource. + /// + None = 0, + + /// + /// Corresponds to a Soft throttling type. Soft throttling is applied when machine resources such as, CPU, IO, storage, + /// and worker threads exceed + /// predefined safety thresholds despite the load balancer’s best efforts. + /// + Soft = 1, + + /// + /// Corresponds to a Hard throttling type. Hard throttling is applied when the machine is out of resources, for example + /// storage space. + /// With hard throttling, no new connections are allowed to the databases hosted on the machine until resources are + /// freed up. + /// + Hard = 2, + + /// + /// Corresponds to an unknown throttling type in the event when the throttling type cannot be determined with + /// certainty. + /// + Unknown = 3, +} + +/// +/// Defines the types of resources in SQL Azure which may be subject to throttling conditions. +/// +public enum ThrottledResourceType +{ + /// + /// Corresponds to "Physical Database Space" resource which may be subject to throttling. + /// + PhysicalDatabaseSpace = 0, + + /// + /// Corresponds to "Physical Log File Space" resource which may be subject to throttling. + /// + PhysicalLogSpace = 1, + + /// + /// Corresponds to "Transaction Log Write IO Delay" resource which may be subject to throttling. + /// + LogWriteIoDelay = 2, + + /// + /// Corresponds to "Database Read IO Delay" resource which may be subject to throttling. + /// + DataReadIoDelay = 3, + + /// + /// Corresponds to "CPU" resource which may be subject to throttling. + /// + Cpu = 4, + + /// + /// Corresponds to "Database Size" resource which may be subject to throttling. + /// + DatabaseSize = 5, + + /// + /// Corresponds to "SQL Worker Thread Pool" resource which may be subject to throttling. + /// + WorkerThreads = 7, + + /// + /// Corresponds to an internal resource which may be subject to throttling. + /// + Internal = 6, + + /// + /// Corresponds to an unknown resource type in the event when the actual resource cannot be determined with certainty. + /// + Unknown = -1, +} + +/// +/// Implements an object holding the decoded reason code returned from SQL Azure when encountering throttling +/// conditions. +/// +[Serializable] +public class ThrottlingCondition +{ + /// + /// Gets the error number that corresponds to throttling conditions reported by SQL Azure. + /// + public const int ThrottlingErrorNumber = 40501; + + /// + /// Provides a compiled regular expression used for extracting the reason code from the error message. + /// + private static readonly Regex _sqlErrorCodeRegEx = + new(@"Code:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Maintains a collection of key-value pairs where a key is resource type and a value is the type of throttling + /// applied to the given resource type. + /// + private readonly IList> _throttledResources = + new List>(9); + + /// + /// Gets an unknown throttling condition in the event the actual throttling condition cannot be determined. + /// + public static ThrottlingCondition Unknown { - /// - /// Gets the error number that corresponds to throttling conditions reported by SQL Azure. - /// - public const int ThrottlingErrorNumber = 40501; - - /// - /// Maintains a collection of key-value pairs where a key is resource type and a value is the type of throttling applied to the given resource type. - /// - private readonly IList> throttledResources = new List>(9); - - /// - /// Provides a compiled regular expression used for extracting the reason code from the error message. - /// - private static readonly Regex sqlErrorCodeRegEx = new Regex(@"Code:\s*(\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - /// - /// Gets an unknown throttling condition in the event the actual throttling condition cannot be determined. - /// - public static ThrottlingCondition Unknown + get { - get + var unknownCondition = new ThrottlingCondition { ThrottlingMode = ThrottlingMode.Unknown }; + unknownCondition._throttledResources.Add(Tuple.Create( + ThrottledResourceType.Unknown, + ThrottlingType.Unknown)); + + return unknownCondition; + } + } + + /// + /// Gets the value that reflects the throttling mode in SQL Azure. + /// + public ThrottlingMode ThrottlingMode { get; private set; } + + /// + /// Gets a list of resources in SQL Azure that were subject to throttling conditions. + /// + public IEnumerable> ThrottledResources => _throttledResources; + + /// + /// Gets a value indicating whether physical data file space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataSpace => + _throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalDatabaseSpace); + + /// + /// Gets a value indicating whether physical log space throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogSpace => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalLogSpace); + + /// + /// Gets a value indicating whether transaction activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnLogWrite => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.LogWriteIoDelay); + + /// + /// Gets a value indicating whether data read activity throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDataRead => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.DataReadIoDelay); + + /// + /// Gets a value indicating whether CPU throttling was reported by SQL Azure. + /// + public bool IsThrottledOnCpu => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.Cpu); + + /// + /// Gets a value indicating whether database size throttling was reported by SQL Azure. + /// + public bool IsThrottledOnDatabaseSize => _throttledResources.Any(x => x.Item1 == ThrottledResourceType.DatabaseSize); + + /// + /// Gets a value indicating whether concurrent requests throttling was reported by SQL Azure. + /// + public bool IsThrottledOnWorkerThreads => + _throttledResources.Any(x => x.Item1 == ThrottledResourceType.WorkerThreads); + + /// + /// Gets a value indicating whether throttling conditions were not determined with certainty. + /// + public bool IsUnknown => ThrottlingMode == ThrottlingMode.Unknown; + + /// + /// Determines throttling conditions from the specified SQL exception. + /// + /// + /// The object containing information relevant to an error returned by SQL + /// Server when encountering throttling conditions. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure upon encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromException(SqlException? ex) + { + if (ex != null) + { + foreach (SqlError error in ex.Errors) { - var unknownCondition = new ThrottlingCondition { ThrottlingMode = ThrottlingMode.Unknown }; - unknownCondition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Unknown, ThrottlingType.Unknown)); - - return unknownCondition; - } - } - - /// - /// Gets the value that reflects the throttling mode in SQL Azure. - /// - public ThrottlingMode ThrottlingMode { get; private set; } - - /// - /// Gets a list of resources in SQL Azure that were subject to throttling conditions. - /// - public IEnumerable> ThrottledResources - { - get { return this.throttledResources; } - } - - /// - /// Gets a value indicating whether physical data file space throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDataSpace - { - get { return throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalDatabaseSpace); } - } - - /// - /// Gets a value indicating whether physical log space throttling was reported by SQL Azure. - /// - public bool IsThrottledOnLogSpace - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.PhysicalLogSpace); } - } - - /// - /// Gets a value indicating whether transaction activity throttling was reported by SQL Azure. - /// - public bool IsThrottledOnLogWrite - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.LogWriteIoDelay); } - } - - /// - /// Gets a value indicating whether data read activity throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDataRead - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.DataReadIoDelay); } - } - - /// - /// Gets a value indicating whether CPU throttling was reported by SQL Azure. - /// - public bool IsThrottledOnCpu - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.Cpu); } - } - - /// - /// Gets a value indicating whether database size throttling was reported by SQL Azure. - /// - public bool IsThrottledOnDatabaseSize - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.DatabaseSize); } - } - - /// - /// Gets a value indicating whether concurrent requests throttling was reported by SQL Azure. - /// - public bool IsThrottledOnWorkerThreads - { - get { return this.throttledResources.Any(x => x.Item1 == ThrottledResourceType.WorkerThreads); } - } - - /// - /// Gets a value indicating whether throttling conditions were not determined with certainty. - /// - public bool IsUnknown - { - get { return ThrottlingMode == ThrottlingMode.Unknown; } - } - - /// - /// Determines throttling conditions from the specified SQL exception. - /// - /// The object containing information relevant to an error returned by SQL Server when encountering throttling conditions. - /// An instance of the object holding the decoded reason codes returned from SQL Azure upon encountering throttling conditions. - public static ThrottlingCondition FromException(SqlException ex) - { - if (ex != null) - { - foreach (SqlError error in ex.Errors) + if (error.Number == ThrottlingErrorNumber) { - if (error.Number == ThrottlingErrorNumber) - { - return FromError(error); - } + return FromError(error); } } - - return Unknown; } - /// - /// Determines the throttling conditions from the specified SQL error. - /// - /// The object containing information relevant to a warning or error returned by SQL Server. - /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. - public static ThrottlingCondition FromError(SqlError error) + return Unknown; + } + + /// + /// Determines the throttling conditions from the specified SQL error. + /// + /// + /// The object containing information relevant to a warning or error returned + /// by SQL Server. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromError(SqlError? error) + { + if (error != null) { - if (error != null) + Match match = _sqlErrorCodeRegEx.Match(error.Message); + + if (match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int reasonCode)) { - var match = sqlErrorCodeRegEx.Match(error.Message); - int reasonCode; - - if (match.Success && int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out reasonCode)) - { - return FromReasonCode(reasonCode); - } + return FromReasonCode(reasonCode); } - - return Unknown; } - /// - /// Determines the throttling conditions from the specified reason code. - /// - /// The reason code returned by SQL Azure which contains the throttling mode and the exceeded resource types. - /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering throttling conditions. - public static ThrottlingCondition FromReasonCode(int reasonCode) + return Unknown; + } + + /// + /// Determines the throttling conditions from the specified reason code. + /// + /// + /// The reason code returned by SQL Azure which contains the throttling mode and the exceeded + /// resource types. + /// + /// + /// An instance of the object holding the decoded reason codes returned from SQL Azure when encountering + /// throttling conditions. + /// + public static ThrottlingCondition FromReasonCode(int reasonCode) + { + if (reasonCode > 0) { - if (reasonCode > 0) - { - // Decode throttling mode from the last 2 bits. - var throttlingMode = (ThrottlingMode)(reasonCode & 3); + // Decode throttling mode from the last 2 bits. + var throttlingMode = (ThrottlingMode)(reasonCode & 3); - var condition = new ThrottlingCondition { ThrottlingMode = throttlingMode }; + var condition = new ThrottlingCondition { ThrottlingMode = throttlingMode }; - // Shift 8 bits to truncate throttling mode. - var groupCode = reasonCode >> 8; + // Shift 8 bits to truncate throttling mode. + var groupCode = reasonCode >> 8; - // Determine throttling type for all well-known resources that may be subject to throttling conditions. - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalDatabaseSpace, (ThrottlingType)(groupCode & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalLogSpace, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.LogWriteIoDelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DataReadIoDelay, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Cpu, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.DatabaseSize, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.WorkerThreads, (ThrottlingType)((groupCode = groupCode >> 2) & 3))); - condition.throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode >> 2) & 3))); + // Determine throttling type for all well-known resources that may be subject to throttling conditions. + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalDatabaseSpace, (ThrottlingType)(groupCode & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.PhysicalLogSpace, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.LogWriteIoDelay, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.DataReadIoDelay, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.Cpu, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.DatabaseSize, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.WorkerThreads, (ThrottlingType)((groupCode >>= 2) & 3))); + condition._throttledResources.Add(Tuple.Create(ThrottledResourceType.Internal, (ThrottlingType)((groupCode >> 2) & 3))); - return condition; - } - - return Unknown; + return condition; } - /// - /// Returns a textual representation the current ThrottlingCondition object including the information held with respect to throttled resources. - /// - /// A string that represents the current ThrottlingCondition object. - public override string ToString() - { - var result = new StringBuilder(); + return Unknown; + } - result.AppendFormat(CultureInfo.CurrentCulture, "Mode: {0} | ", ThrottlingMode); + /// + /// Returns a textual representation the current ThrottlingCondition object including the information held with respect + /// to throttled resources. + /// + /// A string that represents the current ThrottlingCondition object. + public override string ToString() + { + var result = new StringBuilder(); - var resources = - this.throttledResources - .Where(x => x.Item1 != ThrottledResourceType.Internal) - .Select(x => string.Format(CultureInfo.CurrentCulture, "{0}: {1}", x.Item1, x.Item2)) - .OrderBy(x => x).ToArray(); + result.AppendFormat(CultureInfo.CurrentCulture, "Mode: {0} | ", ThrottlingMode); - result.Append(string.Join(", ", resources)); + var resources = + _throttledResources + .Where(x => x.Item1 != ThrottledResourceType.Internal) + .Select(x => string.Format(CultureInfo.CurrentCulture, "{0}: {1}", x.Item1, x.Item2)) + .OrderBy(x => x).ToArray(); - return result.ToString(); - } + result.Append(string.Join(", ", resources)); + + return result.ToString(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs index 6a928b6859..fb2a15a01a 100644 --- a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; +namespace Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Cms.Infrastructure.Persistence +public interface IBulkSqlInsertProvider { - public interface IBulkSqlInsertProvider - { - string ProviderName { get; } - int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records); - } + string ProviderName { get; } + + int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs index 2d97cfbcd3..bf01728075 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseCreator.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence -{ - public interface IDatabaseCreator - { - string ProviderName { get; } +namespace Umbraco.Cms.Infrastructure.Persistence; - void Create(string connectionString); - } +public interface IDatabaseCreator +{ + string ProviderName { get; } + + void Create(string connectionString); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs index c766c50d69..1c06dd089f 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDatabaseProviderMetadata.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.Serialization; using Umbraco.Cms.Core.Install.Models; @@ -7,84 +6,84 @@ namespace Umbraco.Cms.Infrastructure.Persistence; public interface IDatabaseProviderMetadata { /// - /// Gets a unique identifier for this set of metadata used for filtering. + /// Gets a unique identifier for this set of metadata used for filtering. /// [DataMember(Name = "id")] Guid Id { get; } /// - /// Gets a value to determine display order and quick install priority. + /// Gets a value to determine display order and quick install priority. /// [DataMember(Name = "sortOrder")] int SortOrder { get; } /// - /// Gets a friendly name to describe the provider. + /// Gets a friendly name to describe the provider. /// [DataMember(Name = "displayName")] string DisplayName { get; } /// - /// Gets the default database name for the provider. + /// Gets the default database name for the provider. /// [DataMember(Name = "defaultDatabaseName")] string DefaultDatabaseName { get; } /// - /// Gets the database factory provider name. + /// Gets the database factory provider name. /// [DataMember(Name = "providerName")] string? ProviderName { get; } /// - /// Gets a value indicating whether can be used for one click install. + /// Gets a value indicating whether can be used for one click install. /// [DataMember(Name = "supportsQuickInstall")] bool SupportsQuickInstall { get; } /// - /// Gets a value indicating whether should be available for selection. + /// Gets a value indicating whether should be available for selection. /// [DataMember(Name = "isAvailable")] bool IsAvailable { get; } /// - /// Gets a value indicating whether the server/hostname field must be populated. + /// Gets a value indicating whether the server/hostname field must be populated. /// [DataMember(Name = "requiresServer")] bool RequiresServer { get; } /// - /// Gets a value used as input placeholder for server/hostnmae field. + /// Gets a value used as input placeholder for server/hostnmae field. /// [DataMember(Name = "serverPlaceholder")] string? ServerPlaceholder { get; } /// - /// Gets a value indicating whether a username and password are required (in general) to connect to the database + /// Gets a value indicating whether a username and password are required (in general) to connect to the database /// [DataMember(Name = "requiresCredentials")] bool RequiresCredentials { get; } /// - /// Gets a value indicating whether integrated authentication is supported (e.g. SQL Server & Oracle). + /// Gets a value indicating whether integrated authentication is supported (e.g. SQL Server & Oracle). /// [DataMember(Name = "supportsIntegratedAuthentication")] bool SupportsIntegratedAuthentication { get; } /// - /// Gets a value indicating whether the connection should be tested before continuing install process. + /// Gets a value indicating whether the connection should be tested before continuing install process. /// [DataMember(Name = "requiresConnectionTest")] bool RequiresConnectionTest { get; } /// - /// Gets a value indicating to ignore the value of GlobalSettings.InstallMissingDatabase + /// Gets a value indicating to ignore the value of GlobalSettings.InstallMissingDatabase /// public bool ForceCreateDatabase { get; } /// - /// Creates a connection string for this provider. + /// Creates a connection string for this provider. /// string? GenerateConnectionString(DatabaseModel databaseModel); } diff --git a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs index 4ee6ce7d59..4357a11063 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; using System.Data.Common; -using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IDbProviderFactoryCreator { + DbProviderFactory? CreateFactory(string? providerName); - public interface IDbProviderFactoryCreator - { - DbProviderFactory? CreateFactory(string? providerName); - ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); - IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); - void CreateDatabase(string providerName, string connectionString); - NPocoMapperCollection ProviderSpecificMappers(string providerName); + ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); - IEnumerable GetProviderSpecificInterceptors(string providerName) => - Enumerable.Empty(); - } + IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); + + void CreateDatabase(string providerName, string connectionString); + + NPocoMapperCollection ProviderSpecificMappers(string providerName); + + IEnumerable GetProviderSpecificInterceptors(string providerName) => + Enumerable.Empty(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs index 736ba80854..41af78253a 100644 --- a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificInterceptor.cs @@ -23,6 +23,6 @@ public interface IProviderSpecificDataInterceptor : IProviderSpecificInterceptor { } -public interface IProviderSpecificTransactionInterceptor: IProviderSpecificInterceptor, ITransactionInterceptor +public interface IProviderSpecificTransactionInterceptor : IProviderSpecificInterceptor, ITransactionInterceptor { } diff --git a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs index 3a73d647e8..ed463a1bb1 100644 --- a/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IProviderSpecificMapperFactory.cs @@ -1,8 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IProviderSpecificMapperFactory { - public interface IProviderSpecificMapperFactory - { - string ProviderName { get; } - NPocoMapperCollection Mappers { get; } - } + string ProviderName { get; } + + NPocoMapperCollection Mappers { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs index 29f1128a44..c937880a22 100644 --- a/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/IScalarMapper.cs @@ -1,13 +1,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence; /// -/// Provides a mapping function for +/// Provides a mapping function for /// public interface IScalarMapper { /// - /// Performs a mapping operation for a scalar value. + /// Performs a mapping operation for a scalar value. /// object Map(object value); } - diff --git a/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs b/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs index 9178ba8ae7..87bc1dd81f 100644 --- a/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs +++ b/src/Umbraco.Infrastructure/Persistence/ISqlContext.cs @@ -1,53 +1,52 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Specifies the Sql context. +/// +public interface ISqlContext { /// - /// Specifies the Sql context. + /// Gets the Sql syntax provider. /// - public interface ISqlContext - { - /// - /// Gets the Sql syntax provider. - /// - ISqlSyntaxProvider SqlSyntax { get; } + ISqlSyntaxProvider SqlSyntax { get; } - /// - /// Gets the database type. - /// - DatabaseType DatabaseType { get; } + /// + /// Gets the database type. + /// + DatabaseType DatabaseType { get; } - /// - /// Creates a new Sql expression. - /// - Sql Sql(); + /// + /// Gets the Sql templates. + /// + SqlTemplates Templates { get; } - /// - /// Creates a new Sql expression. - /// - Sql Sql(string sql, params object[] args); + /// + /// Gets the Poco data factory. + /// + IPocoDataFactory PocoDataFactory { get; } - /// - /// Creates a new query expression. - /// - IQuery Query(); + /// + /// Gets the mappers. + /// + IMapperCollection? Mappers { get; } - /// - /// Gets the Sql templates. - /// - SqlTemplates Templates { get; } + /// + /// Creates a new Sql expression. + /// + Sql Sql(); - /// - /// Gets the Poco data factory. - /// - IPocoDataFactory PocoDataFactory { get; } + /// + /// Creates a new Sql expression. + /// + Sql Sql(string sql, params object[] args); - /// - /// Gets the mappers. - /// - IMapperCollection? Mappers { get; } - } + /// + /// Creates a new query expression. + /// + IQuery Query(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs index c28b0d984d..431ddeb5e8 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs @@ -1,32 +1,36 @@ -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Install; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public interface IUmbracoDatabase : IDatabase { - public interface IUmbracoDatabase : IDatabase - { - /// - /// Gets the Sql context. - /// - ISqlContext SqlContext { get; } + /// + /// Gets the Sql context. + /// + ISqlContext SqlContext { get; } - /// - /// Gets the database instance unique identifier as a string. - /// - /// UmbracoDatabase returns the first eight digits of its unique Guid and, in some - /// debug mode, the underlying database connection identifier (if any). - string InstanceId { get; } + /// + /// Gets the database instance unique identifier as a string. + /// + /// + /// UmbracoDatabase returns the first eight digits of its unique Guid and, in some + /// debug mode, the underlying database connection identifier (if any). + /// + string InstanceId { get; } - /// - /// Gets a value indicating whether the database is currently in a transaction. - /// - bool InTransaction { get; } + /// + /// Gets a value indicating whether the database is currently in a transaction. + /// + bool InTransaction { get; } - bool EnableSqlCount { get; set; } - int SqlCount { get; } - int BulkInsertRecords(IEnumerable records); - bool IsUmbracoInstalled(); - DatabaseSchemaResult ValidateSchema(); - } + bool EnableSqlCount { get; set; } + + int SqlCount { get; } + + int BulkInsertRecords(IEnumerable records); + + bool IsUmbracoInstalled(); + + DatabaseSchemaResult ValidateSchema(); } diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs index a4d4259cbe..8bb717e577 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs @@ -1,82 +1,83 @@ -using System; using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Creates and manages the "ambient" database. +/// +public interface IUmbracoDatabaseFactory : IDisposable { /// - /// Creates and manages the "ambient" database. + /// Gets a value indicating whether the database factory is configured, i.e. whether + /// its connection string and provider name have been set. The factory may however not + /// be initialized (see ). /// - public interface IUmbracoDatabaseFactory : IDisposable - { - /// - /// Creates a new database. - /// - /// - /// The new database must be disposed after being used. - /// Creating a database causes the factory to initialize if it is not already initialized. - /// - IUmbracoDatabase CreateDatabase(); + bool Configured { get; } - /// - /// Gets a value indicating whether the database factory is configured, i.e. whether - /// its connection string and provider name have been set. The factory may however not - /// be initialized (see ). - /// - bool Configured { get; } + /// + /// Gets a value indicating whether the database factory is initialized, i.e. whether + /// its internal state is ready and it has been possible to connect to the database. + /// + bool Initialized { get; } - /// - /// Gets a value indicating whether the database factory is initialized, i.e. whether - /// its internal state is ready and it has been possible to connect to the database. - /// - bool Initialized { get; } + /// + /// Gets the connection string. + /// + /// May return null if the database factory is not configured. + string? ConnectionString { get; } - /// - /// Gets the connection string. - /// - /// May return null if the database factory is not configured. - string? ConnectionString { get; } + /// + /// Gets the provider name. + /// + /// May return null if the database factory is not configured. + string? ProviderName { get; } - /// - /// Gets the provider name. - /// - /// May return null if the database factory is not configured. - string? ProviderName { get; } + /// + /// Gets a value indicating whether the database factory is configured (see ), + /// and it is possible to connect to the database. The factory may however not be initialized (see + /// ). + /// + bool CanConnect { get; } - /// - /// Gets a value indicating whether the database factory is configured (see ), - /// and it is possible to connect to the database. The factory may however not be initialized (see - /// ). - /// - bool CanConnect { get; } + /// + /// Gets the . + /// + /// + /// Getting the causes the factory to initialize if it is not already initialized. + /// + ISqlContext SqlContext { get; } - /// - /// Configures the database factory. - /// - void Configure(ConnectionStrings umbracoConnectionString); + /// + /// Gets the . + /// + /// + /// + /// Getting the causes the factory to initialize if it is not already + /// initialized. + /// + /// + IBulkSqlInsertProvider? BulkSqlInsertProvider { get; } - [Obsolete("Please use alternative Configure method.")] - void Configure(string connectionString, string providerName) => - Configure(new ConnectionStrings { ConnectionString = connectionString, ProviderName = providerName }); + /// + /// Creates a new database. + /// + /// + /// The new database must be disposed after being used. + /// Creating a database causes the factory to initialize if it is not already initialized. + /// + IUmbracoDatabase CreateDatabase(); - /// - /// Gets the . - /// - /// - /// Getting the causes the factory to initialize if it is not already initialized. - /// - ISqlContext SqlContext { get; } + /// + /// Configures the database factory. + /// + void Configure(ConnectionStrings umbracoConnectionString); - /// - /// Gets the . - /// - /// - /// Getting the causes the factory to initialize if it is not already initialized. - /// - IBulkSqlInsertProvider? BulkSqlInsertProvider { get; } + [Obsolete("Please use alternative Configure method.")] + void Configure(string connectionString, string providerName) => + Configure(new ConnectionStrings { ConnectionString = connectionString, ProviderName = providerName }); - /// - /// Configures the database factory for upgrades. - /// - void ConfigureForUpgrade(); - } + /// + /// Configures the database factory for upgrades. + /// + void ConfigureForUpgrade(); } diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index 709940f3cc..d6c23abba8 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -1,961 +1,1047 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Diagnostics; -using System.IO; -using System.Linq; using Microsoft.Data.SqlClient; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Manages LocalDB databases. +/// +/// +/// +/// Latest version is SQL Server 2016 Express LocalDB, +/// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb +/// which can be installed by downloading the Express installer from +/// https://www.microsoft.com/en-us/sql-server/sql-server-downloads +/// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This +/// installs +/// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server +/// Management +/// Studio which can be used to connect to LocalDB databases. +/// +/// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. +/// +public class LocalDb { + private string? _exe; + private bool _hasVersion; + private int _version; + + #region Availability & Version + /// - /// Manages LocalDB databases. + /// Gets the LocalDb installed version. /// /// - /// Latest version is SQL Server 2016 Express LocalDB, - /// see https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb - /// which can be installed by downloading the Express installer from https://www.microsoft.com/en-us/sql-server/sql-server-downloads - /// (about 5MB) then select 'download media' to download SqlLocalDB.msi (about 44MB), which you can execute. This installs - /// LocalDB only. Though you probably want to install the full Express. You may also want to install SQL Server Management - /// Studio which can be used to connect to LocalDB databases. - /// See also https://github.com/ritterim/automation-sql which is a somewhat simpler version of this. + /// If more than one version is installed, returns the highest available. Returns + /// the major version as an integer e.g. 11, 12... /// - public class LocalDb + /// Thrown when LocalDb is not available. + public int Version { - private int _version; - private bool _hasVersion; - private string? _exe; - - #region Availability & Version - - /// - /// Gets the LocalDb installed version. - /// - /// If more than one version is installed, returns the highest available. Returns - /// the major version as an integer e.g. 11, 12... - /// Thrown when LocalDb is not available. - public int Version + get { - get + EnsureVersion(); + if (_version <= 0) { - EnsureVersion(); - if (_version <= 0) - throw new InvalidOperationException("LocalDb is not available."); - return _version; - } - } - - /// - /// Ensures that the LocalDb version is detected. - /// - private void EnsureVersion() - { - if (_hasVersion) return; - DetectVersion(); - _hasVersion = true; - } - - /// - /// Gets a value indicating whether LocalDb is available. - /// - public bool IsAvailable - { - get - { - EnsureVersion(); - return _version > 0; - } - } - - /// - /// Ensures that LocalDb is available. - /// - /// Thrown when LocalDb is not available. - private void EnsureAvailable() - { - if (IsAvailable == false) throw new InvalidOperationException("LocalDb is not available."); - } - - /// - /// Detects LocalDb installed version. - /// - /// If more than one version is installed, the highest available is detected. - private void DetectVersion() - { - _hasVersion = true; - _version = -1; - _exe = null; - - var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); - - // MS SQL Server installs in e.g. "C:\Program Files\Microsoft SQL Server", so - // we want to detect it in "%ProgramFiles%\Microsoft SQL Server" - however, if - // Umbraco runs as a 32bits process (e.g. IISExpress configured as 32bits) - // on a 64bits system, %ProgramFiles% will point to "C:\Program Files (x86)" - // and SQL Server cannot be found. But then, %ProgramW6432% will point to - // the original "C:\Program Files". Using it to fix the path. - // see also: MSDN doc for WOW64 implementation - // - var programW6432 = Environment.GetEnvironmentVariable("ProgramW6432"); - if (string.IsNullOrWhiteSpace(programW6432) == false && programW6432 != programFiles) - programFiles = programW6432; - - if (string.IsNullOrWhiteSpace(programFiles)) return; - - // detect 15, 14, 13, 12, 11 - for (var i = 15; i > 10; i--) - { - var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe"); - if (File.Exists(exe) == false) continue; - _version = i; - _exe = exe; - break; } + + return _version; } + } - #endregion - - #region Instances - - /// - /// Gets the name of existing LocalDb instances. - /// - /// The name of existing LocalDb instances. - /// Thrown when LocalDb is not available. - public string[]? GetInstances() + /// + /// Ensures that the LocalDb version is detected. + /// + private void EnsureVersion() + { + if (_hasVersion) { - EnsureAvailable(); - var rc = ExecuteSqlLocalDb("i", out var output, out var error); // info - if (rc != 0 || error != string.Empty) return null; - return output.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + return; } - /// - /// Gets a value indicating whether a LocalDb instance exists. - /// - /// The name of the instance. - /// A value indicating whether a LocalDb instance with the specified name exists. - /// Thrown when LocalDb is not available. - public bool InstanceExists(string instanceName) + DetectVersion(); + _hasVersion = true; + } + + /// + /// Gets a value indicating whether LocalDb is available. + /// + public bool IsAvailable + { + get { - EnsureAvailable(); - var instances = GetInstances(); - return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); + EnsureVersion(); + return _version > 0; } + } - /// - /// Creates a LocalDb instance. - /// - /// The name of the instance. - /// A value indicating whether the instance was created without errors. - /// Thrown when LocalDb is not available. - public bool CreateInstance(string instanceName) + /// + /// Ensures that LocalDb is available. + /// + /// Thrown when LocalDb is not available. + private void EnsureAvailable() + { + if (IsAvailable == false) { - EnsureAvailable(); - return ExecuteSqlLocalDb($"c \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + throw new InvalidOperationException("LocalDb is not available."); + } + } + + /// + /// Detects LocalDb installed version. + /// + /// If more than one version is installed, the highest available is detected. + private void DetectVersion() + { + _hasVersion = true; + _version = -1; + _exe = null; + + var programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + + // MS SQL Server installs in e.g. "C:\Program Files\Microsoft SQL Server", so + // we want to detect it in "%ProgramFiles%\Microsoft SQL Server" - however, if + // Umbraco runs as a 32bits process (e.g. IISExpress configured as 32bits) + // on a 64bits system, %ProgramFiles% will point to "C:\Program Files (x86)" + // and SQL Server cannot be found. But then, %ProgramW6432% will point to + // the original "C:\Program Files". Using it to fix the path. + // see also: MSDN doc for WOW64 implementation + // + var programW6432 = Environment.GetEnvironmentVariable("ProgramW6432"); + if (string.IsNullOrWhiteSpace(programW6432) == false && programW6432 != programFiles) + { + programFiles = programW6432; + } + + if (string.IsNullOrWhiteSpace(programFiles)) + { + return; + } + + // detect 15, 14, 13, 12, 11 + for (var i = 15; i > 10; i--) + { + var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe"); + if (File.Exists(exe) == false) + { + continue; + } + + _version = i; + _exe = exe; + break; + } + } + + #endregion + + #region Instances + + /// + /// Gets the name of existing LocalDb instances. + /// + /// The name of existing LocalDb instances. + /// Thrown when LocalDb is not available. + public string[]? GetInstances() + { + EnsureAvailable(); + var rc = ExecuteSqlLocalDb("i", out var output, out var error); // info + if (rc != 0 || error != string.Empty) + { + return null; + } + + return output.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Gets a value indicating whether a LocalDb instance exists. + /// + /// The name of the instance. + /// A value indicating whether a LocalDb instance with the specified name exists. + /// Thrown when LocalDb is not available. + public bool InstanceExists(string instanceName) + { + EnsureAvailable(); + var instances = GetInstances(); + return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Creates a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was created without errors. + /// Thrown when LocalDb is not available. + public bool CreateInstance(string instanceName) + { + EnsureAvailable(); + return ExecuteSqlLocalDb($"c \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + } + + /// + /// Drops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was dropped without errors. + /// Thrown when LocalDb is not available. + /// + /// When an instance is dropped all the attached database files are deleted. + /// Successful if the instance does not exist. + /// + public bool DropInstance(string instanceName) + { + EnsureAvailable(); + Instance? instance = GetInstance(instanceName); + if (instance == null) + { + return true; + } + + instance.DropDatabases(); // else the files remain + + // -i force NOWAIT, -k kills + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty + && ExecuteSqlLocalDb($"d \"{instanceName}\"", out _, out error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was stopped without errors. + /// Thrown when LocalDb is not available. + /// + /// Successful if the instance does not exist. + /// + public bool StopInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) + { + return true; + } + + // -i force NOWAIT, -k kills + return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty; + } + + /// + /// Stops a LocalDb instance. + /// + /// The name of the instance. + /// A value indicating whether the instance was started without errors. + /// Thrown when LocalDb is not available. + /// + /// Failed if the instance does not exist. + /// + public bool StartInstance(string instanceName) + { + EnsureAvailable(); + if (InstanceExists(instanceName) == false) + { + return false; + } + + return ExecuteSqlLocalDb($"s \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; + } + + /// + /// Gets a LocalDb instance. + /// + /// The name of the instance. + /// The instance with the specified name if it exists, otherwise null. + /// Thrown when LocalDb is not available. + public Instance? GetInstance(string instanceName) + { + EnsureAvailable(); + return InstanceExists(instanceName) ? new Instance(instanceName) : null; + } + + #endregion + + #region Databases + + /// + /// Represents a LocalDb instance. + /// + /// + /// LocalDb is assumed to be available, and the instance is assumed to exist. + /// + public class Instance + { + private readonly string _masterCstr; + + /// + /// Initializes a new instance of the class. + /// + /// + public Instance(string instanceName) + { + InstanceName = instanceName; + _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; } /// - /// Drops a LocalDb instance. + /// Gets the name of the instance. /// - /// The name of the instance. - /// A value indicating whether the instance was dropped without errors. - /// Thrown when LocalDb is not available. + public string InstanceName { get; } + + public static string GetConnectionString(string instanceName, string databaseName) => + $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; + + /// + /// Gets a LocalDb connection string. + /// + /// The name of the database. + /// The connection string for the specified database. /// - /// When an instance is dropped all the attached database files are deleted. - /// Successful if the instance does not exist. + /// The database should exist in the LocalDb instance. /// - public bool DropInstance(string instanceName) - { - EnsureAvailable(); - var instance = GetInstance(instanceName); - if (instance == null) return true; - instance.DropDatabases(); // else the files remain - - // -i force NOWAIT, -k kills - return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty - && ExecuteSqlLocalDb($"d \"{instanceName}\"", out _, out error) == 0 && error == string.Empty; - } + public string GetConnectionString(string databaseName) => _masterCstr + $@"Database={databaseName};"; /// - /// Stops a LocalDb instance. + /// Gets a LocalDb connection string for an attached database. /// - /// The name of the instance. - /// A value indicating whether the instance was stopped without errors. - /// Thrown when LocalDb is not available. + /// The name of the database. + /// The directory containing database files. + /// The connection string for the specified database. /// - /// Successful if the instance does not exist. + /// The database should not exist in the LocalDb instance. + /// It will be attached with its name being its MDF filename (full path), uppercased, when + /// the first connection is opened, and remain attached until explicitly detached. /// - public bool StopInstance(string instanceName) + public string GetAttachedConnectionString(string databaseName, string filesPath) { - EnsureAvailable(); - if (InstanceExists(instanceName) == false) return true; + GetDatabaseFiles(databaseName, filesPath, out _, out _, out _, out var mdfFilename, out _); - // -i force NOWAIT, -k kills - return ExecuteSqlLocalDb($"p \"{instanceName}\" -i", out _, out var error) == 0 && error == string.Empty; + return _masterCstr + $@"AttachDbFileName='{mdfFilename}';"; } /// - /// Stops a LocalDb instance. + /// Gets the name of existing databases. /// - /// The name of the instance. - /// A value indicating whether the instance was started without errors. - /// Thrown when LocalDb is not available. - /// - /// Failed if the instance does not exist. - /// - public bool StartInstance(string instanceName) + /// The name of existing databases. + public string[] GetDatabases() { - EnsureAvailable(); - if (InstanceExists(instanceName) == false) return false; - return ExecuteSqlLocalDb($"s \"{instanceName}\"", out _, out var error) == 0 && error == string.Empty; - } + var userDatabases = new List(); - /// - /// Gets a LocalDb instance. - /// - /// The name of the instance. - /// The instance with the specified name if it exists, otherwise null. - /// Thrown when LocalDb is not available. - public Instance? GetInstance(string instanceName) - { - EnsureAvailable(); - return InstanceExists(instanceName) ? new Instance(instanceName) : null; - } - - #endregion - - #region Databases - - /// - /// Represents a LocalDb instance. - /// - /// - /// LocalDb is assumed to be available, and the instance is assumed to exist. - /// - public class Instance - { - private readonly string _masterCstr; - - /// - /// Gets the name of the instance. - /// - public string InstanceName { get; } - - /// - /// Initializes a new instance of the class. - /// - /// - public Instance(string instanceName) + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - InstanceName = instanceName; - _masterCstr = $@"Server=(localdb)\{instanceName};Integrated Security=True;"; - } + conn.Open(); - public static string GetConnectionString(string instanceName, string databaseName) - { - return $@"Server=(localdb)\{instanceName};Integrated Security=True;Database={databaseName};"; - } + var databases = new Dictionary(); - /// - /// Gets a LocalDb connection string. - /// - /// The name of the database. - /// The connection string for the specified database. - /// - /// The database should exist in the LocalDb instance. - /// - public string GetConnectionString(string databaseName) - { - return _masterCstr + $@"Database={databaseName};"; - } - - /// - /// Gets a LocalDb connection string for an attached database. - /// - /// The name of the database. - /// The directory containing database files. - /// The connection string for the specified database. - /// - /// The database should not exist in the LocalDb instance. - /// It will be attached with its name being its MDF filename (full path), uppercased, when - /// the first connection is opened, and remain attached until explicitly detached. - /// - public string GetAttachedConnectionString(string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, out _, out _, out _, out var mdfFilename, out _); - - return _masterCstr + $@"AttachDbFileName='{mdfFilename}';"; - } - - /// - /// Gets the name of existing databases. - /// - /// The name of existing databases. - public string[] GetDatabases() - { - var userDatabases = new List(); - - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var databases = new Dictionary(); - - SetCommand(cmd, @" - SELECT name, filename FROM sys.sysdatabases"); - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - databases[reader.GetString(0)] = reader.GetString(1); - } - } - - foreach (var database in databases) - { - var dbname = database.Key; - - if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") - continue; - - // TODO: shall we deal with stale databases? - // TODO: is it always ok to assume file names? - //var mdf = database.Value; - //var ldf = mdf.Replace(".mdf", "_log.ldf"); - //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) - // continue; - - //ExecuteDropDatabase(cmd, dbname, mdf, ldf); - //count++; - - userDatabases.Add(dbname); - } - } - - return userDatabases.ToArray(); - } - - /// - /// Gets a value indicating whether a database exists. - /// - /// The name of the database. - /// A value indicating whether a database with the specified name exists. - /// - /// A database exists if it is registered in the instance, and its files exist. If the database - /// is registered but some of its files are missing, the database is dropped. - /// - public bool DatabaseExists(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) return false; - - // it can exist, even though its files have been deleted - // if files exist assume all is ok (should we try to connect?) - var ldf = GetLogFilename(mdf); - if (File.Exists(mdf) && File.Exists(ldf)) - return true; - - ExecuteDropDatabase(cmd, databaseName, mdf, ldf); - } - - return false; - } - - /// - /// Creates a new database. - /// - /// The name of the database. - /// The directory containing database files. - /// A value indicating whether the database was created without errors. - /// - /// Failed if a database with the specified name already exists in the instance, - /// or if the database files already exist in the specified directory. - /// - public bool CreateDatabase(string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename, out var ldfFilename); - - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf != null) return false; - - // cannot use parameters on CREATE DATABASE - // ie "CREATE DATABASE @0 ..." does not work - SetCommand(cmd, $@" - CREATE DATABASE {QuotedName(databaseName)} - ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) - LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')})"); - - var unused = cmd.ExecuteNonQuery(); - } - return true; - } - - /// - /// Drops a database. - /// - /// The name of the database. - /// A value indicating whether the database was dropped without errors. - /// - /// Successful if the database does not exist. - /// Deletes the database files. - /// - public bool DropDatabase(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - SetCommand(cmd, @" - SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", - databaseName); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) return true; - - ExecuteDropDatabase(cmd, databaseName, mdf); - } - - return true; - } - - /// - /// Drops stale databases. - /// - /// The number of databases that were dropped. - /// - /// A database is considered stale when its files cannot be found. - /// - public int DropStaleDatabases() - { - return DropDatabases(true); - } - - /// - /// Drops databases. - /// - /// A value indicating whether to delete only stale database. - /// The number of databases that were dropped. - /// - /// A database is considered stale when its files cannot be found. - /// - public int DropDatabases(bool staleOnly = false) - { - var count = 0; - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var databases = new Dictionary(); - - SetCommand(cmd, @" - SELECT name, filename FROM sys.sysdatabases"); - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - databases[reader.GetString(0)] = reader.GetString(1); - } - } - - foreach (var database in databases) - { - var dbname = database.Key; - - if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") - continue; - - var mdf = database.Value; - var ldf = mdf.Replace(".mdf", "_log.ldf"); - if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) - continue; - - ExecuteDropDatabase(cmd, dbname, mdf, ldf); - count++; - } - } - - return count; - } - - /// - /// Detaches a database. - /// - /// The name of the database. - /// The directory containing the database files. - /// Thrown when a database with the specified name does not exist. - public string? DetachDatabase(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf == null) - throw new InvalidOperationException("Database does not exist."); - - DetachDatabase(cmd, databaseName); - - return Path.GetDirectoryName(mdf); - } - } - - /// - /// Attaches a database. - /// - /// The name of the database. - /// The directory containing database files. - /// Thrown when a database with the specified name already exists. - public void AttachDatabase(string databaseName, string filesPath) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - var mdf = GetDatabase(cmd, databaseName); - if (mdf != null) - throw new InvalidOperationException("Database already exists."); - - AttachDatabase(cmd, databaseName, filesPath); - } - } - - /// - /// Gets the file names of a database. - /// - /// The name of the database. - /// The MDF logical name. - /// The LDF logical name. - /// The MDF filename. - /// The LDF filename. - public void GetFilenames(string databaseName, - out string? mdfName, out string? ldfName, - out string? mdfFilename, out string? ldfFilename) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); - } - } - - /// - /// Kills all existing connections. - /// - /// The name of the database. - public void KillConnections(string databaseName) - { - using (var conn = new SqlConnection(_masterCstr)) - using (var cmd = conn.CreateCommand()) - { - conn.Open(); - - SetCommand(cmd, @" - DECLARE @sql VARCHAR(MAX); - SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' - FROM master.sys.sysprocesses - WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; - EXEC(@sql);", - databaseName); - cmd.ExecuteNonQuery(); - } - } - - /// - /// Gets a database. - /// - /// The Sql Command. - /// The name of the database. - /// The full filename of the MDF file, if the database exists, otherwise null. - private static string? GetDatabase(SqlCommand cmd, string databaseName) - { SetCommand(cmd, @" - SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", - databaseName); + SELECT name, filename FROM sys.sysdatabases"); - string? mdf = null; - using (var reader = cmd.ExecuteReader()) + using (SqlDataReader? reader = cmd.ExecuteReader()) { - if (reader.Read()) - mdf = reader.GetString(1) ?? string.Empty; while (reader.Read()) { + databases[reader.GetString(0)] = reader.GetString(1); } } - return mdf; - } - - /// - /// Drops a database and its files. - /// - /// The Sql command. - /// The name of the database. - /// The name of the database (MDF) file. - /// The name of the log (LDF) file. - private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string? ldf = null) - { - try + foreach (KeyValuePair database in databases) { - // cannot use parameters on ALTER DATABASE - // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, $@" - ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); + var dbname = database.Key; - var unused1 = cmd.ExecuteNonQuery(); + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + { + continue; + } + + // TODO: shall we deal with stale databases? + // TODO: is it always ok to assume file names? + //var mdf = database.Value; + //var ldf = mdf.Replace(".mdf", "_log.ldf"); + //if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + // continue; + + //ExecuteDropDatabase(cmd, dbname, mdf, ldf); + //count++; + + userDatabases.Add(dbname); } - catch (SqlException e) + } + + return userDatabases.ToArray(); + } + + /// + /// Gets a value indicating whether a database exists. + /// + /// The name of the database. + /// A value indicating whether a database with the specified name exists. + /// + /// A database exists if it is registered in the instance, and its files exist. If the database + /// is registered but some of its files are missing, the database is dropped. + /// + public bool DatabaseExists(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) { - if (e.Message.Contains("Unable to open the physical file") && e.Message.Contains("Operating system error 2:")) - { - // quite probably, the files were missing - // yet, it should be possible to drop the database anyways - // but we'll have to deal with the files - } - else - { - // no idea, throw - throw; - } + return false; } - // cannot use parameters on DROP DATABASE - // ie "DROP DATABASE @0 ..." does not work - SetCommand(cmd, $@" - DROP DATABASE {QuotedName(databaseName)}"); + // it can exist, even though its files have been deleted + // if files exist assume all is ok (should we try to connect?) + var ldf = GetLogFilename(mdf); + if (File.Exists(mdf) && File.Exists(ldf)) + { + return true; + } - var unused2 = cmd.ExecuteNonQuery(); - - // be absolutely sure - if (File.Exists(mdf)) File.Delete(mdf); - ldf = ldf ?? GetLogFilename(mdf); - if (File.Exists(ldf)) File.Delete(ldf); + ExecuteDropDatabase(cmd, databaseName, mdf, ldf); } - /// - /// Gets the log (LDF) filename corresponding to a database (MDF) filename. - /// - /// The MDF filename. - /// - private static string GetLogFilename(string mdfFilename) + return false; + } + + /// + /// Creates a new database. + /// + /// The name of the database. + /// The directory containing database files. + /// A value indicating whether the database was created without errors. + /// + /// Failed if a database with the specified name already exists in the instance, + /// or if the database files already exist in the specified directory. + /// + public bool CreateDatabase(string databaseName, string filesPath) + { + GetDatabaseFiles(databaseName, filesPath, out var logName, out _, out _, out var mdfFilename, + out var ldfFilename); + + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - if (mdfFilename.EndsWith(".mdf") == false) - throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename)); - return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; - } + conn.Open(); - /// - /// Detaches a database. - /// - /// The Sql command. - /// The name of the database. - private static void DetachDatabase(SqlCommand cmd, string databaseName) - { - // cannot use parameters on ALTER DATABASE - // ie "ALTER DATABASE @0 ..." does not work - SetCommand(cmd, $@" - ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); - - var unused1 = cmd.ExecuteNonQuery(); - - SetCommand(cmd, @" - EXEC sp_detach_db @dbname=@0", - databaseName); - - var unused2 = cmd.ExecuteNonQuery(); - } - - /// - /// Attaches a database. - /// - /// The Sql command. - /// The name of the database. - /// The directory containing database files. - private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) - { - GetDatabaseFiles(databaseName, filesPath, - out var logName, out _, out _, out var mdfFilename, out var ldfFilename); + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + { + return false; + } // cannot use parameters on CREATE DATABASE // ie "CREATE DATABASE @0 ..." does not work SetCommand(cmd, $@" CREATE DATABASE {QuotedName(databaseName)} ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) - LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')}) - FOR ATTACH"); + LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')})"); var unused = cmd.ExecuteNonQuery(); } - /// - /// Sets a database command. - /// - /// The command. - /// The command text. - /// The command arguments. - /// - /// The command text must refer to arguments as @0, @1... each referring - /// to the corresponding position in . - /// - private static void SetCommand(SqlCommand cmd, string sql, params object[] args) - { - cmd.CommandType = CommandType.Text; - cmd.CommandText = sql; - cmd.Parameters.Clear(); - for (var i = 0; i < args.Length; i++) - cmd.Parameters.AddWithValue("@" + i, args[i]); - } + return true; + } - /// - /// Gets the file names of a database. - /// - /// The Sql command. - /// The name of the database. - /// The MDF logical name. - /// The LDF logical name. - /// The MDF filename. - /// The LDF filename. - private void GetFilenames(SqlCommand cmd, string databaseName, - out string? mdfName, out string? ldfName, - out string? mdfFilename, out string? ldfFilename) + /// + /// Drops a database. + /// + /// The name of the database. + /// A value indicating whether the database was dropped without errors. + /// + /// Successful if the database does not exist. + /// Deletes the database files. + /// + public bool DropDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) { - mdfName = ldfName = mdfFilename = ldfFilename = null; + conn.Open(); SetCommand(cmd, @" - SELECT DB_NAME(database_id), type_desc, name, physical_name - FROM master.sys.master_files - WHERE database_id=DB_ID(@0)", + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", databaseName); - using (var reader = cmd.ExecuteReader()) + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + { + return true; + } + + ExecuteDropDatabase(cmd, databaseName, mdf); + } + + return true; + } + + /// + /// Drops stale databases. + /// + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropStaleDatabases() => DropDatabases(true); + + /// + /// Drops databases. + /// + /// A value indicating whether to delete only stale database. + /// The number of databases that were dropped. + /// + /// A database is considered stale when its files cannot be found. + /// + public int DropDatabases(bool staleOnly = false) + { + var count = 0; + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var databases = new Dictionary(); + + SetCommand(cmd, @" + SELECT name, filename FROM sys.sysdatabases"); + + using (SqlDataReader? reader = cmd.ExecuteReader()) { while (reader.Read()) { - var type = reader.GetString(1); - if (type == "ROWS") - { - mdfName = reader.GetString(2); - ldfName = reader.GetString(3); - } - else if (type == "LOG") - { - ldfName = reader.GetString(2); - ldfFilename = reader.GetString(3); - } + databases[reader.GetString(0)] = reader.GetString(1); + } + } + + foreach (KeyValuePair database in databases) + { + var dbname = database.Key; + + if (dbname == "master" || dbname == "tempdb" || dbname == "model" || dbname == "msdb") + { + continue; + } + + var mdf = database.Value; + var ldf = mdf.Replace(".mdf", "_log.ldf"); + if (staleOnly && File.Exists(mdf) && File.Exists(ldf)) + { + continue; + } + + ExecuteDropDatabase(cmd, dbname, mdf, ldf); + count++; + } + } + + return count; + } + + /// + /// Detaches a database. + /// + /// The name of the database. + /// The directory containing the database files. + /// Thrown when a database with the specified name does not exist. + public string? DetachDatabase(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf == null) + { + throw new InvalidOperationException("Database does not exist."); + } + + DetachDatabase(cmd, databaseName); + + return Path.GetDirectoryName(mdf); + } + } + + /// + /// Attaches a database. + /// + /// The name of the database. + /// The directory containing database files. + /// Thrown when a database with the specified name already exists. + public void AttachDatabase(string databaseName, string filesPath) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + var mdf = GetDatabase(cmd, databaseName); + if (mdf != null) + { + throw new InvalidOperationException("Database already exists."); + } + + AttachDatabase(cmd, databaseName, filesPath); + } + } + + /// + /// Gets the file names of a database. + /// + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + public void GetFilenames(string databaseName, + out string? mdfName, out string? ldfName, + out string? mdfFilename, out string? ldfFilename) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + GetFilenames(cmd, databaseName, out mdfName, out ldfName, out mdfFilename, out ldfFilename); + } + } + + /// + /// Kills all existing connections. + /// + /// The name of the database. + public void KillConnections(string databaseName) + { + using (var conn = new SqlConnection(_masterCstr)) + using (SqlCommand? cmd = conn.CreateCommand()) + { + conn.Open(); + + SetCommand(cmd, @" + DECLARE @sql VARCHAR(MAX); + SELECT @sql = COALESCE(@sql,'') + 'kill ' + CONVERT(VARCHAR, SPId) + ';' + FROM master.sys.sysprocesses + WHERE DBId = DB_ID(@0) AND SPId <> @@SPId; + EXEC(@sql);", + databaseName); + cmd.ExecuteNonQuery(); + } + } + + /// + /// Gets a database. + /// + /// The Sql Command. + /// The name of the database. + /// The full filename of the MDF file, if the database exists, otherwise null. + private static string? GetDatabase(SqlCommand cmd, string databaseName) + { + SetCommand(cmd, @" + SELECT name, filename FROM master.dbo.sysdatabases WHERE ('[' + name + ']' = @0 OR name = @0)", + databaseName); + + string? mdf = null; + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + mdf = reader.GetString(1) ?? string.Empty; + } + + while (reader.Read()) + { + } + } + + return mdf; + } + + /// + /// Drops a database and its files. + /// + /// The Sql command. + /// The name of the database. + /// The name of the database (MDF) file. + /// The name of the log (LDF) file. + private static void ExecuteDropDatabase(SqlCommand cmd, string databaseName, string mdf, string? ldf = null) + { + try + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, $@" + ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); + + var unused1 = cmd.ExecuteNonQuery(); + } + catch (SqlException e) + { + if (e.Message.Contains("Unable to open the physical file") && + e.Message.Contains("Operating system error 2:")) + { + // quite probably, the files were missing + // yet, it should be possible to drop the database anyways + // but we'll have to deal with the files + } + else + { + // no idea, throw + throw; + } + } + + // cannot use parameters on DROP DATABASE + // ie "DROP DATABASE @0 ..." does not work + SetCommand(cmd, $@" + DROP DATABASE {QuotedName(databaseName)}"); + + var unused2 = cmd.ExecuteNonQuery(); + + // be absolutely sure + if (File.Exists(mdf)) + { + File.Delete(mdf); + } + + ldf = ldf ?? GetLogFilename(mdf); + if (File.Exists(ldf)) + { + File.Delete(ldf); + } + } + + /// + /// Gets the log (LDF) filename corresponding to a database (MDF) filename. + /// + /// The MDF filename. + /// + private static string GetLogFilename(string mdfFilename) + { + if (mdfFilename.EndsWith(".mdf") == false) + { + throw new ArgumentException("Not a valid MDF filename (no .mdf extension).", nameof(mdfFilename)); + } + + return mdfFilename.Substring(0, mdfFilename.Length - ".mdf".Length) + "_log.ldf"; + } + + /// + /// Detaches a database. + /// + /// The Sql command. + /// The name of the database. + private static void DetachDatabase(SqlCommand cmd, string databaseName) + { + // cannot use parameters on ALTER DATABASE + // ie "ALTER DATABASE @0 ..." does not work + SetCommand(cmd, $@" + ALTER DATABASE {QuotedName(databaseName)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"); + + var unused1 = cmd.ExecuteNonQuery(); + + SetCommand(cmd, @" + EXEC sp_detach_db @dbname=@0", + databaseName); + + var unused2 = cmd.ExecuteNonQuery(); + } + + /// + /// Attaches a database. + /// + /// The Sql command. + /// The name of the database. + /// The directory containing database files. + private static void AttachDatabase(SqlCommand cmd, string databaseName, string filesPath) + { + GetDatabaseFiles(databaseName, filesPath, + out var logName, out _, out _, out var mdfFilename, out var ldfFilename); + + // cannot use parameters on CREATE DATABASE + // ie "CREATE DATABASE @0 ..." does not work + SetCommand(cmd, $@" + CREATE DATABASE {QuotedName(databaseName)} + ON (NAME=N{QuotedName(databaseName, '\'')}, FILENAME={QuotedName(mdfFilename, '\'')}) + LOG ON (NAME=N{QuotedName(logName, '\'')}, FILENAME={QuotedName(ldfFilename, '\'')}) + FOR ATTACH"); + + var unused = cmd.ExecuteNonQuery(); + } + + /// + /// Sets a database command. + /// + /// The command. + /// The command text. + /// The command arguments. + /// + /// The command text must refer to arguments as @0, @1... each referring + /// to the corresponding position in . + /// + private static void SetCommand(SqlCommand cmd, string sql, params object[] args) + { + cmd.CommandType = CommandType.Text; + cmd.CommandText = sql; + cmd.Parameters.Clear(); + for (var i = 0; i < args.Length; i++) + { + cmd.Parameters.AddWithValue("@" + i, args[i]); + } + } + + /// + /// Gets the file names of a database. + /// + /// The Sql command. + /// The name of the database. + /// The MDF logical name. + /// The LDF logical name. + /// The MDF filename. + /// The LDF filename. + private void GetFilenames(SqlCommand cmd, string databaseName, + out string? mdfName, out string? ldfName, + out string? mdfFilename, out string? ldfFilename) + { + mdfName = ldfName = mdfFilename = ldfFilename = null; + + SetCommand(cmd, @" + SELECT DB_NAME(database_id), type_desc, name, physical_name + FROM master.sys.master_files + WHERE database_id=DB_ID(@0)", + databaseName); + using (SqlDataReader? reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var type = reader.GetString(1); + if (type == "ROWS") + { + mdfName = reader.GetString(2); + ldfName = reader.GetString(3); + } + else if (type == "LOG") + { + ldfName = reader.GetString(2); + ldfFilename = reader.GetString(3); } } } } + } - /// - /// Copy database files. - /// - /// The name of the source database. - /// The directory containing source database files. - /// The name of the target database. - /// The directory containing target database files. - /// The source database files extension. - /// The target database files extension. - /// A value indicating whether to overwrite the target files. - /// A value indicating whether to delete the source files. - /// - /// The , , - /// and parameters are optional. If they result in target being identical - /// to source, no copy is performed. If is false, nothing happens, otherwise the source - /// files are deleted. - /// If target is not identical to source, files are copied or moved, depending on the value of . - /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. - /// - public void CopyDatabaseFiles(string databaseName, string filesPath, - string? targetDatabaseName = null, string? targetFilesPath = null, - string? sourceExtension = null, string? targetExtension = null, - bool overwrite = false, bool delete = false) + /// + /// Copy database files. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The name of the target database. + /// The directory containing target database files. + /// The source database files extension. + /// The target database files extension. + /// A value indicating whether to overwrite the target files. + /// A value indicating whether to delete the source files. + /// + /// The , , + /// + /// and parameters are optional. If they result in target being identical + /// to source, no copy is performed. If is false, nothing happens, otherwise the source + /// files are deleted. + /// If target is not identical to source, files are copied or moved, depending on the value of + /// . + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public void CopyDatabaseFiles(string databaseName, string filesPath, + string? targetDatabaseName = null, string? targetFilesPath = null, + string? sourceExtension = null, string? targetExtension = null, + bool overwrite = false, bool delete = false) + { + var nop = (targetFilesPath == null || targetFilesPath == filesPath) + && (targetDatabaseName == null || targetDatabaseName == databaseName) + && ((sourceExtension == null && targetExtension == null) || sourceExtension == targetExtension); + if (nop && delete == false) { - var nop = (targetFilesPath == null || targetFilesPath == filesPath) - && (targetDatabaseName == null || targetDatabaseName == databaseName) - && (sourceExtension == null && targetExtension == null || sourceExtension == targetExtension); - if (nop && delete == false) return; + return; + } - GetDatabaseFiles(databaseName, filesPath, - out _, out _, out _, out var mdfFilename, out var ldfFilename); + GetDatabaseFiles(databaseName, filesPath, + out _, out _, out _, out var mdfFilename, out var ldfFilename); - if (sourceExtension != null) + if (sourceExtension != null) + { + mdfFilename += "." + sourceExtension; + ldfFilename += "." + sourceExtension; + } + + if (nop) + { + // delete + if (File.Exists(mdfFilename)) { - mdfFilename += "." + sourceExtension; - ldfFilename += "." + sourceExtension; + File.Delete(mdfFilename); } - if (nop) + if (File.Exists(ldfFilename)) { - // delete - if (File.Exists(mdfFilename)) File.Delete(mdfFilename); - if (File.Exists(ldfFilename)) File.Delete(ldfFilename); + File.Delete(ldfFilename); + } + } + else + { + // copy or copy+delete ie move + GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, + out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename); + + if (targetExtension != null) + { + targetMdfFilename += "." + targetExtension; + targetLdfFilename += "." + targetExtension; + } + + if (delete) + { + if (overwrite && File.Exists(targetMdfFilename)) + { + File.Delete(targetMdfFilename); + } + + if (overwrite && File.Exists(targetLdfFilename)) + { + File.Delete(targetLdfFilename); + } + + File.Move(mdfFilename, targetMdfFilename); + File.Move(ldfFilename, targetLdfFilename); } else { - // copy or copy+delete ie move - GetDatabaseFiles(targetDatabaseName ?? databaseName, targetFilesPath ?? filesPath, - out _, out _, out _, out var targetMdfFilename, out var targetLdfFilename); - - if (targetExtension != null) - { - targetMdfFilename += "." + targetExtension; - targetLdfFilename += "." + targetExtension; - } - - if (delete) - { - if (overwrite && File.Exists(targetMdfFilename)) File.Delete(targetMdfFilename); - if (overwrite && File.Exists(targetLdfFilename)) File.Delete(targetLdfFilename); - File.Move(mdfFilename, targetMdfFilename); - File.Move(ldfFilename, targetLdfFilename); - } - else - { - File.Copy(mdfFilename, targetMdfFilename, overwrite); - File.Copy(ldfFilename, targetLdfFilename, overwrite); - } + File.Copy(mdfFilename, targetMdfFilename, overwrite); + File.Copy(ldfFilename, targetLdfFilename, overwrite); } } - - /// - /// Gets a value indicating whether database files exist. - /// - /// The name of the source database. - /// The directory containing source database files. - /// The database files extension. - /// A value indicating whether the database files exist. - /// - /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. - /// - public bool DatabaseFilesExist(string databaseName, string filesPath, string? extension = null) - { - GetDatabaseFiles(databaseName, filesPath, - out _, out _, out _, out var mdfFilename, out var ldfFilename); - - if (extension != null) - { - mdfFilename += "." + extension; - ldfFilename += "." + extension; - } - - return File.Exists(mdfFilename) && File.Exists(ldfFilename); - } - - /// - /// Gets the name of the database files. - /// - /// The name of the database. - /// The directory containing database files. - /// The name of the log. - /// The base filename (the MDF filename without the .mdf extension). - /// The base log filename (the LDF filename without the .ldf extension). - /// The MDF filename. - /// The LDF filename. - private static void GetDatabaseFiles(string databaseName, string filesPath, - out string logName, - out string baseFilename, out string baseLogFilename, - out string mdfFilename, out string ldfFilename) - { - logName = databaseName + "_log"; - baseFilename = Path.Combine(filesPath, databaseName); - baseLogFilename = Path.Combine(filesPath, logName); - mdfFilename = baseFilename + ".mdf"; - ldfFilename = baseFilename + "_log.ldf"; - } - - #endregion - - #region SqlLocalDB - - /// - /// Executes the SqlLocalDB command. - /// - /// The arguments. - /// The command standard output. - /// The command error output. - /// The process exit code. - /// - /// Execution is successful if the exit code is zero, and error is empty. - /// - private int ExecuteSqlLocalDb(string args, out string output, out string error) - { - if (_exe == null) // should never happen - we should not execute if not available - { - output = string.Empty; - error = "SqlLocalDB.exe not found"; - return -1; - } - - using (var p = new Process - { - StartInfo = - { - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - FileName = _exe, - Arguments = args, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden - } - }) - { - p.Start(); - output = p.StandardOutput.ReadToEnd(); - error = p.StandardError.ReadToEnd(); - p.WaitForExit(); - - return p.ExitCode; - } - - } - - /// - /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited identifier. - /// - /// The name to quote. - /// A quote character. - /// - /// - /// This is a C# implementation of T-SQL QUOTEDNAME. - /// is optional, it can be '[' (default), ']', '\'' or '"'. - /// - internal static string QuotedName(string name, char quote = '[') - { - switch (quote) - { - case '[': - case ']': - return "[" + name.Replace("]", "]]") + "]"; - case '\'': - return "'" + name.Replace("'", "''") + "'"; - case '"': - return "\"" + name.Replace("\"", "\"\"") + "\""; - default: - throw new NotSupportedException("Not a valid quote character."); - } - } - - #endregion } + + /// + /// Gets a value indicating whether database files exist. + /// + /// The name of the source database. + /// The directory containing source database files. + /// The database files extension. + /// A value indicating whether the database files exist. + /// + /// Extensions are used eg to copy MyDatabase.mdf to MyDatabase.mdf.temp. + /// + public bool DatabaseFilesExist(string databaseName, string filesPath, string? extension = null) + { + GetDatabaseFiles(databaseName, filesPath, + out _, out _, out _, out var mdfFilename, out var ldfFilename); + + if (extension != null) + { + mdfFilename += "." + extension; + ldfFilename += "." + extension; + } + + return File.Exists(mdfFilename) && File.Exists(ldfFilename); + } + + /// + /// Gets the name of the database files. + /// + /// The name of the database. + /// The directory containing database files. + /// The name of the log. + /// The base filename (the MDF filename without the .mdf extension). + /// The base log filename (the LDF filename without the .ldf extension). + /// The MDF filename. + /// The LDF filename. + private static void GetDatabaseFiles(string databaseName, string filesPath, + out string logName, + out string baseFilename, out string baseLogFilename, + out string mdfFilename, out string ldfFilename) + { + logName = databaseName + "_log"; + baseFilename = Path.Combine(filesPath, databaseName); + baseLogFilename = Path.Combine(filesPath, logName); + mdfFilename = baseFilename + ".mdf"; + ldfFilename = baseFilename + "_log.ldf"; + } + + #endregion + + #region SqlLocalDB + + /// + /// Executes the SqlLocalDB command. + /// + /// The arguments. + /// The command standard output. + /// The command error output. + /// The process exit code. + /// + /// Execution is successful if the exit code is zero, and error is empty. + /// + private int ExecuteSqlLocalDb(string args, out string output, out string error) + { + if (_exe == null) // should never happen - we should not execute if not available + { + output = string.Empty; + error = "SqlLocalDB.exe not found"; + return -1; + } + + using (var p = new Process + { + StartInfo = + { + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + FileName = _exe, + Arguments = args, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + } + }) + { + p.Start(); + output = p.StandardOutput.ReadToEnd(); + error = p.StandardError.ReadToEnd(); + p.WaitForExit(); + + return p.ExitCode; + } + } + + /// + /// Returns a Unicode string with the delimiters added to make the input string a valid SQL Server delimited + /// identifier. + /// + /// The name to quote. + /// A quote character. + /// + /// + /// This is a C# implementation of T-SQL QUOTEDNAME. + /// is optional, it can be '[' (default), ']', '\'' or '"'. + /// + internal static string QuotedName(string name, char quote = '[') + { + switch (quote) + { + case '[': + case ']': + return "[" + name.Replace("]", "]]") + "]"; + case '\'': + return "'" + name.Replace("'", "''") + "'"; + case '"': + return "\"" + name.Replace("\"", "\"\"") + "\""; + default: + throw new NotSupportedException("Not a valid quote character."); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs index 8d88b2d7df..1d3abdac14 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs @@ -1,25 +1,23 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +[MapperFor(typeof(PublicAccessEntry))] +public sealed class AccessMapper : BaseMapper { - - [MapperFor(typeof(PublicAccessEntry))] - public sealed class AccessMapper : BaseMapper + public AccessMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public AccessMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(PublicAccessEntry.Key), nameof(AccessDto.Id)); - DefineMap(nameof(PublicAccessEntry.LoginNodeId), nameof(AccessDto.LoginNodeId)); - DefineMap(nameof(PublicAccessEntry.NoAccessNodeId), nameof(AccessDto.NoAccessNodeId)); - DefineMap(nameof(PublicAccessEntry.ProtectedNodeId), nameof(AccessDto.NodeId)); - DefineMap(nameof(PublicAccessEntry.CreateDate), nameof(AccessDto.CreateDate)); - DefineMap(nameof(PublicAccessEntry.UpdateDate), nameof(AccessDto.UpdateDate)); - } + protected override void DefineMaps() + { + DefineMap(nameof(PublicAccessEntry.Key), nameof(AccessDto.Id)); + DefineMap(nameof(PublicAccessEntry.LoginNodeId), nameof(AccessDto.LoginNodeId)); + DefineMap(nameof(PublicAccessEntry.NoAccessNodeId), nameof(AccessDto.NoAccessNodeId)); + DefineMap(nameof(PublicAccessEntry.ProtectedNodeId), nameof(AccessDto.NodeId)); + DefineMap(nameof(PublicAccessEntry.CreateDate), nameof(AccessDto.CreateDate)); + DefineMap(nameof(PublicAccessEntry.UpdateDate), nameof(AccessDto.UpdateDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs index 25bf413cc9..6f27cf830f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditEntryMapper.cs @@ -1,31 +1,30 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a mapper for audit entry entities. - /// - [MapperFor(typeof(IAuditEntry))] - [MapperFor(typeof(AuditEntry))] - public sealed class AuditEntryMapper : BaseMapper - { - public AuditEntryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(AuditEntry.Id), nameof(AuditEntryDto.Id)); - DefineMap(nameof(AuditEntry.PerformingUserId), nameof(AuditEntryDto.PerformingUserId)); - DefineMap(nameof(AuditEntry.PerformingDetails), nameof(AuditEntryDto.PerformingDetails)); - DefineMap(nameof(AuditEntry.PerformingIp), nameof(AuditEntryDto.PerformingIp)); - DefineMap(nameof(AuditEntry.EventDateUtc), nameof(AuditEntryDto.EventDateUtc)); - DefineMap(nameof(AuditEntry.AffectedUserId), nameof(AuditEntryDto.AffectedUserId)); - DefineMap(nameof(AuditEntry.AffectedDetails), nameof(AuditEntryDto.AffectedDetails)); - DefineMap(nameof(AuditEntry.EventType), nameof(AuditEntryDto.EventType)); - DefineMap(nameof(AuditEntry.EventDetails), nameof(AuditEntryDto.EventDetails)); - } +/// +/// Represents a mapper for audit entry entities. +/// +[MapperFor(typeof(IAuditEntry))] +[MapperFor(typeof(AuditEntry))] +public sealed class AuditEntryMapper : BaseMapper +{ + public AuditEntryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(AuditEntry.Id), nameof(AuditEntryDto.Id)); + DefineMap(nameof(AuditEntry.PerformingUserId), nameof(AuditEntryDto.PerformingUserId)); + DefineMap(nameof(AuditEntry.PerformingDetails), nameof(AuditEntryDto.PerformingDetails)); + DefineMap(nameof(AuditEntry.PerformingIp), nameof(AuditEntryDto.PerformingIp)); + DefineMap(nameof(AuditEntry.EventDateUtc), nameof(AuditEntryDto.EventDateUtc)); + DefineMap(nameof(AuditEntry.AffectedUserId), nameof(AuditEntryDto.AffectedUserId)); + DefineMap(nameof(AuditEntry.AffectedDetails), nameof(AuditEntryDto.AffectedDetails)); + DefineMap(nameof(AuditEntry.EventType), nameof(AuditEntryDto.EventType)); + DefineMap(nameof(AuditEntry.EventDetails), nameof(AuditEntryDto.EventDetails)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs index 3267a5d14a..c07a309b69 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AuditItemMapper.cs @@ -1,25 +1,25 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(AuditItem))] - [MapperFor(typeof(IAuditItem))] - public sealed class AuditItemMapper : BaseMapper - { - public AuditItemMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); - DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); - DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); - // we cannot map that one - because AuditType is an enum but Header is a string - //DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); - DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); - } +[MapperFor(typeof(AuditItem))] +[MapperFor(typeof(IAuditItem))] +public sealed class AuditItemMapper : BaseMapper +{ + public AuditItemMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(AuditItem.Id), nameof(LogDto.NodeId)); + DefineMap(nameof(AuditItem.CreateDate), nameof(LogDto.Datestamp)); + DefineMap(nameof(AuditItem.UserId), nameof(LogDto.UserId)); + + // we cannot map that one - because AuditType is an enum but Header is a string + // DefineMap(nameof(AuditItem.AuditType), nameof(LogDto.Header)); + DefineMap(nameof(AuditItem.Comment), nameof(LogDto.Comment)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs index f046ee9548..f482d7da6d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/BaseMapper.cs @@ -1,82 +1,108 @@ -using System; using System.Collections.Concurrent; +using System.Reflection; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public abstract class BaseMapper { - public abstract class BaseMapper + private readonly object _definedLock = new(); + + private readonly MapperConfigurationStore _maps; + + // note: using a Lazy here because during installs, we are resolving the + // mappers way before we have a configured IUmbracoDatabaseFactory, ie way before we + // have an ISqlContext - this is some nasty temporal coupling which we might want to + // cleanup eventually. + private readonly Lazy _sqlContext; + private bool _defined; + + private ISqlSyntaxProvider? _sqlSyntax; + + protected BaseMapper(Lazy sqlContext, MapperConfigurationStore maps) { - // note: using a Lazy here because during installs, we are resolving the - // mappers way before we have a configured IUmbracoDatabaseFactory, ie way before we - // have an ISqlContext - this is some nasty temporal coupling which we might want to - // cleanup eventually. + _sqlContext = sqlContext; + _maps = maps; + } - private readonly Lazy _sqlContext; - private readonly object _definedLock = new object(); - private readonly MapperConfigurationStore _maps; - - private ISqlSyntaxProvider? _sqlSyntax; - private bool _defined; - - protected BaseMapper(Lazy sqlContext, MapperConfigurationStore maps) + internal string Map(string? propertyName) + { + lock (_definedLock) { - _sqlContext = sqlContext; - _maps = maps; - } - - protected abstract void DefineMaps(); - - internal string Map(string? propertyName) - { - lock (_definedLock) + if (!_defined) { - if (!_defined) + ISqlContext? sqlContext = _sqlContext.Value; + if (sqlContext == null) { - var sqlContext = _sqlContext.Value; - if (sqlContext == null) - throw new InvalidOperationException("Could not get an ISqlContext."); - _sqlSyntax = sqlContext.SqlSyntax; - - DefineMaps(); - - _defined = true; + throw new InvalidOperationException("Could not get an ISqlContext."); } + + _sqlSyntax = sqlContext.SqlSyntax; + + DefineMaps(); + + _defined = true; } - - if (!_maps.TryGetValue(GetType(), out var mapperMaps)) - throw new InvalidOperationException($"No maps defined for mapper {GetType().FullName}."); - if (propertyName is null || !mapperMaps.TryGetValue(propertyName, out var mappedName)) - throw new InvalidOperationException($"No map defined by mapper {GetType().FullName} for property {propertyName}."); - return mappedName; } - // fixme: TSource is used for nothing - protected void DefineMap(string sourceName, string targetName) + if (!_maps.TryGetValue(GetType(), out ConcurrentDictionary? mapperMaps)) { - if (_sqlSyntax == null) - throw new InvalidOperationException("Do not define maps outside of DefineMaps."); - - var targetType = typeof(TTarget); - - // TODO ensure that sourceName is a valid sourceType property (but, slow?) - - var tableNameAttribute = targetType.FirstAttribute(); - if (tableNameAttribute == null) throw new InvalidOperationException($"Type {targetType.FullName} is not marked with a TableName attribute."); - var tableName = tableNameAttribute.Value; - - // TODO maybe get all properties once and then index them - var targetProperty = targetType.GetProperty(targetName); - if (targetProperty == null) throw new InvalidOperationException($"Type {targetType.FullName} does not have a property named {targetName}."); - var columnAttribute = targetProperty.FirstAttribute(); - if (columnAttribute == null) throw new InvalidOperationException($"Property {targetType.FullName}.{targetName} is not marked with a Column attribute."); - - var columnName = columnAttribute.Name; - var columnMap = _sqlSyntax.GetQuotedTableName(tableName) + "." + _sqlSyntax.GetQuotedColumnName(columnName); - - var mapperMaps = _maps.GetOrAdd(GetType(), type => new ConcurrentDictionary()); - mapperMaps[sourceName] = columnMap; + throw new InvalidOperationException($"No maps defined for mapper {GetType().FullName}."); } + + if (propertyName is null || !mapperMaps.TryGetValue(propertyName, out var mappedName)) + { + throw new InvalidOperationException( + $"No map defined by mapper {GetType().FullName} for property {propertyName}."); + } + + return mappedName; + } + + protected abstract void DefineMaps(); + + // fixme: TSource is used for nothing + protected void DefineMap(string sourceName, string targetName) + { + if (_sqlSyntax == null) + { + throw new InvalidOperationException("Do not define maps outside of DefineMaps."); + } + + Type targetType = typeof(TTarget); + + // TODO ensure that sourceName is a valid sourceType property (but, slow?) + TableNameAttribute? tableNameAttribute = targetType.FirstAttribute(); + if (tableNameAttribute == null) + { + throw new InvalidOperationException( + $"Type {targetType.FullName} is not marked with a TableName attribute."); + } + + var tableName = tableNameAttribute.Value; + + // TODO maybe get all properties once and then index them + PropertyInfo? targetProperty = targetType.GetProperty(targetName); + if (targetProperty == null) + { + throw new InvalidOperationException( + $"Type {targetType.FullName} does not have a property named {targetName}."); + } + + ColumnAttribute? columnAttribute = targetProperty.FirstAttribute(); + if (columnAttribute == null) + { + throw new InvalidOperationException( + $"Property {targetType.FullName}.{targetName} is not marked with a Column attribute."); + } + + var columnName = columnAttribute.Name; + var columnMap = _sqlSyntax.GetQuotedTableName(tableName) + "." + _sqlSyntax.GetQuotedColumnName(columnName); + + ConcurrentDictionary mapperMaps = + _maps.GetOrAdd(GetType(), type => new ConcurrentDictionary()); + mapperMaps[sourceName] = columnMap; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs index 884db7c09e..81f3c00c8c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ConsentMapper.cs @@ -1,30 +1,29 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a mapper for consent entities. - /// - [MapperFor(typeof(IConsent))] - [MapperFor(typeof(Consent))] - public sealed class ConsentMapper : BaseMapper - { - public ConsentMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Consent.Id), nameof(ConsentDto.Id)); - DefineMap(nameof(Consent.Current), nameof(ConsentDto.Current)); - DefineMap(nameof(Consent.CreateDate), nameof(ConsentDto.CreateDate)); - DefineMap(nameof(Consent.Source), nameof(ConsentDto.Source)); - DefineMap(nameof(Consent.Context), nameof(ConsentDto.Context)); - DefineMap(nameof(Consent.Action), nameof(ConsentDto.Action)); - DefineMap(nameof(Consent.State), nameof(ConsentDto.State)); - DefineMap(nameof(Consent.Comment), nameof(ConsentDto.Comment)); - } +/// +/// Represents a mapper for consent entities. +/// +[MapperFor(typeof(IConsent))] +[MapperFor(typeof(Consent))] +public sealed class ConsentMapper : BaseMapper +{ + public ConsentMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Consent.Id), nameof(ConsentDto.Id)); + DefineMap(nameof(Consent.Current), nameof(ConsentDto.Current)); + DefineMap(nameof(Consent.CreateDate), nameof(ConsentDto.CreateDate)); + DefineMap(nameof(Consent.Source), nameof(ConsentDto.Source)); + DefineMap(nameof(Consent.Context), nameof(ConsentDto.Context)); + DefineMap(nameof(Consent.Action), nameof(ConsentDto.Action)); + DefineMap(nameof(Consent.State), nameof(ConsentDto.State)); + DefineMap(nameof(Consent.Comment), nameof(ConsentDto.Comment)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs index 77e3b4edc4..9ee4219e04 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentMapper.cs @@ -1,45 +1,44 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Content))] +[MapperFor(typeof(IContent))] +public sealed class ContentMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Content))] - [MapperFor(typeof(IContent))] - public sealed class ContentMapper : BaseMapper + public ContentMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public ContentMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Content.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Content.Key), nameof(NodeDto.UniqueId)); + protected override void DefineMaps() + { + DefineMap(nameof(Content.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Content.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); - DefineMap(nameof(Content.Name), nameof(ContentVersionDto.Text)); + DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); + DefineMap(nameof(Content.Name), nameof(ContentVersionDto.Text)); - DefineMap(nameof(Content.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Content.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Content.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Content.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Content.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Content.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Content.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Content.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Content.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Content.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Content.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Content.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Content.ContentTypeId), nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Content.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Content.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Content.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Content.UpdateDate), nameof(ContentVersionDto.VersionDate)); - DefineMap(nameof(Content.Published), nameof(DocumentDto.Published)); + DefineMap(nameof(Content.UpdateDate), nameof(ContentVersionDto.VersionDate)); + DefineMap(nameof(Content.Published), nameof(DocumentDto.Published)); - //DefineMap(nameof(Content.Name), nameof(DocumentDto.Alias)); - //CacheMap(src => src, dto => dto.Newest); - //DefineMap(nameof(Content.Template), nameof(DocumentDto.TemplateId)); - } + // DefineMap(nameof(Content.Name), nameof(DocumentDto.Alias)); + // CacheMap(src => src, dto => dto.Newest); + // DefineMap(nameof(Content.Template), nameof(DocumentDto.TemplateId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs index d24dac2894..48fab6bda1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ContentTypeMapper.cs @@ -1,40 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(ContentType))] - [MapperFor(typeof(IContentType))] - public sealed class ContentTypeMapper : BaseMapper - { - public ContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(ContentType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(ContentType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(ContentType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(ContentType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(ContentType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(ContentType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(ContentType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(ContentType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(ContentType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(ContentType))] +[MapperFor(typeof(IContentType))] +public sealed class ContentTypeMapper : BaseMapper +{ + public ContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(ContentType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(ContentType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(ContentType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(ContentType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(ContentType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(ContentType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(ContentType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(ContentType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(ContentType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs index 8a84b8b153..e380ff57aa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DataTypeMapper.cs @@ -1,35 +1,34 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DataType))] - [MapperFor(typeof(IDataType))] - public sealed class DataTypeMapper : BaseMapper - { - public DataTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(DataType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(DataType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(DataType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(DataType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(DataType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(DataType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(DataType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(DataType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(DataType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(DataType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(DataType.EditorAlias), nameof(DataTypeDto.EditorAlias)); - DefineMap(nameof(DataType.DatabaseType), nameof(DataTypeDto.DbType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DataType))] +[MapperFor(typeof(IDataType))] +public sealed class DataTypeMapper : BaseMapper +{ + public DataTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(DataType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(DataType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(DataType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(DataType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(DataType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(DataType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(DataType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(DataType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(DataType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(DataType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(DataType.EditorAlias), nameof(DataTypeDto.EditorAlias)); + DefineMap(nameof(DataType.DatabaseType), nameof(DataTypeDto.DbType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs index da04c254f8..aea84e5a11 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryMapper.cs @@ -1,27 +1,26 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DictionaryItem))] - [MapperFor(typeof(IDictionaryItem))] - public sealed class DictionaryMapper : BaseMapper - { - public DictionaryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(DictionaryItem.Id), nameof(DictionaryDto.PrimaryKey)); - DefineMap(nameof(DictionaryItem.Key), nameof(DictionaryDto.UniqueId)); - DefineMap(nameof(DictionaryItem.ItemKey), nameof(DictionaryDto.Key)); - DefineMap(nameof(DictionaryItem.ParentId), nameof(DictionaryDto.Parent)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DictionaryItem))] +[MapperFor(typeof(IDictionaryItem))] +public sealed class DictionaryMapper : BaseMapper +{ + public DictionaryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(DictionaryItem.Id), nameof(DictionaryDto.PrimaryKey)); + DefineMap(nameof(DictionaryItem.Key), nameof(DictionaryDto.UniqueId)); + DefineMap(nameof(DictionaryItem.ItemKey), nameof(DictionaryDto.Key)); + DefineMap(nameof(DictionaryItem.ParentId), nameof(DictionaryDto.Parent)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs index ead88959d3..eba2563835 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DictionaryTranslationMapper.cs @@ -1,27 +1,34 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(DictionaryTranslation))] - [MapperFor(typeof(IDictionaryTranslation))] - public sealed class DictionaryTranslationMapper : BaseMapper - { - public DictionaryTranslationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(DictionaryTranslation.Id), nameof(LanguageTextDto.PrimaryKey)); - DefineMap(nameof(DictionaryTranslation.Key), nameof(LanguageTextDto.UniqueId)); - DefineMap(nameof(DictionaryTranslation.Language), nameof(LanguageTextDto.LanguageId)); - DefineMap(nameof(DictionaryTranslation.Value), nameof(LanguageTextDto.Value)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(DictionaryTranslation))] +[MapperFor(typeof(IDictionaryTranslation))] +public sealed class DictionaryTranslationMapper : BaseMapper +{ + public DictionaryTranslationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap( + nameof(DictionaryTranslation.Id), + nameof(LanguageTextDto.PrimaryKey)); + DefineMap( + nameof(DictionaryTranslation.Key), + nameof(LanguageTextDto.UniqueId)); + DefineMap( + nameof(DictionaryTranslation.Language), + nameof(LanguageTextDto.LanguageId)); + DefineMap( + nameof(DictionaryTranslation.Value), + nameof(LanguageTextDto.Value)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs index 860d34edbf..2f7b3991d2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/DomainMapper.cs @@ -1,23 +1,22 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IDomain))] - [MapperFor(typeof(UmbracoDomain))] - public sealed class DomainMapper : BaseMapper - { - public DomainMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(UmbracoDomain.Id), nameof(DomainDto.Id)); - DefineMap(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId)); - DefineMap(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage)); - DefineMap(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName)); - } +[MapperFor(typeof(IDomain))] +[MapperFor(typeof(UmbracoDomain))] +public sealed class DomainMapper : BaseMapper +{ + public DomainMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(UmbracoDomain.Id), nameof(DomainDto.Id)); + DefineMap(nameof(UmbracoDomain.RootContentId), nameof(DomainDto.RootStructureId)); + DefineMap(nameof(UmbracoDomain.LanguageId), nameof(DomainDto.DefaultLanguage)); + DefineMap(nameof(UmbracoDomain.DomainName), nameof(DomainDto.DomainName)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs index 85db7bf553..c587b3ac3a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs @@ -1,25 +1,34 @@ -using System; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IIdentityUserLogin))] - [MapperFor(typeof(IdentityUserLogin))] - public sealed class ExternalLoginMapper : BaseMapper - { - public ExternalLoginMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(IdentityUserLogin.Id), nameof(ExternalLoginDto.Id)); - DefineMap(nameof(IdentityUserLogin.CreateDate), nameof(ExternalLoginDto.CreateDate)); - DefineMap(nameof(IdentityUserLogin.LoginProvider), nameof(ExternalLoginDto.LoginProvider)); - DefineMap(nameof(IdentityUserLogin.ProviderKey), nameof(ExternalLoginDto.ProviderKey)); - DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); - DefineMap(nameof(IdentityUserLogin.UserData), nameof(ExternalLoginDto.UserData)); - } +[MapperFor(typeof(IIdentityUserLogin))] +[MapperFor(typeof(IdentityUserLogin))] +public sealed class ExternalLoginMapper : BaseMapper +{ + public ExternalLoginMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(IdentityUserLogin.Id), nameof(ExternalLoginDto.Id)); + DefineMap( + nameof(IdentityUserLogin.CreateDate), + nameof(ExternalLoginDto.CreateDate)); + DefineMap( + nameof(IdentityUserLogin.LoginProvider), + nameof(ExternalLoginDto.LoginProvider)); + DefineMap( + nameof(IdentityUserLogin.ProviderKey), + nameof(ExternalLoginDto.ProviderKey)); + DefineMap( + nameof(IdentityUserLogin.Key), + nameof(ExternalLoginDto.UserOrMemberKey)); + DefineMap( + nameof(IdentityUserLogin.UserData), + nameof(ExternalLoginDto.UserData)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs index ca8360c626..a344fb9f49 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs @@ -1,25 +1,35 @@ -using System; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IIdentityUserToken))] - [MapperFor(typeof(IdentityUserToken))] - public sealed class ExternalLoginTokenMapper : BaseMapper - { - public ExternalLoginTokenMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(IdentityUserToken.Id), nameof(ExternalLoginTokenDto.Id)); - DefineMap(nameof(IdentityUserToken.CreateDate), nameof(ExternalLoginTokenDto.CreateDate)); - DefineMap(nameof(IdentityUserToken.Name), nameof(ExternalLoginTokenDto.Name)); - DefineMap(nameof(IdentityUserToken.Value), nameof(ExternalLoginTokenDto.Value)); - // separate table - DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); - } +[MapperFor(typeof(IIdentityUserToken))] +[MapperFor(typeof(IdentityUserToken))] +public sealed class ExternalLoginTokenMapper : BaseMapper +{ + public ExternalLoginTokenMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap( + nameof(IdentityUserToken.Id), + nameof(ExternalLoginTokenDto.Id)); + DefineMap( + nameof(IdentityUserToken.CreateDate), + nameof(ExternalLoginTokenDto.CreateDate)); + DefineMap( + nameof(IdentityUserToken.Name), + nameof(ExternalLoginTokenDto.Name)); + DefineMap( + nameof(IdentityUserToken.Value), + nameof(ExternalLoginTokenDto.Value)); + + // separate table + DefineMap( + nameof(IdentityUserLogin.Key), + nameof(ExternalLoginDto.UserOrMemberKey)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs index db2f104ed7..ff70b2e88c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/IMapperCollection.cs @@ -1,12 +1,11 @@ -using System; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public interface IMapperCollection : IBuilderCollection { - public interface IMapperCollection : IBuilderCollection - { - bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper); - BaseMapper this[Type type] { get; } - } + BaseMapper this[Type type] { get; } + + bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper); } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs index a133d4066c..05bb514e15 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs @@ -1,22 +1,21 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(KeyValue))] - [MapperFor(typeof(IKeyValue))] - public sealed class KeyValueMapper : BaseMapper - { - public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); - DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); - DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); - } +[MapperFor(typeof(KeyValue))] +[MapperFor(typeof(IKeyValue))] +public sealed class KeyValueMapper : BaseMapper +{ + public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); + DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); + DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs index d4313ad4d5..ed5ef6b224 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LanguageMapper.cs @@ -1,26 +1,25 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(ILanguage))] - [MapperFor(typeof(Language))] - public sealed class LanguageMapper : BaseMapper - { - public LanguageMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Language.Id), nameof(LanguageDto.Id)); - DefineMap(nameof(Language.IsoCode), nameof(LanguageDto.IsoCode)); - DefineMap(nameof(Language.CultureName), nameof(LanguageDto.CultureName)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(ILanguage))] +[MapperFor(typeof(Language))] +public sealed class LanguageMapper : BaseMapper +{ + public LanguageMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Language.Id), nameof(LanguageDto.Id)); + DefineMap(nameof(Language.IsoCode), nameof(LanguageDto.IsoCode)); + DefineMap(nameof(Language.CultureName), nameof(LanguageDto.CultureName)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs index 807e3b6c02..110fe17447 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/LogViewerQueryMapper.cs @@ -1,22 +1,21 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(ILogViewerQuery))] - [MapperFor(typeof(LogViewerQuery))] - public sealed class LogViewerQueryMapper : BaseMapper - { - public LogViewerQueryMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(ILogViewerQuery.Id), nameof(LogViewerQueryDto.Id)); - DefineMap(nameof(ILogViewerQuery.Name), nameof(LogViewerQueryDto.Name)); - DefineMap(nameof(ILogViewerQuery.Query), nameof(LogViewerQueryDto.Query)); - } +[MapperFor(typeof(ILogViewerQuery))] +[MapperFor(typeof(LogViewerQuery))] +public sealed class LogViewerQueryMapper : BaseMapper +{ + public LogViewerQueryMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(ILogViewerQuery.Id), nameof(LogViewerQueryDto.Id)); + DefineMap(nameof(ILogViewerQuery.Name), nameof(LogViewerQueryDto.Name)); + DefineMap(nameof(ILogViewerQuery.Query), nameof(LogViewerQueryDto.Query)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs index f40dbdd477..2384f239c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MacroMapper.cs @@ -1,28 +1,27 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(Macro))] - [MapperFor(typeof(IMacro))] - internal sealed class MacroMapper : BaseMapper - { - public MacroMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Macro.Id), nameof(MacroDto.Id)); - DefineMap(nameof(Macro.Alias), nameof(MacroDto.Alias)); - DefineMap(nameof(Macro.CacheByPage), nameof(MacroDto.CacheByPage)); - DefineMap(nameof(Macro.CacheByMember), nameof(MacroDto.CachePersonalized)); - DefineMap(nameof(Macro.DontRender), nameof(MacroDto.DontRender)); - DefineMap(nameof(Macro.Name), nameof(MacroDto.Name)); - DefineMap(nameof(Macro.CacheDuration), nameof(MacroDto.RefreshRate)); - DefineMap(nameof(Macro.MacroSource), nameof(MacroDto.MacroSource)); - DefineMap(nameof(Macro.UseInEditor), nameof(MacroDto.UseInEditor)); - } +[MapperFor(typeof(Macro))] +[MapperFor(typeof(IMacro))] +internal sealed class MacroMapper : BaseMapper +{ + public MacroMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Macro.Id), nameof(MacroDto.Id)); + DefineMap(nameof(Macro.Alias), nameof(MacroDto.Alias)); + DefineMap(nameof(Macro.CacheByPage), nameof(MacroDto.CacheByPage)); + DefineMap(nameof(Macro.CacheByMember), nameof(MacroDto.CachePersonalized)); + DefineMap(nameof(Macro.DontRender), nameof(MacroDto.DontRender)); + DefineMap(nameof(Macro.Name), nameof(MacroDto.Name)); + DefineMap(nameof(Macro.CacheDuration), nameof(MacroDto.RefreshRate)); + DefineMap(nameof(Macro.MacroSource), nameof(MacroDto.MacroSource)); + DefineMap(nameof(Macro.UseInEditor), nameof(MacroDto.UseInEditor)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs index aab89f8cd9..ceb8bfe49d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollection.cs @@ -1,50 +1,50 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Umbraco.Cms.Core.Composing; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperCollection : BuilderCollectionBase, IMapperCollection { - public class MapperCollection : BuilderCollectionBase, IMapperCollection - { - public MapperCollection(Func> items) - : base(items) - { + private readonly Lazy> _index; - _index = new Lazy>(() => + public MapperCollection(Func> items) + : base(items) => + _index = new Lazy>(() => + { + var d = new ConcurrentDictionary(); + foreach (BaseMapper mapper in this) { - var d = new ConcurrentDictionary(); - foreach(var mapper in this) + IEnumerable attributes = + mapper.GetType().GetCustomAttributes(false); + foreach (MapperForAttribute a in attributes) { - var attributes = mapper.GetType().GetCustomAttributes(false); - foreach(var a in attributes) - { - d.TryAdd(a.EntityType, mapper); - } + d.TryAdd(a.EntityType, mapper); } - return d; - }); - } - - private readonly Lazy> _index; - - /// - /// Returns a mapper for this type, throw an exception if not found - /// - /// - /// - public BaseMapper this[Type type] - { - get - { - if (_index.Value.TryGetValue(type, out var mapper)) - return mapper; - throw new Exception($"Could not find a mapper matching type {type.FullName}."); } - } - public bool TryGetMapper(Type type,[MaybeNullWhen(false)] out BaseMapper mapper) => _index.Value.TryGetValue(type, out mapper); + return d; + }); + + /// + /// Returns a mapper for this type, throw an exception if not found + /// + /// + /// + public BaseMapper this[Type type] + { + get + { + if (_index.Value.TryGetValue(type, out BaseMapper? mapper)) + { + return mapper; + } + + throw new Exception($"Could not find a mapper matching type {type.FullName}."); + } } + + public bool TryGetMapper(Type type, [MaybeNullWhen(false)] out BaseMapper mapper) => + _index.Value.TryGetValue(type, out mapper); } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs index 8b993365a0..3502e32752 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs @@ -1,62 +1,60 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperCollectionBuilder : SetCollectionBuilderBase { - public class MapperCollectionBuilder : SetCollectionBuilderBase + protected override MapperCollectionBuilder This => this; + + public override void RegisterWith(IServiceCollection services) { - protected override MapperCollectionBuilder This => this; + base.RegisterWith(services); - public override void RegisterWith(IServiceCollection services) - { - base.RegisterWith(services); + // default initializer registers + // - service MapperCollectionBuilder, returns MapperCollectionBuilder + // - service MapperCollection, returns MapperCollectionBuilder's collection + // we want to register extra + // - service IMapperCollection, returns MappersCollectionBuilder's collection + services.AddSingleton(); + services.AddSingleton(factory => factory.GetRequiredService()); + } - // default initializer registers - // - service MapperCollectionBuilder, returns MapperCollectionBuilder - // - service MapperCollection, returns MapperCollectionBuilder's collection - // we want to register extra - // - service IMapperCollection, returns MappersCollectionBuilder's collection - - services.AddSingleton(); - services.AddSingleton(factory => factory.GetRequiredService()); - } - - public MapperCollectionBuilder AddCoreMappers() - { - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - Add(); - return this; - } + public MapperCollectionBuilder AddCoreMappers() + { + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + Add(); + return this; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs index 460e50677d..28d3857c28 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperConfigurationStore.cs @@ -1,8 +1,7 @@ -using System; using System.Collections.Concurrent; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +public class MapperConfigurationStore : ConcurrentDictionary> { - public class MapperConfigurationStore : ConcurrentDictionary> - { } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs index c2caa89bd9..8cb1fae20a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperForAttribute.cs @@ -1,16 +1,12 @@ -using System; +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +/// +/// An attribute used to decorate mappers to be associated with entities +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public sealed class MapperForAttribute : Attribute { - /// - /// An attribute used to decorate mappers to be associated with entities - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - public sealed class MapperForAttribute : Attribute - { - public Type EntityType { get; private set; } - - public MapperForAttribute(Type entityType) => EntityType = entityType; - } + public MapperForAttribute(Type entityType) => EntityType = entityType; + public Type EntityType { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs index a2c5ff305b..0495cabdd1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaMapper.cs @@ -1,38 +1,37 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMedia))] +[MapperFor(typeof(Core.Models.Media))] +public sealed class MediaMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMedia))] - [MapperFor(typeof(Core.Models.Media))] - public sealed class MediaMapper : BaseMapper + public MediaMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MediaMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Core.Models.Media.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Core.Models.Media.Key), nameof(NodeDto.UniqueId)); + protected override void DefineMaps() + { + DefineMap(nameof(Core.Models.Media.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Core.Models.Media.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); + DefineMap(nameof(Content.VersionId), nameof(ContentVersionDto.Id)); - DefineMap(nameof(Core.Models.Media.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Core.Models.Media.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Core.Models.Media.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Core.Models.Media.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Core.Models.Media.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Core.Models.Media.Name), nameof(NodeDto.Text)); - DefineMap(nameof(Core.Models.Media.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Core.Models.Media.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Core.Models.Media.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Core.Models.Media.UpdateDate), nameof(ContentVersionDto.VersionDate)); - } + DefineMap(nameof(Core.Models.Media.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Core.Models.Media.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Core.Models.Media.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Core.Models.Media.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Core.Models.Media.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Core.Models.Media.Name), nameof(NodeDto.Text)); + DefineMap(nameof(Core.Models.Media.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Core.Models.Media.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Core.Models.Media.ContentTypeId), nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Core.Models.Media.UpdateDate), nameof(ContentVersionDto.VersionDate)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs index 823ee7ce88..47f6d2b7b6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MediaTypeMapper.cs @@ -1,40 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMediaType))] - [MapperFor(typeof(MediaType))] - public sealed class MediaTypeMapper : BaseMapper - { - public MediaTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(MediaType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MediaType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MediaType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(MediaType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(MediaType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(MediaType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(MediaType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MediaType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(MediaType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(MediaType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MediaType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(MediaType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(MediaType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(MediaType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(MediaType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(MediaType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(MediaType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMediaType))] +[MapperFor(typeof(MediaType))] +public sealed class MediaTypeMapper : BaseMapper +{ + public MediaTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(MediaType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MediaType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MediaType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(MediaType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(MediaType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(MediaType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(MediaType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MediaType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(MediaType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(MediaType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MediaType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(MediaType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(MediaType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(MediaType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(MediaType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(MediaType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(MediaType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs index 749335b0a2..46dc9fa4ff 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberGroupMapper.cs @@ -1,24 +1,23 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof (IMemberGroup))] - [MapperFor(typeof (MemberGroup))] - public sealed class MemberGroupMapper : BaseMapper - { - public MemberGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(MemberGroup.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MemberGroup.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MemberGroup.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MemberGroup.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MemberGroup.Key), nameof(NodeDto.UniqueId)); - } +[MapperFor(typeof(IMemberGroup))] +[MapperFor(typeof(MemberGroup))] +public sealed class MemberGroupMapper : BaseMapper +{ + public MemberGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(MemberGroup.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MemberGroup.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MemberGroup.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MemberGroup.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MemberGroup.Key), nameof(NodeDto.UniqueId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs index c9fce21a73..2a39db1bb6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberMapper.cs @@ -1,56 +1,55 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IMember))] +[MapperFor(typeof(Member))] +public sealed class MemberMapper : BaseMapper { - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IMember))] - [MapperFor(typeof(Member))] - public sealed class MemberMapper : BaseMapper + public MemberMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public MemberMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - DefineMap(nameof(Member.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(Member.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(Member.Level), nameof(NodeDto.Level)); - DefineMap(nameof(Member.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Member.Path), nameof(NodeDto.Path)); - DefineMap(nameof(Member.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(Member.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(Member.Name), nameof(NodeDto.Text)); - DefineMap(nameof(Member.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(Member.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Member.ContentTypeId), nameof(ContentDto.ContentTypeId)); - DefineMap(nameof(Member.ContentTypeAlias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(Member.UpdateDate), nameof(ContentVersionDto.VersionDate)); + protected override void DefineMaps() + { + DefineMap(nameof(Member.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(Member.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(Member.Level), nameof(NodeDto.Level)); + DefineMap(nameof(Member.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Member.Path), nameof(NodeDto.Path)); + DefineMap(nameof(Member.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(Member.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(Member.Name), nameof(NodeDto.Text)); + DefineMap(nameof(Member.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(Member.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(Member.ContentTypeId), nameof(ContentDto.ContentTypeId)); + DefineMap(nameof(Member.ContentTypeAlias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(Member.UpdateDate), nameof(ContentVersionDto.VersionDate)); - DefineMap(nameof(Member.Email), nameof(MemberDto.Email)); - DefineMap(nameof(Member.Username), nameof(MemberDto.LoginName)); - DefineMap(nameof(Member.RawPasswordValue), nameof(MemberDto.Password)); - DefineMap(nameof(Member.IsApproved), nameof(MemberDto.IsApproved)); - DefineMap(nameof(Member.IsLockedOut), nameof(MemberDto.IsLockedOut)); - DefineMap(nameof(Member.FailedPasswordAttempts), nameof(MemberDto.FailedPasswordAttempts)); - DefineMap(nameof(Member.LastLockoutDate), nameof(MemberDto.LastLockoutDate)); - DefineMap(nameof(Member.LastLoginDate), nameof(MemberDto.LastLoginDate)); - DefineMap(nameof(Member.LastPasswordChangeDate), nameof(MemberDto.LastPasswordChangeDate)); + DefineMap(nameof(Member.Email), nameof(MemberDto.Email)); + DefineMap(nameof(Member.Username), nameof(MemberDto.LoginName)); + DefineMap(nameof(Member.RawPasswordValue), nameof(MemberDto.Password)); + DefineMap(nameof(Member.IsApproved), nameof(MemberDto.IsApproved)); + DefineMap(nameof(Member.IsLockedOut), nameof(MemberDto.IsLockedOut)); + DefineMap(nameof(Member.FailedPasswordAttempts), nameof(MemberDto.FailedPasswordAttempts)); + DefineMap(nameof(Member.LastLockoutDate), nameof(MemberDto.LastLockoutDate)); + DefineMap(nameof(Member.LastLoginDate), nameof(MemberDto.LastLoginDate)); + DefineMap(nameof(Member.LastPasswordChangeDate), nameof(MemberDto.LastPasswordChangeDate)); - DefineMap(nameof(Member.Comments), nameof(PropertyDataDto.TextValue)); + DefineMap(nameof(Member.Comments), nameof(PropertyDataDto.TextValue)); - /* Internal experiment */ - DefineMap(nameof(Member.DateTimePropertyValue), nameof(PropertyDataDto.DateValue)); - DefineMap(nameof(Member.IntegerPropertyValue), nameof(PropertyDataDto.IntegerValue)); - DefineMap(nameof(Member.BoolPropertyValue), nameof(PropertyDataDto.IntegerValue)); - DefineMap(nameof(Member.LongStringPropertyValue), nameof(PropertyDataDto.TextValue)); - DefineMap(nameof(Member.ShortStringPropertyValue), nameof(PropertyDataDto.VarcharValue)); - DefineMap(nameof(Member.PropertyTypeAlias), nameof(PropertyTypeDto.Alias)); - } + /* Internal experiment */ + DefineMap(nameof(Member.DateTimePropertyValue), nameof(PropertyDataDto.DateValue)); + DefineMap(nameof(Member.IntegerPropertyValue), nameof(PropertyDataDto.IntegerValue)); + DefineMap(nameof(Member.BoolPropertyValue), nameof(PropertyDataDto.IntegerValue)); + DefineMap(nameof(Member.LongStringPropertyValue), nameof(PropertyDataDto.TextValue)); + DefineMap(nameof(Member.ShortStringPropertyValue), nameof(PropertyDataDto.VarcharValue)); + DefineMap(nameof(Member.PropertyTypeAlias), nameof(PropertyTypeDto.Alias)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs index d23e219e24..b1f4c1d2e9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MemberTypeMapper.cs @@ -1,40 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof (MemberType))] - [MapperFor(typeof (IMemberType))] - public sealed class MemberTypeMapper : BaseMapper - { - public MemberTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(MemberType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(MemberType.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(MemberType.Level), nameof(NodeDto.Level)); - DefineMap(nameof(MemberType.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(MemberType.Path), nameof(NodeDto.Path)); - DefineMap(nameof(MemberType.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(MemberType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(MemberType.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(MemberType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(MemberType.CreatorId), nameof(NodeDto.UserId)); - DefineMap(nameof(MemberType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(MemberType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(MemberType.Description), nameof(ContentTypeDto.Description)); - DefineMap(nameof(MemberType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(MemberType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(MemberType.IsElement), nameof(ContentTypeDto.IsElement)); - DefineMap(nameof(MemberType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(MemberType))] +[MapperFor(typeof(IMemberType))] +public sealed class MemberTypeMapper : BaseMapper +{ + public MemberTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(MemberType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(MemberType.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(MemberType.Level), nameof(NodeDto.Level)); + DefineMap(nameof(MemberType.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(MemberType.Path), nameof(NodeDto.Path)); + DefineMap(nameof(MemberType.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(MemberType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(MemberType.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(MemberType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(MemberType.CreatorId), nameof(NodeDto.UserId)); + DefineMap(nameof(MemberType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(MemberType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(MemberType.Description), nameof(ContentTypeDto.Description)); + DefineMap(nameof(MemberType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(MemberType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(MemberType.IsElement), nameof(ContentTypeDto.IsElement)); + DefineMap(nameof(MemberType.Thumbnail), nameof(ContentTypeDto.Thumbnail)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs index 86f5401e52..96e502242f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/NullableDateMapper.cs @@ -1,33 +1,31 @@ -using System; using System.Reflection; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +/// +/// Extends NPoco default mapper and ensures that nullable dates are not saved to the database. +/// +public class NullableDateMapper : DefaultMapper { - /// - /// Extends NPoco default mapper and ensures that nullable dates are not saved to the database. - /// - public class NullableDateMapper : DefaultMapper + public override Func? GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) { - public override Func? GetToDbConverter(Type destType, MemberInfo sourceMemberInfo) + // ensures that NPoco does not try to insert an invalid date + // from a nullable DateTime property + if (sourceMemberInfo.GetMemberInfoType() == typeof(DateTime)) { - // ensures that NPoco does not try to insert an invalid date - // from a nullable DateTime property - if (sourceMemberInfo.GetMemberInfoType() == typeof(DateTime)) + return datetimeVal => { - return datetimeVal => + var datetime = datetimeVal as DateTime?; + if (datetime.HasValue && datetime.Value > DateTime.MinValue) { - var datetime = datetimeVal as DateTime?; - if (datetime.HasValue && datetime.Value > DateTime.MinValue) - { - return datetime.Value; - } + return datetime.Value; + } - return null; - }; - } - - return null; + return null; + }; } + + return null; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs index 47c7df3a8e..00f23f6187 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs @@ -1,28 +1,27 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(PropertyGroup))] - public sealed class PropertyGroupMapper : BaseMapper - { - public PropertyGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(PropertyGroup.Id), nameof(PropertyTypeGroupDto.Id)); - DefineMap(nameof(PropertyGroup.Key), nameof(PropertyTypeGroupDto.UniqueId)); - DefineMap(nameof(PropertyGroup.Type), nameof(PropertyTypeGroupDto.Type)); - DefineMap(nameof(PropertyGroup.Name), nameof(PropertyTypeGroupDto.Text)); - DefineMap(nameof(PropertyGroup.Alias), nameof(PropertyTypeGroupDto.Alias)); - DefineMap(nameof(PropertyGroup.SortOrder), nameof(PropertyTypeGroupDto.SortOrder)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(PropertyGroup))] +public sealed class PropertyGroupMapper : BaseMapper +{ + public PropertyGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(PropertyGroup.Id), nameof(PropertyTypeGroupDto.Id)); + DefineMap(nameof(PropertyGroup.Key), nameof(PropertyTypeGroupDto.UniqueId)); + DefineMap(nameof(PropertyGroup.Type), nameof(PropertyTypeGroupDto.Type)); + DefineMap(nameof(PropertyGroup.Name), nameof(PropertyTypeGroupDto.Text)); + DefineMap(nameof(PropertyGroup.Alias), nameof(PropertyTypeGroupDto.Alias)); + DefineMap(nameof(PropertyGroup.SortOrder), nameof(PropertyTypeGroupDto.SortOrder)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs index 08ca8d6a13..86a2872c12 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyMapper.cs @@ -1,20 +1,19 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(Property))] - public sealed class PropertyMapper : BaseMapper - { - public PropertyMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Property.Id), nameof(PropertyDataDto.Id)); - DefineMap(nameof(Property.PropertyTypeId), nameof(PropertyDataDto.PropertyTypeId)); - } +[MapperFor(typeof(Property))] +public sealed class PropertyMapper : BaseMapper +{ + public PropertyMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Property.Id), nameof(PropertyDataDto.Id)); + DefineMap(nameof(Property.PropertyTypeId), nameof(PropertyDataDto.PropertyTypeId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs index 3da3d16fcb..f84193230b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyTypeMapper.cs @@ -1,36 +1,35 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(PropertyType))] - public sealed class PropertyTypeMapper : BaseMapper - { - public PropertyTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(PropertyType.Key), nameof(PropertyTypeDto.UniqueId)); - DefineMap(nameof(PropertyType.Id), nameof(PropertyTypeDto.Id)); - DefineMap(nameof(PropertyType.Alias), nameof(PropertyTypeDto.Alias)); - DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId)); - DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description)); - DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory)); - DefineMap(nameof(PropertyType.MandatoryMessage), nameof(PropertyTypeDto.MandatoryMessage)); - DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name)); - DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder)); - DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp)); - DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage)); - DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop)); - DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias)); - DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(PropertyType))] +public sealed class PropertyTypeMapper : BaseMapper +{ + public PropertyTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(PropertyType.Key), nameof(PropertyTypeDto.UniqueId)); + DefineMap(nameof(PropertyType.Id), nameof(PropertyTypeDto.Id)); + DefineMap(nameof(PropertyType.Alias), nameof(PropertyTypeDto.Alias)); + DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId)); + DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description)); + DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory)); + DefineMap(nameof(PropertyType.MandatoryMessage), nameof(PropertyTypeDto.MandatoryMessage)); + DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name)); + DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder)); + DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp)); + DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage)); + DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop)); + DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias)); + DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs index e75492be18..2cc0c2d9ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationMapper.cs @@ -1,29 +1,28 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IRelation))] - [MapperFor(typeof(Relation))] - public sealed class RelationMapper : BaseMapper - { - public RelationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Relation.Id), nameof(RelationDto.Id)); - DefineMap(nameof(Relation.ChildId), nameof(RelationDto.ChildId)); - DefineMap(nameof(Relation.Comment), nameof(RelationDto.Comment)); - DefineMap(nameof(Relation.CreateDate), nameof(RelationDto.Datetime)); - DefineMap(nameof(Relation.ParentId), nameof(RelationDto.ParentId)); - DefineMap(nameof(Relation.RelationTypeId), nameof(RelationDto.RelationType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IRelation))] +[MapperFor(typeof(Relation))] +public sealed class RelationMapper : BaseMapper +{ + public RelationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Relation.Id), nameof(RelationDto.Id)); + DefineMap(nameof(Relation.ChildId), nameof(RelationDto.ChildId)); + DefineMap(nameof(Relation.Comment), nameof(RelationDto.Comment)); + DefineMap(nameof(Relation.CreateDate), nameof(RelationDto.Datetime)); + DefineMap(nameof(Relation.ParentId), nameof(RelationDto.ParentId)); + DefineMap(nameof(Relation.RelationTypeId), nameof(RelationDto.RelationType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs index 732563fef7..5fdf01f21e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs @@ -1,30 +1,29 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(RelationType))] - [MapperFor(typeof(IRelationType))] - public sealed class RelationTypeMapper : BaseMapper - { - public RelationTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(RelationType.Id), nameof(RelationTypeDto.Id)); - DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); - DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType)); - DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); - DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency)); - DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); - DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(RelationType))] +[MapperFor(typeof(IRelationType))] +public sealed class RelationTypeMapper : BaseMapper +{ + public RelationTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(RelationType.Id), nameof(RelationTypeDto.Id)); + DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); + DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType)); + DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); + DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency)); + DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); + DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs index 61b597be46..793bebb8fb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ServerRegistrationMapper.cs @@ -1,26 +1,39 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(ServerRegistration))] - [MapperFor(typeof(IServerRegistration))] - internal sealed class ServerRegistrationMapper : BaseMapper - { - public ServerRegistrationMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(ServerRegistration.Id), nameof(ServerRegistrationDto.Id)); - DefineMap(nameof(ServerRegistration.IsActive), nameof(ServerRegistrationDto.IsActive)); - DefineMap(nameof(ServerRegistration.IsSchedulingPublisher), nameof(ServerRegistrationDto.IsSchedulingPublisher)); - DefineMap(nameof(ServerRegistration.ServerAddress), nameof(ServerRegistrationDto.ServerAddress)); - DefineMap(nameof(ServerRegistration.CreateDate), nameof(ServerRegistrationDto.DateRegistered)); - DefineMap(nameof(ServerRegistration.UpdateDate), nameof(ServerRegistrationDto.DateAccessed)); - DefineMap(nameof(ServerRegistration.ServerIdentity), nameof(ServerRegistrationDto.ServerIdentity)); - } +[MapperFor(typeof(ServerRegistration))] +[MapperFor(typeof(IServerRegistration))] +internal sealed class ServerRegistrationMapper : BaseMapper +{ + public ServerRegistrationMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap( + nameof(ServerRegistration.Id), + nameof(ServerRegistrationDto.Id)); + DefineMap( + nameof(ServerRegistration.IsActive), + nameof(ServerRegistrationDto.IsActive)); + DefineMap( + nameof(ServerRegistration.IsSchedulingPublisher), + nameof(ServerRegistrationDto.IsSchedulingPublisher)); + DefineMap( + nameof(ServerRegistration.ServerAddress), + nameof(ServerRegistrationDto.ServerAddress)); + DefineMap( + nameof(ServerRegistration.CreateDate), + nameof(ServerRegistrationDto.DateRegistered)); + DefineMap( + nameof(ServerRegistration.UpdateDate), + nameof(ServerRegistrationDto.DateAccessed)); + DefineMap( + nameof(ServerRegistration.ServerIdentity), + nameof(ServerRegistrationDto.ServerIdentity)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs index b2bcc6098a..2024ad7622 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/SimpleContentTypeMapper.cs @@ -1,36 +1,33 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; + +// TODO: This mapper is actually very useless because the only time it would ever be used is when trying to generate a strongly typed query +// on an IContentBase object which is what exposes ISimpleContentType, however the queries that we execute in the content repositories don't actually +// join on the content type table. The content type data is resolved outside of the query so the end result of the query that is generated by using +// this mapper will either fail or the syntax will target umbracoNode which will be filtering on the actual content NOT the content type. +// I'm leaving this here purely because the ExpressionTests rely on this which is fine for testing that the expressions work, but note that this +// resulting query will not. +[MapperFor(typeof(ISimpleContentType))] +[MapperFor(typeof(SimpleContentType))] +public sealed class SimpleContentTypeMapper : BaseMapper { - // TODO: This mapper is actually very useless because the only time it would ever be used is when trying to generate a strongly typed query - // on an IContentBase object which is what exposes ISimpleContentType, however the queries that we execute in the content repositories don't actually - // join on the content type table. The content type data is resolved outside of the query so the end result of the query that is generated by using - // this mapper will either fail or the syntax will target umbracoNode which will be filtering on the actual content NOT the content type. - // I'm leaving this here purely because the ExpressionTests rely on this which is fine for testing that the expressions work, but note that this - // resulting query will not. - - [MapperFor(typeof(ISimpleContentType))] - [MapperFor(typeof(SimpleContentType))] - public sealed class SimpleContentTypeMapper : BaseMapper + public SimpleContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) { - public SimpleContentTypeMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } + } - protected override void DefineMaps() - { - // There is no reason for using ContentType here instead of SimpleContentType, in fact the underlying DefineMap call does nothing with the first type parameter - - DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); - DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); - DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); - DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); - DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); - DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); - } + protected override void DefineMaps() + { + // There is no reason for using ContentType here instead of SimpleContentType, in fact the underlying DefineMap call does nothing with the first type parameter + DefineMap(nameof(ContentType.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(ContentType.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(ContentType.Name), nameof(NodeDto.Text)); + DefineMap(nameof(ContentType.Alias), nameof(ContentTypeDto.Alias)); + DefineMap(nameof(ContentType.Icon), nameof(ContentTypeDto.Icon)); + DefineMap(nameof(ContentType.IsContainer), nameof(ContentTypeDto.IsContainer)); + DefineMap(nameof(ContentType.AllowedAsRoot), nameof(ContentTypeDto.AllowAtRoot)); + DefineMap(nameof(ContentType.IsElement), nameof(ContentTypeDto.IsElement)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs index 8c6df88a4d..744f5b400e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/TagMapper.cs @@ -1,27 +1,26 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Tag))] - [MapperFor(typeof(ITag))] - public sealed class TagMapper : BaseMapper - { - public TagMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Tag.Id), nameof(TagDto.Id)); - DefineMap(nameof(Tag.Text), nameof(TagDto.Text)); - DefineMap(nameof(Tag.Group), nameof(TagDto.Group)); - DefineMap(nameof(Tag.LanguageId), nameof(TagDto.LanguageId)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Tag))] +[MapperFor(typeof(ITag))] +public sealed class TagMapper : BaseMapper +{ + public TagMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Tag.Id), nameof(TagDto.Id)); + DefineMap(nameof(Tag.Text), nameof(TagDto.Text)); + DefineMap(nameof(Tag.Group), nameof(TagDto.Group)); + DefineMap(nameof(Tag.LanguageId), nameof(TagDto.LanguageId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs index f2c8627cc9..f37511cb2b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/TemplateMapper.cs @@ -1,27 +1,26 @@ -using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(Template))] - [MapperFor(typeof(ITemplate))] - public sealed class TemplateMapper : BaseMapper - { - public TemplateMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(Template.Id), nameof(TemplateDto.NodeId)); - DefineMap(nameof(Template.MasterTemplateId), nameof(NodeDto.ParentId)); - DefineMap(nameof(Template.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(Template.Alias), nameof(TemplateDto.Alias)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(Template))] +[MapperFor(typeof(ITemplate))] +public sealed class TemplateMapper : BaseMapper +{ + public TemplateMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(Template.Id), nameof(TemplateDto.NodeId)); + DefineMap(nameof(Template.MasterTemplateId), nameof(NodeDto.ParentId)); + DefineMap(nameof(Template.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(Template.Alias), nameof(TemplateDto.Alias)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs index 5f60861667..068ee6a74d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UmbracoEntityMapper.cs @@ -1,28 +1,27 @@ -using System; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof (IUmbracoEntity))] - public sealed class UmbracoEntityMapper : BaseMapper - { - public UmbracoEntityMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(IUmbracoEntity.Id), nameof(NodeDto.NodeId)); - DefineMap(nameof(IUmbracoEntity.CreateDate), nameof(NodeDto.CreateDate)); - DefineMap(nameof(IUmbracoEntity.Level), nameof(NodeDto.Level)); - DefineMap(nameof(IUmbracoEntity.ParentId), nameof(NodeDto.ParentId)); - DefineMap(nameof(IUmbracoEntity.Path), nameof(NodeDto.Path)); - DefineMap(nameof(IUmbracoEntity.SortOrder), nameof(NodeDto.SortOrder)); - DefineMap(nameof(IUmbracoEntity.Name), nameof(NodeDto.Text)); - DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); - DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); - DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); - } +[MapperFor(typeof(IUmbracoEntity))] +public sealed class UmbracoEntityMapper : BaseMapper +{ + public UmbracoEntityMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(IUmbracoEntity.Id), nameof(NodeDto.NodeId)); + DefineMap(nameof(IUmbracoEntity.CreateDate), nameof(NodeDto.CreateDate)); + DefineMap(nameof(IUmbracoEntity.Level), nameof(NodeDto.Level)); + DefineMap(nameof(IUmbracoEntity.ParentId), nameof(NodeDto.ParentId)); + DefineMap(nameof(IUmbracoEntity.Path), nameof(NodeDto.Path)); + DefineMap(nameof(IUmbracoEntity.SortOrder), nameof(NodeDto.SortOrder)); + DefineMap(nameof(IUmbracoEntity.Name), nameof(NodeDto.Text)); + DefineMap(nameof(IUmbracoEntity.Trashed), nameof(NodeDto.Trashed)); + DefineMap(nameof(IUmbracoEntity.Key), nameof(NodeDto.UniqueId)); + DefineMap(nameof(IUmbracoEntity.CreatorId), nameof(NodeDto.UserId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs index 51f4d0bbcc..6c6ef11d35 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs @@ -1,29 +1,28 @@ -using System; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - /// - /// Represents a to DTO mapper used to translate the properties of the public api - /// implementation to that of the database's DTO as sql: [tableName].[columnName]. - /// - [MapperFor(typeof(IUserGroup))] - [MapperFor(typeof(UserGroup))] - public sealed class UserGroupMapper : BaseMapper - { - public UserGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(UserGroup.Id), nameof(UserGroupDto.Id)); - DefineMap(nameof(UserGroup.Alias), nameof(UserGroupDto.Alias)); - DefineMap(nameof(UserGroup.Name), nameof(UserGroupDto.Name)); - DefineMap(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon)); - DefineMap(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId)); - DefineMap(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId)); - } +/// +/// Represents a to DTO mapper used to translate the properties of the public api +/// implementation to that of the database's DTO as sql: [tableName].[columnName]. +/// +[MapperFor(typeof(IUserGroup))] +[MapperFor(typeof(UserGroup))] +public sealed class UserGroupMapper : BaseMapper +{ + public UserGroupMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(UserGroup.Id), nameof(UserGroupDto.Id)); + DefineMap(nameof(UserGroup.Alias), nameof(UserGroupDto.Alias)); + DefineMap(nameof(UserGroup.Name), nameof(UserGroupDto.Name)); + DefineMap(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon)); + DefineMap(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId)); + DefineMap(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs index 53c229c935..92af2773cf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserMapper.cs @@ -1,35 +1,35 @@ -using System; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - [MapperFor(typeof(IUser))] - [MapperFor(typeof(User))] - public sealed class UserMapper : BaseMapper - { - public UserMapper(Lazy sqlContext, MapperConfigurationStore maps) - : base(sqlContext, maps) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - protected override void DefineMaps() - { - DefineMap(nameof(User.Id), nameof(UserDto.Id)); - DefineMap(nameof(User.Email), nameof(UserDto.Email)); - DefineMap(nameof(User.Username), nameof(UserDto.Login)); - DefineMap(nameof(User.RawPasswordValue), nameof(UserDto.Password)); - DefineMap(nameof(User.Name), nameof(UserDto.UserName)); - //NOTE: This column in the db is *not* used! - //DefineMap(nameof(User.DefaultPermissions), nameof(UserDto.DefaultPermissions)); - DefineMap(nameof(User.IsApproved), nameof(UserDto.Disabled)); - DefineMap(nameof(User.IsLockedOut), nameof(UserDto.NoConsole)); - DefineMap(nameof(User.Language), nameof(UserDto.UserLanguage)); - DefineMap(nameof(User.CreateDate), nameof(UserDto.CreateDate)); - DefineMap(nameof(User.UpdateDate), nameof(UserDto.UpdateDate)); - DefineMap(nameof(User.LastLockoutDate), nameof(UserDto.LastLockoutDate)); - DefineMap(nameof(User.LastLoginDate), nameof(UserDto.LastLoginDate)); - DefineMap(nameof(User.LastPasswordChangeDate), nameof(UserDto.LastPasswordChangeDate)); - DefineMap(nameof(User.SecurityStamp), nameof(UserDto.SecurityStampToken)); - } +[MapperFor(typeof(IUser))] +[MapperFor(typeof(User))] +public sealed class UserMapper : BaseMapper +{ + public UserMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { + } + + protected override void DefineMaps() + { + DefineMap(nameof(User.Id), nameof(UserDto.Id)); + DefineMap(nameof(User.Email), nameof(UserDto.Email)); + DefineMap(nameof(User.Username), nameof(UserDto.Login)); + DefineMap(nameof(User.RawPasswordValue), nameof(UserDto.Password)); + DefineMap(nameof(User.Name), nameof(UserDto.UserName)); + + // NOTE: This column in the db is *not* used! + // DefineMap(nameof(User.DefaultPermissions), nameof(UserDto.DefaultPermissions)); + DefineMap(nameof(User.IsApproved), nameof(UserDto.Disabled)); + DefineMap(nameof(User.IsLockedOut), nameof(UserDto.NoConsole)); + DefineMap(nameof(User.Language), nameof(UserDto.UserLanguage)); + DefineMap(nameof(User.CreateDate), nameof(UserDto.CreateDate)); + DefineMap(nameof(User.UpdateDate), nameof(UserDto.UpdateDate)); + DefineMap(nameof(User.LastLockoutDate), nameof(UserDto.LastLockoutDate)); + DefineMap(nameof(User.LastLoginDate), nameof(UserDto.LastLoginDate)); + DefineMap(nameof(User.LastPasswordChangeDate), nameof(UserDto.LastPasswordChangeDate)); + DefineMap(nameof(User.SecurityStamp), nameof(UserDto.SecurityStampToken)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs index 86aeed4b7e..703d081b10 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserSectionMapper.cs @@ -1,30 +1,30 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.Mappers -{ - //[MapperFor(typeof(UserSection))] - //public sealed class UserSectionMapper : BaseMapper - //{ - // private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers; - // //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it - // // otherwise that would fail because there is no public constructor. - // public UserSectionMapper() - // { - // BuildMap(); - // } - // #region Overrides of BaseMapper +// [MapperFor(typeof(UserSection))] +// public sealed class UserSectionMapper : BaseMapper +// { +// private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); - // internal override ConcurrentDictionary PropertyInfoCache - // { - // get { return PropertyInfoCacheInstance; } - // } +// //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it +// // otherwise that would fail because there is no public constructor. +// public UserSectionMapper() +// { +// BuildMap(); +// } - // internal override void BuildMap() - // { - // CacheMap(src => src.UserId, dto => dto.UserId); - // CacheMap(src => src.SectionAlias, dto => dto.AppAlias); - // } +// #region Overrides of BaseMapper - // #endregion - //} -} +// internal override ConcurrentDictionary PropertyInfoCache +// { +// get { return PropertyInfoCacheInstance; } +// } + +// internal override void BuildMap() +// { +// CacheMap(src => src.UserId, dto => dto.UserId); +// CacheMap(src => src.SectionAlias, dto => dto.AppAlias); +// } + +// #endregion +// } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs index c53076ff18..92ccb0fc81 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -1,121 +1,117 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Linq; using Microsoft.Data.SqlClient; using NPoco; using NPoco.SqlServer; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to NPoco Database class. +/// +public static partial class NPocoDatabaseExtensions { /// - /// Provides extension methods to NPoco Database class. + /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the + /// underlying RetryDbConnection and ProfiledDbTransaction /// - public static partial class NPocoDatabaseExtensions + /// + /// This is required to use NPoco's own method because we use + /// wrapped DbConnection and DbTransaction instances. + /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for + /// bulk inserting of records for + /// any other database type and in which case will just insert records one at a time. + /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own + /// BulkInsertRecords methods + /// do not handle this scenario. + /// + public static void ConfigureNPocoBulkExtensions() { - /// - /// Configures NPoco's SqlBulkCopyHelper to use the correct SqlConnection and SqlTransaction instances from the - /// underlying RetryDbConnection and ProfiledDbTransaction - /// - /// - /// This is required to use NPoco's own method because we use - /// wrapped DbConnection and DbTransaction instances. - /// NPoco's InsertBulk method only caters for efficient bulk inserting records for Sql Server, it does not cater for - /// bulk inserting of records for - /// any other database type and in which case will just insert records one at a time. - /// NPoco's InsertBulk method also deals with updating the passed in entity's PK/ID once it's inserted whereas our own - /// BulkInsertRecords methods - /// do not handle this scenario. - /// - public static void ConfigureNPocoBulkExtensions() + SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); + SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); + } + + /// + /// Determines whether a column should be part of a bulk-insert. + /// + /// The PocoData object corresponding to the record's type. + /// The column. + /// A value indicating whether the column should be part of the bulk-insert. + /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. + public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => + column.Value.ResultColumn == false + && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); + + /// + /// Creates bulk-insert commands. + /// + /// The type of the records. + /// The database. + /// The records. + /// The sql commands to execute. + internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) + { + if (database.Connection == null) { - SqlBulkCopyHelper.SqlConnectionResolver = dbConn => GetTypedConnection(dbConn); - SqlBulkCopyHelper.SqlTransactionResolver = dbTran => GetTypedTransaction(dbTran); + throw new ArgumentException("Null database?.connection.", nameof(database)); } + PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); - /// - /// Creates bulk-insert commands. - /// - /// The type of the records. - /// The database. - /// The records. - /// The sql commands to execute. - internal static IDbCommand[] GenerateBulkInsertCommands(this IUmbracoDatabase database, T[] records) + // get columns to include, = number of parameters per row + KeyValuePair[] columns = + pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); + var paramsPerRecord = columns.Length; + + // format columns to sql + var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); + var columnNames = string.Join( + ", ", + columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); + + // example: + // assume 4168 records, each record containing 8 fields, ie 8 command parameters + // max 2100 parameter per command + // Math.Floor(2100 / 8) = 262 record per command + // 4168 / 262 = 15.908... = there will be 16 command in total + // (if we have disabled db parameters, then all records will be included, in only one command) + var recordsPerCommand = paramsPerRecord == 0 + ? int.MaxValue + : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); + var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); + + var commands = new IDbCommand[commandsCount]; + var recordsIndex = 0; + var recordsLeftToInsert = records.Length; + var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); + for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) { - if (database?.Connection == null) + DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); + var parameterIndex = 0; + var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); + var recordsValues = new string[commandRecords]; + for (var commandRecordIndex = 0; + commandRecordIndex < commandRecords; + commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) { - throw new ArgumentException("Null database?.connection.", nameof(database)); - } - - PocoData pocoData = database.PocoDataFactory.ForType(typeof(T)); - - // get columns to include, = number of parameters per row - KeyValuePair[] columns = - pocoData.Columns.Where(c => IncludeColumn(pocoData, c)).ToArray(); - var paramsPerRecord = columns.Length; - - // format columns to sql - var tableName = database.DatabaseType.EscapeTableName(pocoData.TableInfo.TableName); - var columnNames = string.Join(", ", - columns.Select(c => tableName + "." + database.DatabaseType.EscapeSqlIdentifier(c.Key))); - - // example: - // assume 4168 records, each record containing 8 fields, ie 8 command parameters - // max 2100 parameter per command - // Math.Floor(2100 / 8) = 262 record per command - // 4168 / 262 = 15.908... = there will be 16 command in total - // (if we have disabled db parameters, then all records will be included, in only one command) - var recordsPerCommand = paramsPerRecord == 0 - ? int.MaxValue - : Convert.ToInt32(Math.Floor((double)Constants.Sql.MaxParameterCount / paramsPerRecord)); - var commandsCount = Convert.ToInt32(Math.Ceiling((double)records.Length / recordsPerCommand)); - - var commands = new IDbCommand[commandsCount]; - var recordsIndex = 0; - var recordsLeftToInsert = records.Length; - var prefix = database.DatabaseType.GetParameterPrefix(database.ConnectionString); - for (var commandIndex = 0; commandIndex < commandsCount; commandIndex++) - { - DbCommand command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty); - var parameterIndex = 0; - var commandRecords = Math.Min(recordsPerCommand, recordsLeftToInsert); - var recordsValues = new string[commandRecords]; - for (var commandRecordIndex = 0; - commandRecordIndex < commandRecords; - commandRecordIndex++, recordsIndex++, recordsLeftToInsert--) + T record = records[recordsIndex]; + var recordValues = new string[columns.Length]; + for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) { - T record = records[recordsIndex]; - var recordValues = new string[columns.Length]; - for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++) - { - database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); - recordValues[columnIndex] = prefix + parameterIndex++; - } - - recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; + database.AddParameter(command, columns[columnIndex].Value.GetValue(record)); + recordValues[columnIndex] = prefix + parameterIndex++; } - command.CommandText = - $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; - commands[commandIndex] = command; + recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")"; } - return commands; + command.CommandText = + $"INSERT INTO {tableName} ({columnNames}) VALUES {string.Join(", ", recordsValues)}"; + commands[commandIndex] = command; } - /// - /// Determines whether a column should be part of a bulk-insert. - /// - /// The PocoData object corresponding to the record's type. - /// The column. - /// A value indicating whether the column should be part of the bulk-insert. - /// Columns that are primary keys and auto-incremental, or result columns, are excluded from bulk-inserts. - public static bool IncludeColumn(PocoData pocoData, KeyValuePair column) => - column.Value.ResultColumn == false - && (pocoData.TableInfo.AutoIncrement == false || column.Key != pocoData.TableInfo.PrimaryKey); + return commands; } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs index 7537ffc48f..1a64b44aa6 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs @@ -1,6 +1,5 @@ -using System; -using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; using NPoco; @@ -9,290 +8,320 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to NPoco Database class. +/// +public static partial class NPocoDatabaseExtensions { /// - /// Provides extension methods to NPoco Database class. + /// Iterates over the result of a paged data set with a db reader /// - public static partial class NPocoDatabaseExtensions + /// + /// + /// + /// 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, Sql? sqlCount) { - /// - /// Iterates over the result of a paged data set with a db reader - /// - /// - /// - /// - /// 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, Sql? sqlCount) - { - var sqlString = sql.SQL; - var sqlArgs = sql.Arguments; + var sqlString = sql.SQL; + var sqlArgs = sql.Arguments; - int? itemCount = null; - long pageIndex = 0; - do + int? itemCount = null; + long pageIndex = 0; + do + { + // Get the paged queries + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage); + + // get the item count once + if (itemCount == null) { - // Get the paged queries - database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage); + itemCount = database.ExecuteScalar( + sqlCount?.SQL ?? generatedSqlCount, + sqlCount?.Arguments ?? sqlArgs); + } - // get the item count once - if (itemCount == null) - { - itemCount = database.ExecuteScalar(sqlCount?.SQL ?? generatedSqlCount, sqlCount?.Arguments ?? sqlArgs); - } - pageIndex++; + pageIndex++; - // iterate over rows without allocating all items to memory (Query vs Fetch) - foreach (var row in database.Query(sqlPage, sqlArgs)) - { - yield return row; - } + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (T row in database.Query(sqlPage, sqlArgs)) + { + yield return row; + } + } + while (pageIndex * pageSize < itemCount); + } - } 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 + // IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...) + // BEGIN + // UPDATE table SET ... WHERE ... + // END + // ELSE + // BEGIN + // INSERT INTO table (...) VALUES (...) + // END + // + // works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK + // + // TODO: use the proper database syntax, not this kludge + + /// + /// Safely inserts a record, or updates if it exists, based on a unique constraint. + /// + /// + /// + /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the + /// poco object + /// passed in will contain the updated value. + /// + /// + /// + /// We cannot rely on database-specific options because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that + /// would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's + /// value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. + /// + /// + public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco) + where T : class => + db.InsertOrUpdate(poco, null, null); + + /// + /// Safely inserts a record, or updates if it exists, based on a unique constraint. + /// + /// + /// + /// + /// If the entity has a composite key they you need to specify the update command explicitly + /// + /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the + /// poco object + /// passed in will contain the updated value. + /// + /// + /// + /// We cannot rely on database-specific options because SQLCE + /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that + /// would mean revisiting + /// isolation levels globally. We want to keep it simple for the time being and manage it manually. + /// + /// We handle it by trying to update, then insert, etc. until something works, or we get bored. + /// + /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's + /// value + /// once T1 and T2 have completed. Whereas here, it could contain T1's value. + /// + /// + public static RecordPersistenceType InsertOrUpdate( + this IUmbracoDatabase db, + T poco, + string? updateCommand, + object? updateArgs) + where T : class + { + if (poco == null) + { + throw new ArgumentNullException(nameof(poco)); } - /// - /// 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); + // TODO: NPoco has a Save method that works with the primary key + // in any case, no point trying to update if there's no primary key! - // NOTE - // - // proper way to do it with TSQL and SQLCE - // IF EXISTS (SELECT ... FROM table WITH (UPDLOCK,HOLDLOCK)) WHERE ...) - // BEGIN - // UPDATE table SET ... WHERE ... - // END - // ELSE - // BEGIN - // INSERT INTO table (...) VALUES (...) - // END - // - // works in READ COMMITED, TSQL & SQLCE lock the constraint even if it does not exist, so INSERT is OK - // - // TODO: use the proper database syntax, not this kludge - - /// - /// Safely inserts a record, or updates if it exists, based on a unique constraint. - /// - /// - /// - /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object - /// passed in will contain the updated value. - /// - /// We cannot rely on database-specific options because SQLCE - /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting - /// isolation levels globally. We want to keep it simple for the time being and manage it manually. - /// We handle it by trying to update, then insert, etc. until something works, or we get bored. - /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value - /// once T1 and T2 have completed. Whereas here, it could contain T1's value. - /// - public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco) - where T : class + // try to update + var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null + ? db.Update(poco) + : db.Update(updateCommand!, updateArgs); + if (rowCount > 0) { - return db.InsertOrUpdate(poco, null, null); + return RecordPersistenceType.Update; } - /// - /// Safely inserts a record, or updates if it exists, based on a unique constraint. - /// - /// - /// - /// - /// If the entity has a composite key they you need to specify the update command explicitly - /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object - /// passed in will contain the updated value. - /// - /// We cannot rely on database-specific options because SQLCE - /// does not support any of them. Ideally this should be achieved with proper transaction isolation levels but that would mean revisiting - /// isolation levels globally. We want to keep it simple for the time being and manage it manually. - /// We handle it by trying to update, then insert, etc. until something works, or we get bored. - /// Note that with proper transactions, if T2 begins after T1 then we are sure that the database will contain T2's value - /// once T1 and T2 have completed. Whereas here, it could contain T1's value. - /// - public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, - T poco, - string? updateCommand, - object? updateArgs) - where T : class + // failed: does not exist, need to insert + // RC1 race cond here: another thread may insert a record with the same constraint + var i = 0; + while (i++ < 4) { - if (poco == null) - throw new ArgumentNullException(nameof(poco)); + try + { + // try to insert + db.Insert(poco); + return RecordPersistenceType.Insert; + } + catch (SqlException) + { + // assuming all db engines will throw SQLException exception + // failed: exists (due to race cond RC1) + // RC2 race cond here: another thread may remove the record - // TODO: NPoco has a Save method that works with the primary key - // in any case, no point trying to update if there's no primary key! - - // try to update - var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null + // try to update + rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null ? db.Update(poco) : db.Update(updateCommand!, updateArgs); - if (rowCount > 0) - return RecordPersistenceType.Update; - - // failed: does not exist, need to insert - // RC1 race cond here: another thread may insert a record with the same constraint - - var i = 0; - while (i++ < 4) - { - try + if (rowCount > 0) { - // try to insert - db.Insert(poco); - return RecordPersistenceType.Insert; + return RecordPersistenceType.Update; } - catch (SqlException) // assuming all db engines will throw that exception - { - // failed: exists (due to race cond RC1) - // RC2 race cond here: another thread may remove the record - // try to update - rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null - ? db.Update(poco) - : db.Update(updateCommand!, updateArgs); - if (rowCount > 0) - return RecordPersistenceType.Update; - - // failed: does not exist (due to race cond RC2), need to insert - // loop - } - } - - // this can go on forever... have to break at some point and report an error. - throw new DataException("Record could not be inserted or updated."); - } - - /// - /// This will escape single @ symbols for npoco values so it doesn't think it's a parameter - /// - /// - /// - public static string EscapeAtSymbols(string value) - { - if (value.Contains("@") == false) return value; - - //this fancy regex will only match a single @ not a double, etc... - var regex = new Regex("(? - /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TConnection GetTypedConnection(IDbConnection connection) - where TConnection : class, IDbConnection - { - var c = connection; - for (;;) - { - switch (c) - { - case TConnection ofType: - return ofType; - case RetryDbConnection retry: - c = retry.Inner; - break; - case ProfiledDbConnection profiled: - c = profiled.WrappedConnection; - break; - default: - throw new NotSupportedException(connection.GetType().FullName); - } + // failed: does not exist (due to race cond RC2), need to insert + // loop } } - /// - /// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TTransaction GetTypedTransaction(IDbTransaction? transaction) - where TTransaction : class, IDbTransaction + // this can go on forever... have to break at some point and report an error. + throw new DataException("Record could not be inserted or updated."); + } + + /// + /// This will escape single @ symbols for npoco values so it doesn't think it's a parameter + /// + /// + /// + public static string EscapeAtSymbols(string value) + { + if (value.Contains("@") == false) { - var t = transaction; - for (;;) + return value; + } + + // this fancy regex will only match a single @ not a double, etc... + var regex = new Regex("(? + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TConnection GetTypedConnection(IDbConnection connection) + where TConnection : class, IDbConnection + { + IDbConnection? c = connection; + for (; ;) + { + switch (c) { - switch (t) - { - case TTransaction ofType: - return ofType; - case ProfiledDbTransaction profiled: - t = profiled.WrappedTransaction; - break; - default: - throw new NotSupportedException(transaction?.GetType().FullName); - } + case TConnection ofType: + return ofType; + case RetryDbConnection retry: + c = retry.Inner; + break; + case ProfiledDbConnection profiled: + c = profiled.WrappedConnection; + break; + default: + throw new NotSupportedException(connection.GetType().FullName); } } - - /// - /// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff - /// - /// - /// - /// - public static TCommand GetTypedCommand(IDbCommand command) - where TCommand : class, IDbCommand - { - var c = command; - for (;;) - { - switch (c) - { - case TCommand ofType: - return ofType; - case FaultHandlingDbCommand faultHandling: - c = faultHandling.Inner; - break; - case ProfiledDbCommand profiled: - c = profiled.InternalCommand; - break; - default: - throw new NotSupportedException(command.GetType().FullName); - } - } - } - - public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName) - { - var sql = new Sql(string.Format( - sqlSyntax.TruncateTable, - sqlSyntax.GetQuotedTableName(tableName))); - db.Execute(sql); - } - - public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database) - { - var transaction = database.Transaction; - return transaction?.IsolationLevel ?? IsolationLevel.Unspecified; - } - - public static IEnumerable FetchByGroups(this IDatabase db, IEnumerable source, int groupSize, Func, Sql> sqlFactory) - { - return source.SelectByGroups(x => db.Fetch(sqlFactory(x)), groupSize); - } } + + /// + /// Returns the underlying transaction as a typed transaction - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TTransaction GetTypedTransaction(IDbTransaction? transaction) + where TTransaction : class, IDbTransaction + { + IDbTransaction? t = transaction; + for (; ;) + { + switch (t) + { + case TTransaction ofType: + return ofType; + case ProfiledDbTransaction profiled: + t = profiled.WrappedTransaction; + break; + default: + throw new NotSupportedException(transaction?.GetType().FullName); + } + } + } + + /// + /// Returns the underlying command as a typed command - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + public static TCommand GetTypedCommand(IDbCommand command) + where TCommand : class, IDbCommand + { + IDbCommand? c = command; + for (; ;) + { + switch (c) + { + case TCommand ofType: + return ofType; + case FaultHandlingDbCommand faultHandling: + c = faultHandling.Inner; + break; + case ProfiledDbCommand profiled: + c = profiled.InternalCommand; + break; + default: + throw new NotSupportedException(command.GetType().FullName); + } + } + } + + public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName) + { + var sql = new Sql(string.Format( + sqlSyntax.TruncateTable, + sqlSyntax.GetQuotedTableName(tableName))); + db.Execute(sql); + } + + public static IsolationLevel GetCurrentTransactionIsolationLevel(this IDatabase database) + { + DbTransaction? transaction = database.Transaction; + return transaction?.IsolationLevel ?? IsolationLevel.Unspecified; + } + + public static IEnumerable FetchByGroups(this IDatabase db, IEnumerable source, int groupSize, Func, Sql> sqlFactory) => + source.SelectByGroups(x => db.Fetch(sqlFactory(x)), groupSize); } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs index b349824591..3159cf34e1 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseTypeExtensions.cs @@ -1,19 +1,19 @@ -using System; using NPoco; +using NPoco.DatabaseTypes; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class NPocoDatabaseTypeExtensions { - internal static class NPocoDatabaseTypeExtensions - { - [Obsolete("Usage of this method indicates a code smell.")] - public static bool IsSqlServer(this DatabaseType databaseType) => - // note that because SqlServerDatabaseType is the base class for - // all Sql Server types eg SqlServer2012DatabaseType, this will - // test *any* version of Sql Server. - databaseType is NPoco.DatabaseTypes.SqlServerDatabaseType; + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlServer(this DatabaseType databaseType) => - [Obsolete("Usage of this method indicates a code smell.")] - public static bool IsSqlite(this DatabaseType databaseType) - => databaseType is NPoco.DatabaseTypes.SQLiteDatabaseType; - } + // note that because SqlServerDatabaseType is the base class for + // all Sql Server types eg SqlServer2012DatabaseType, this will + // test *any* version of Sql Server. + databaseType is SqlServerDatabaseType; + + [Obsolete("Usage of this method indicates a code smell.")] + public static bool IsSqlite(this DatabaseType databaseType) + => databaseType is SQLiteDatabaseType; } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs index 3d71c0225e..23ae1b2516 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollection.cs @@ -1,14 +1,12 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public sealed class NPocoMapperCollection : BuilderCollectionBase { - public sealed class NPocoMapperCollection : BuilderCollectionBase + public NPocoMapperCollection(Func> items) + : base(items) { - public NPocoMapperCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs index 4840ceafe8..df7411d120 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoMapperCollectionBuilder.cs @@ -1,10 +1,9 @@ using NPoco; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public sealed class NPocoMapperCollectionBuilder : SetCollectionBuilderBase { - public sealed class NPocoMapperCollectionBuilder : SetCollectionBuilderBase - { - protected override NPocoMapperCollectionBuilder This => this; - } + protected override NPocoMapperCollectionBuilder This => this; } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index 0d6e455ecf..9fae556372 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -1,7 +1,4 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; @@ -10,6 +7,7 @@ using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; namespace Umbraco.Extensions { @@ -133,13 +131,17 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql WhereAnyIn(this Sql sql, Expression>[] fields, IEnumerable values) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var fieldNames = fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); var sb = new StringBuilder(); sb.Append("("); for (var i = 0; i < fieldNames.Length; i++) { - if (i > 0) sb.Append(" OR "); + if (i > 0) + { + sb.Append(" OR "); + } + sb.Append(fieldNames[i]); sql.Append(" IN (@values)"); } @@ -174,7 +176,10 @@ namespace Umbraco.Extensions for (var i = 0; i < predicates.Length; i++) { if (i > 0) + { wsql.Append(") OR ("); + } + var temp = new Sql(sql.SqlContext); temp = predicates[i](temp); wsql.Append(temp.SQL.TrimStart("WHERE "), temp.Arguments); @@ -225,12 +230,15 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql From(this Sql sql, string? alias = null) { - var type = typeof (TDto); + Type type = typeof (TDto); var tableName = type.GetTableName(); var from = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); if (!string.IsNullOrWhiteSpace(alias)) + { from += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } + sql.From(from); return sql; @@ -261,7 +269,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql OrderBy(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -289,7 +297,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql OrderByDescending(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -328,7 +336,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql GroupBy(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -344,7 +352,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndBy(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -360,7 +368,7 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndByDescending(this Sql sql, params Expression>[] fields) { - var sqlSyntax = sql.SqlContext.SqlSyntax; + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); @@ -380,10 +388,13 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql CrossJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.Append("CROSS JOIN " + join); } @@ -397,10 +408,13 @@ namespace Umbraco.Extensions /// A SqlJoin statement. public static Sql.SqlJoinClause InnerJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.InnerJoin(join); } @@ -432,10 +446,13 @@ namespace Umbraco.Extensions /// A SqlJoin statement. public static Sql.SqlJoinClause LeftJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.LeftJoin(join); } @@ -482,10 +499,13 @@ namespace Umbraco.Extensions /// A SqlJoin statement. public static Sql.SqlJoinClause RightJoin(this Sql sql, string? alias = null) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName); - if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + if (alias != null) + { + join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias); + } return sql.RightJoin(join); } @@ -587,7 +607,11 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql SelectTop(this Sql sql, int count) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.SqlContext.SqlSyntax.SelectTop(sql, count); } @@ -599,9 +623,17 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql SelectCount(this Sql sql, string? alias = null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + var text = "COUNT(*)"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Select(text); } @@ -631,13 +663,21 @@ namespace Umbraco.Extensions /// public static Sql SelectCount(this Sql sql, string? alias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var sqlSyntax = sql.SqlContext.SqlSyntax; + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); var text = "COUNT (" + string.Join(", ", columns) + ")"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Select(text); } @@ -648,7 +688,11 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql SelectAll(this Sql sql) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Select("*"); } @@ -664,7 +708,11 @@ namespace Umbraco.Extensions /// public static Sql Select(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Select(sql.GetColumns(columnExpressions: fields)); } @@ -680,7 +728,11 @@ namespace Umbraco.Extensions /// public static Sql SelectDistinct(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + var columns = sql.GetColumns(columnExpressions: fields); sql.Append("SELECT DISTINCT " + string.Join(", ", columns)); return sql; @@ -707,7 +759,11 @@ namespace Umbraco.Extensions /// public static Sql Select(this Sql sql, string tableAlias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Select(sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields)); } @@ -719,7 +775,11 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndSelect(this Sql sql, params string[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Append(", " + string.Join(", ", fields)); } @@ -735,7 +795,11 @@ namespace Umbraco.Extensions /// public static Sql AndSelect(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Append(", " + string.Join(", ", sql.GetColumns(columnExpressions: fields))); } @@ -752,7 +816,11 @@ namespace Umbraco.Extensions /// public static Sql AndSelect(this Sql sql, string tableAlias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return sql.Append(", " + string.Join(", ", sql.GetColumns(tableAlias: tableAlias, columnExpressions: fields))); } @@ -764,9 +832,17 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql AndSelectCount(this Sql sql, string? alias = null) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + var text = ", COUNT(*)"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Append(text); } @@ -796,13 +872,21 @@ namespace Umbraco.Extensions /// public static Sql AndSelectCount(this Sql sql, string? alias = null, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - var sqlSyntax = sql.SqlContext.SqlSyntax; + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + ISqlSyntaxProvider sqlSyntax = sql.SqlContext.SqlSyntax; var columns = fields.Length == 0 ? sql.GetColumns(withAlias: false) : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray(); var text = ", COUNT (" + string.Join(", ", columns) + ")"; - if (alias != null) text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + if (alias != null) + { + text += " AS " + sql.SqlContext.SqlSyntax.GetQuotedColumnName(alias); + } + return sql.Append(text); } @@ -815,7 +899,10 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql Select(this Sql sql, Func, SqlRef> reference) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } sql.Select(sql.GetColumns()); @@ -835,7 +922,10 @@ namespace Umbraco.Extensions /// is added, so that it is possible to add (e.g. calculated) columns to the referencing Dto. public static Sql Select(this Sql sql, Func, SqlRef> reference, Func, Sql> sqlexpr) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } sql.Select(sql.GetColumns()); @@ -907,7 +997,7 @@ namespace Umbraco.Extensions /// A SqlRef statement. public SqlRef Select(Expression> field, string? tableAlias, Func, SqlRef>? reference = null) { - var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; + PropertyInfo? property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; return Select(property, tableAlias, reference); } @@ -937,14 +1027,17 @@ namespace Umbraco.Extensions /// public SqlRef Select(Expression>> field, string? tableAlias, Func, SqlRef>? reference = null) { - var property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; + PropertyInfo? property = field == null ? null : ExpressionHelper.FindProperty(field).Item1 as PropertyInfo; return Select(property, tableAlias, reference); } private SqlRef Select(PropertyInfo? propertyInfo, string? tableAlias, Func, SqlRef>? nested = null) { var referenceName = propertyInfo?.Name ?? typeof (TDto).Name; - if (Prefix != null) referenceName = Prefix + PocoData.Separator + referenceName; + if (Prefix != null) + { + referenceName = Prefix + PocoData.Separator + referenceName; + } var columns = Sql.GetColumns(tableAlias, referenceName); Sql.Append(", " + string.Join(", ", columns)); @@ -965,7 +1058,11 @@ namespace Umbraco.Extensions /// public static string Columns(this Sql sql, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false)); } @@ -974,7 +1071,11 @@ namespace Umbraco.Extensions /// public static string ColumnsForInsert(this Sql sql, params Expression>[]? fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, forInsert: true)); } @@ -991,7 +1092,11 @@ namespace Umbraco.Extensions /// public static string Columns(this Sql sql, string alias, params Expression>[] fields) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + return string.Join(", ", sql.GetColumns(columnExpressions: fields, withAlias: false, tableAlias: alias)); } @@ -1007,7 +1112,7 @@ namespace Umbraco.Extensions public static Sql Delete(this Sql sql) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); // FROM optional SQL server, but not elsewhere. @@ -1027,7 +1132,7 @@ namespace Umbraco.Extensions public static Sql Update(this Sql sql) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)}"); @@ -1036,7 +1141,7 @@ namespace Umbraco.Extensions public static Sql Update(this Sql sql, Func, SqlUpd> updates) { - var type = typeof(TDto); + Type type = typeof(TDto); var tableName = type.GetTableName(); sql.Append($"UPDATE {sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName)} SET"); @@ -1044,7 +1149,7 @@ namespace Umbraco.Extensions var u = new SqlUpd(sql.SqlContext); u = updates(u); var first = true; - foreach (var setExpression in u.SetExpressions) + foreach (Tuple setExpression in u.SetExpressions) { switch (setExpression.Item2) { @@ -1063,7 +1168,9 @@ namespace Umbraco.Extensions } if (!first) + { sql.Append(" "); + } return sql; } @@ -1120,8 +1227,8 @@ namespace Umbraco.Extensions // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]" // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]" - var matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); - var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); + MatchCollection matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); + Match? match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); return match == null ? field : match.Groups[2].Value; } @@ -1131,7 +1238,7 @@ namespace Umbraco.Extensions private static string[] GetColumns(this Sql sql, string? tableAlias = null, string? referenceName = null, Expression>[]? columnExpressions = null, bool withAlias = true, bool forInsert = false) { - var pd = sql.SqlContext.PocoDataFactory.ForType(typeof (TDto)); + PocoData? pd = sql.SqlContext.PocoDataFactory.ForType(typeof (TDto)); var tableName = tableAlias ?? pd.TableInfo.TableName; var queryColumns = pd.QueryColumns.ToList(); @@ -1141,13 +1248,16 @@ namespace Umbraco.Extensions { var names = columnExpressions.Select(x => { - (var member, var alias) = ExpressionHelper.FindProperty(x); + (MemberInfo member, var alias) = ExpressionHelper.FindProperty(x); var field = member as PropertyInfo; var fieldName = field?.GetColumnName(); if (alias != null && fieldName is not null) { if (aliases == null) + { aliases = new Dictionary(); + } + aliases[fieldName] = alias; } return fieldName; @@ -1163,7 +1273,9 @@ namespace Umbraco.Extensions string? GetAlias(PocoColumn column) { if (aliases != null && aliases.TryGetValue(column.ColumnName, out var alias)) + { return alias; + } return withAlias ? (string.IsNullOrEmpty(column.ColumnAlias) ? column.MemberInfoKey : column.ColumnAlias) : null; } @@ -1178,13 +1290,13 @@ namespace Umbraco.Extensions // TODO: returning string.Empty for now // BUT the code bits that calls this method cannot deal with string.Empty so we // should either throw, or fix these code bits... - var attr = type.FirstAttribute(); + TableNameAttribute? attr = type.FirstAttribute(); return string.IsNullOrWhiteSpace(attr?.Value) ? string.Empty : attr.Value; } private static string GetColumnName(this PropertyInfo column) { - var attr = column.FirstAttribute(); + ColumnAttribute? attr = column.FirstAttribute(); return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; } @@ -1205,7 +1317,9 @@ namespace Umbraco.Extensions text.AppendLine(sql); if (arguments == null || arguments.Length == 0) + { return; + } text.Append(" --"); diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs b/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs index ae812193c9..2f3041fb33 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/CachedExpression.cs @@ -1,48 +1,46 @@ -using System; using System.Linq.Expressions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents an expression which caches the visitor's result. +/// +internal class CachedExpression : Expression { + private string _visitResult = null!; + /// - /// Represents an expression which caches the visitor's result. + /// Gets or sets the inner Expression. /// - internal class CachedExpression : Expression + public Expression InnerExpression { get; private set; } = null!; + + /// + /// Gets or sets the compiled SQL statement output. + /// + public string VisitResult { - private string _visitResult = null!; - - /// - /// Gets or sets the inner Expression. - /// - public Expression InnerExpression { get; private set; } = null!; - - /// - /// Gets or sets the compiled SQL statement output. - /// - public string VisitResult + get => _visitResult; + set { - get => _visitResult; - set + if (Visited) { - if (Visited) - throw new InvalidOperationException("Cached expression has already been visited."); - _visitResult = value; - Visited = true; + throw new InvalidOperationException("Cached expression has already been visited."); } - } - /// - /// Gets or sets a value indicating whether the cache Expression has been compiled already. - /// - public bool Visited { get; private set; } - - /// - /// Replaces the inner expression. - /// - /// expression. - /// The new expression is assumed to have different parameter but produce the same SQL statement. - public void Wrap(Expression expression) - { - InnerExpression = expression; + _visitResult = value; + Visited = true; } } + + /// + /// Gets or sets a value indicating whether the cache Expression has been compiled already. + /// + public bool Visited { get; private set; } + + /// + /// Replaces the inner expression. + /// + /// expression. + /// The new expression is assumed to have different parameter but produce the same SQL statement. + public void Wrap(Expression expression) => InnerExpression = expression; } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs index f399f66aca..897628f806 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs @@ -1,8 +1,5 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Linq.Expressions; using System.Text; using Umbraco.Cms.Core.Composing; @@ -10,769 +7,840 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +// TODO: are we basically duplicating entire parts of NPoco just because of SqlSyntax ?! +// try to use NPoco's version ! + +/// +/// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. +/// +/// This object is stateful and cannot be re-used to parse an expression. +internal abstract class ExpressionVisitorBase { - // TODO: are we basically duplicating entire parts of NPoco just because of SqlSyntax ?! - // try to use NPoco's version ! + /// + /// Gets the list of SQL parameters. + /// + protected readonly List SqlParameters = new(); + + protected ExpressionVisitorBase(ISqlSyntaxProvider sqlSyntax) => SqlSyntax = sqlSyntax; /// - /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. + /// Gets or sets a value indicating whether the visited expression has been visited already, + /// in which case visiting will just populate the SQL parameters. /// - /// This object is stateful and cannot be re-used to parse an expression. - internal abstract class ExpressionVisitorBase + protected bool Visited { get; set; } + + /// + /// Gets or sets the SQL syntax provider for the current database. + /// + protected ISqlSyntaxProvider SqlSyntax { get; } + + /// + /// Gets the SQL parameters. + /// + /// + public object[] GetSqlParameters() => SqlParameters.ToArray(); + + /// + /// Visits the expression and produces the corresponding SQL statement. + /// + /// The expression + /// The SQL statement corresponding to the expression. + /// Also populates the SQL parameters. + public virtual string Visit(Expression? expression) { - protected ExpressionVisitorBase(ISqlSyntaxProvider sqlSyntax) + if (expression == null) { - SqlSyntax = sqlSyntax; + return string.Empty; } - /// - /// Gets or sets a value indicating whether the visited expression has been visited already, - /// in which case visiting will just populate the SQL parameters. - /// - protected bool Visited { get; set; } - - /// - /// Gets or sets the SQL syntax provider for the current database. - /// - protected ISqlSyntaxProvider SqlSyntax { get; } - - /// - /// Gets the list of SQL parameters. - /// - protected readonly List SqlParameters = new List(); - - /// - /// Gets the SQL parameters. - /// - /// - public object[] GetSqlParameters() + // if the expression is a CachedExpression, + // visit the inner expression if not already visited + var cachedExpression = expression as CachedExpression; + if (cachedExpression != null) { - return SqlParameters.ToArray(); + Visited = cachedExpression.Visited; + expression = cachedExpression.InnerExpression; } - /// - /// Visits the expression and produces the corresponding SQL statement. - /// - /// The expression - /// The SQL statement corresponding to the expression. - /// Also populates the SQL parameters. - public virtual string Visit(Expression? expression) + string result; + + switch (expression.NodeType) { - if (expression == null) return string.Empty; - - // if the expression is a CachedExpression, - // visit the inner expression if not already visited - var cachedExpression = expression as CachedExpression; - if (cachedExpression != null) - { - Visited = cachedExpression.Visited; - expression = cachedExpression.InnerExpression; - } - - string result; - - switch (expression.NodeType) - { - case ExpressionType.Lambda: - result = VisitLambda(expression as LambdaExpression); - break; - case ExpressionType.MemberAccess: - result = VisitMemberAccess(expression as MemberExpression); - break; - case ExpressionType.Constant: - result = VisitConstant(expression as ConstantExpression); - break; - case ExpressionType.Add: - case ExpressionType.AddChecked: - case ExpressionType.Subtract: - case ExpressionType.SubtractChecked: - case ExpressionType.Multiply: - case ExpressionType.MultiplyChecked: - case ExpressionType.Divide: - case ExpressionType.Modulo: - case ExpressionType.And: - case ExpressionType.AndAlso: - case ExpressionType.Or: - case ExpressionType.OrElse: - case ExpressionType.LessThan: - case ExpressionType.LessThanOrEqual: - case ExpressionType.GreaterThan: - case ExpressionType.GreaterThanOrEqual: - case ExpressionType.Equal: - case ExpressionType.NotEqual: - case ExpressionType.Coalesce: - case ExpressionType.ArrayIndex: - case ExpressionType.RightShift: - case ExpressionType.LeftShift: - case ExpressionType.ExclusiveOr: - result = VisitBinary(expression as BinaryExpression); - break; - case ExpressionType.Negate: - case ExpressionType.NegateChecked: - case ExpressionType.Not: - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - case ExpressionType.ArrayLength: - case ExpressionType.Quote: - case ExpressionType.TypeAs: - result = VisitUnary(expression as UnaryExpression); - break; - case ExpressionType.Parameter: - result = VisitParameter(expression as ParameterExpression); - break; - case ExpressionType.Call: - result = VisitMethodCall(expression as MethodCallExpression); - break; - case ExpressionType.New: - result = VisitNew(expression as NewExpression); - break; - case ExpressionType.NewArrayInit: - case ExpressionType.NewArrayBounds: - result = VisitNewArray(expression as NewArrayExpression); - break; - default: - result = expression.ToString(); - break; - } - - // if the expression is a CachedExpression, - // and is not already compiled, assign the result - if (cachedExpression == null) - return result; - if (!cachedExpression.Visited) - cachedExpression.VisitResult = result; - return cachedExpression.VisitResult; + case ExpressionType.Lambda: + result = VisitLambda(expression as LambdaExpression); + break; + case ExpressionType.MemberAccess: + result = VisitMemberAccess(expression as MemberExpression); + break; + case ExpressionType.Constant: + result = VisitConstant(expression as ConstantExpression); + break; + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: + case ExpressionType.Modulo: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.Coalesce: + case ExpressionType.ArrayIndex: + case ExpressionType.RightShift: + case ExpressionType.LeftShift: + case ExpressionType.ExclusiveOr: + result = VisitBinary(expression as BinaryExpression); + break; + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.ArrayLength: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + result = VisitUnary(expression as UnaryExpression); + break; + case ExpressionType.Parameter: + result = VisitParameter(expression as ParameterExpression); + break; + case ExpressionType.Call: + result = VisitMethodCall(expression as MethodCallExpression); + break; + case ExpressionType.New: + result = VisitNew(expression as NewExpression); + break; + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + result = VisitNewArray(expression as NewArrayExpression); + break; + default: + result = expression.ToString(); + break; } - protected abstract string VisitMemberAccess(MemberExpression? m); - - protected virtual string VisitLambda(LambdaExpression? lambda) + // if the expression is a CachedExpression, + // and is not already compiled, assign the result + if (cachedExpression == null) { - if (lambda?.Body.NodeType == ExpressionType.MemberAccess && - lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) - { - //This deals with members that are boolean (i.e. x => IsTrashed ) - var result = VisitMemberAccess(memberExpression); - - SqlParameters.Add(true); - - return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; - } - - return Visit(lambda?.Body); + return result; } - protected virtual string VisitBinary(BinaryExpression? b) + if (!cachedExpression.Visited) { - if (b is null) + cachedExpression.VisitResult = result; + } + + return cachedExpression.VisitResult; + } + + public virtual string GetQuotedTableName(string tableName) + => GetQuotedName(tableName); + + protected abstract string VisitMemberAccess(MemberExpression? m); + + protected virtual string VisitLambda(LambdaExpression? lambda) + { + if (lambda?.Body.NodeType == ExpressionType.MemberAccess && + lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null) + { + // This deals with members that are boolean (i.e. x => IsTrashed ) + var result = VisitMemberAccess(memberExpression); + + SqlParameters.Add(true); + + return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}"; + } + + return Visit(lambda?.Body); + } + + protected virtual string VisitBinary(BinaryExpression? b) + { + if (b is null) + { + return string.Empty; + } + + var left = string.Empty; + var right = string.Empty; + + var operand = BindOperant(b.NodeType); + if (operand == "AND" || operand == "OR") + { + if (b.Left is MemberExpression mLeft && mLeft.Expression != null) { - return string.Empty; - } - var left = string.Empty; - var right = string.Empty; + var r = VisitMemberAccess(mLeft); - var operand = BindOperant(b.NodeType); - if (operand == "AND" || operand == "OR") - { - if (b.Left is MemberExpression mLeft && mLeft.Expression != null) + SqlParameters.Add(true); + + if (Visited == false) { - var r = VisitMemberAccess(mLeft); - - SqlParameters.Add(true); - - if (Visited == false) - left = $"{r} = @{SqlParameters.Count - 1}"; + left = $"{r} = @{SqlParameters.Count - 1}"; } - else - { - left = Visit(b.Left); - } - if (b.Right is MemberExpression mRight && mRight.Expression != null) - { - var r = VisitMemberAccess(mRight); - - SqlParameters.Add(true); - - if (Visited == false) - right = $"{r} = @{SqlParameters.Count - 1}"; - } - else - { - right = Visit(b.Right); - } - } - else if (operand == "=") - { - // deal with (x == true|false) - most common - if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) - return (bool) constRight.Value! ? VisitNotNot(b.Left) : VisitNot(b.Left); - right = Visit(b.Right); - - // deal with (true|false == x) - why not - if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) - return (bool) constLeft.Value! ? VisitNotNot(b.Right) : VisitNot(b.Right); - left = Visit(b.Left); - } - else if (operand == "<>") - { - // deal with (x != true|false) - most common - if (b.Right is ConstantExpression constRight && constRight.Type == typeof (bool)) - return (bool) constRight.Value! ? VisitNot(b.Left) : VisitNotNot(b.Left); - right = Visit(b.Right); - - // deal with (true|false != x) - why not - if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof (bool)) - return (bool) constLeft.Value! ? VisitNot(b.Right) : VisitNotNot(b.Right); - left = Visit(b.Left); } else { left = Visit(b.Left); + } + + if (b.Right is MemberExpression mRight && mRight.Expression != null) + { + var r = VisitMemberAccess(mRight); + + SqlParameters.Add(true); + + if (Visited == false) + { + right = $"{r} = @{SqlParameters.Count - 1}"; + } + } + else + { right = Visit(b.Right); } - - if (operand == "=" && right == "null") operand = "is"; - else if (operand == "<>" && right == "null") operand = "is not"; - else if (operand == "=" || operand == "<>") + } + else if (operand == "=") + { + // deal with (x == true|false) - most common + if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) { - //if (IsTrueExpression(right)) right = GetQuotedTrueValue(); - //else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); - - //if (IsTrueExpression(left)) left = GetQuotedTrueValue(); - //else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); - + return (bool)constRight.Value! ? VisitNotNot(b.Left) : VisitNot(b.Left); } - switch (operand) - { - case "MOD": - case "COALESCE": - return Visited ? string.Empty : $"{operand}({left},{right})"; + right = Visit(b.Right); - default: - return Visited ? string.Empty : $"({left} {operand} {right})"; + // deal with (true|false == x) - why not + if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) + { + return (bool)constLeft.Value! ? VisitNotNot(b.Right) : VisitNot(b.Right); } + + left = Visit(b.Left); + } + else if (operand == "<>") + { + // deal with (x != true|false) - most common + if (b.Right is ConstantExpression constRight && constRight.Type == typeof(bool)) + { + return (bool)constRight.Value! ? VisitNot(b.Left) : VisitNotNot(b.Left); + } + + right = Visit(b.Right); + + // deal with (true|false != x) - why not + if (b.Left is ConstantExpression constLeft && constLeft.Type == typeof(bool)) + { + return (bool)constLeft.Value! ? VisitNot(b.Right) : VisitNotNot(b.Right); + } + + left = Visit(b.Left); + } + else + { + left = Visit(b.Left); + right = Visit(b.Right); } - protected virtual List VisitExpressionList(ReadOnlyCollection? original) + if (operand == "=" && right == "null") + { + operand = "is"; + } + else if (operand == "<>" && right == "null") + { + operand = "is not"; + } + else if (operand == "=" || operand == "<>") + { + // if (IsTrueExpression(right)) right = GetQuotedTrueValue(); + // else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); + + // if (IsTrueExpression(left)) left = GetQuotedTrueValue(); + // else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); + } + + switch (operand) + { + case "MOD": + case "COALESCE": + return Visited ? string.Empty : $"{operand}({left},{right})"; + + default: + return Visited ? string.Empty : $"({left} {operand} {right})"; + } + } + + protected virtual List VisitExpressionList(ReadOnlyCollection? original) + { + var list = new List(); + if (original is null) { - var list = new List(); - if (original is null) - { - return list; - } - for (int i = 0, n = original.Count; i < n; i++) - { - if (original[i].NodeType == ExpressionType.NewArrayInit || - original[i].NodeType == ExpressionType.NewArrayBounds) - { - list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); - } - else - { - list.Add(Visit(original[i])); - } - } return list; } - protected virtual string VisitNew(NewExpression? newExpression) + for (int i = 0, n = original.Count; i < n; i++) { - if (newExpression is null) + if (original[i].NodeType == ExpressionType.NewArrayInit || + original[i].NodeType == ExpressionType.NewArrayBounds) { - return string.Empty; + list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); } - // TODO: check ! - var member = Expression.Convert(newExpression, typeof(object)); - var lambda = Expression.Lambda>(member); - try + else { - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; - } - catch (InvalidOperationException) - { - if (Visited) - return string.Empty; - - var exprs = VisitExpressionList(newExpression.Arguments); - return string.Join(",", exprs); + list.Add(Visit(original[i])); } } - protected virtual string VisitParameter(ParameterExpression? p) + return list; + } + + protected virtual string VisitNew(NewExpression? newExpression) + { + if (newExpression is null) { - return p?.Name ?? string.Empty; + return string.Empty; } - protected virtual string VisitConstant(ConstantExpression? c) + // TODO: check ! + UnaryExpression member = Expression.Convert(newExpression, typeof(object)); + var lambda = Expression.Lambda>(member); + try { - if (c?.Value == null) - return "null"; + Func getter = lambda.Compile(); + var o = getter(); - SqlParameters.Add(c.Value); + SqlParameters.Add(o); return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; } - - protected virtual string VisitUnary(UnaryExpression? u) + catch (InvalidOperationException) { - switch (u?.NodeType) - { - case ExpressionType.Not: - return VisitNot(u.Operand); - default: - return Visit(u?.Operand); - } - } - - private string VisitNot(Expression exp) - { - var o = Visit(exp); - - // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers - // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") - - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // false property , i.e. x => !Trashed - // 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, ")"); - } - } - - private string VisitNotNot(Expression exp) - { - var o = Visit(exp); - - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // true property, i.e. x => Trashed - SqlParameters.Add(true); - return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; - default: - // could be anything else, such as: x => x.Path.StartsWith("-20") - return Visited ? string.Empty : o; - } - } - - protected virtual string VisitNewArray(NewArrayExpression? na) - { - if (na is null) + if (Visited) { return string.Empty; } - var exprs = VisitExpressionList(na.Expressions); - return Visited ? string.Empty : string.Join(",", exprs); - } - protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression? na) - => VisitExpressionList(na?.Expressions); - - protected virtual string BindOperant(ExpressionType e) - { - switch (e) - { - case ExpressionType.Equal: - return "="; - case ExpressionType.NotEqual: - return "<>"; - case ExpressionType.GreaterThan: - return ">"; - case ExpressionType.GreaterThanOrEqual: - return ">="; - case ExpressionType.LessThan: - return "<"; - case ExpressionType.LessThanOrEqual: - return "<="; - case ExpressionType.AndAlso: - return "AND"; - case ExpressionType.OrElse: - return "OR"; - case ExpressionType.Add: - return "+"; - case ExpressionType.Subtract: - return "-"; - case ExpressionType.Multiply: - return "*"; - case ExpressionType.Divide: - return "/"; - case ExpressionType.Modulo: - return "MOD"; - case ExpressionType.Coalesce: - return "COALESCE"; - default: - return e.ToString(); - } - } - - protected virtual string VisitMethodCall(MethodCallExpression? m) - { - if (m is null) - { - return string.Empty; - } - // m.Object is the expression that represent the instance for instance method class, or null for static method calls - // m.Arguments is the collection of expressions that represent arguments of the called method - // m.MethodInfo is the method info for the method to be called - - // assume that static methods are extension methods (probably not ok) - // and then, the method object is its first argument - get "safe" object - var methodObject = m.Object ?? m.Arguments[0]; - var visitedMethodObject = Visit(methodObject); - // and then, "safe" arguments are what would come after the first arg - var methodArgs = m.Object == null - ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) - : m.Arguments; - - switch (m.Method.Name) - { - case "ToString": - SqlParameters.Add(methodObject.ToString()); - return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; - - case "ToUpper": - return Visited ? string.Empty : $"upper({visitedMethodObject})"; - - case "ToLower": - return Visited ? string.Empty : $"lower({visitedMethodObject})"; - - case "Contains": - // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains - // method, which would then need to become a SQL IN clause - but beware that string is - // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN - - var isCollectionContains = - ( - m.Object == null && // static (extension?) method - m.Arguments.Count == 2 && // with two args - m.Arguments[0].Type != typeof(string) && // but not for string - TypeHelper.IsTypeAssignableFrom(m.Arguments[0].Type) && // first arg being an enumerable - m.Arguments[1].NodeType == ExpressionType.MemberAccess // second arg being a member access - ) || - ( - m.Object != null && // instance method - TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable - m.Object.Type != typeof(string) && // but not for string - m.Arguments.Count == 1 && // with 1 arg - m.Arguments[0].NodeType == ExpressionType.MemberAccess // arg being a member access - ); - - if (isCollectionContains) - goto case "SqlIn"; - else - goto case "Contains**String"; - - case nameof(SqlExpressionExtensions.SqlWildcard): - case "StartsWith": - case "EndsWith": - case "Contains**String": // see "Contains" above - case "Equals": - case nameof(SqlExpressionExtensions.SqlStartsWith): - case nameof(SqlExpressionExtensions.SqlEndsWith): - case nameof(SqlExpressionExtensions.SqlContains): - case nameof(SqlExpressionExtensions.SqlEquals): - case nameof(StringExtensions.InvariantStartsWith): - case nameof(StringExtensions.InvariantEndsWith): - case nameof(StringExtensions.InvariantContains): - case nameof(StringExtensions.InvariantEquals): - - string compareValue; - - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - // if it's a field accessor, we could Visit(methodArgs[0]) and get [a].[b] - // but then, what if we want more, eg .StartsWith(node.Path + ',') ? => not - - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - compareValue = getter().ToString()!; - } - else - { - compareValue = methodArgs[0].ToString(); - } - - //default column type - var colType = TextColumnType.NVarchar; - - //then check if the col type argument has been passed to the current method (this will be the case for methods like - // SqlContains and other Sql methods) - if (methodArgs.Count > 1) - { - var colTypeArg = methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); - if (colTypeArg != null) - { - colType = (TextColumnType)((ConstantExpression)colTypeArg).Value!; - } - } - - return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); - - case "Replace": - string searchValue; - - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - searchValue = getter().ToString()!; - } - else - { - searchValue = methodArgs[0].ToString(); - } - - if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - - string replaceValue; - - if (methodArgs[1].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[1], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - replaceValue = getter().ToString()!; - } - else - { - replaceValue = methodArgs[1].ToString(); - } - - if (methodArgs[1].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - - SqlParameters.Add(RemoveQuote(searchValue)!); - SqlParameters.Add(RemoveQuote(replaceValue)!); - - //don't execute if compiled - return Visited ? string.Empty : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; - - //case "Substring": - // var startIndex = Int32.Parse(args[0].ToString()) + 1; - // if (args.Count == 2) - // { - // var length = Int32.Parse(args[1].ToString()); - // return string.Format("substring({0} from {1} for {2})", - // r, - // startIndex, - // length); - // } - // else - // return string.Format("substring({0} from {1})", - // r, - // startIndex); - //case "Round": - //case "Floor": - //case "Ceiling": - //case "Coalesce": - //case "Abs": - //case "Sum": - // return string.Format("{0}({1}{2})", - // m.Method.Name, - // r, - // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); - //case "Concat": - // var s = new StringBuilder(); - // foreach (Object e in args) - // { - // s.AppendFormat(" || {0}", e); - // } - // return string.Format("{0}{1}", r, s); - - case "SqlIn": - - if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) - throw new NotSupportedException("SqlIn must contain the member being accessed."); - - var memberAccess = VisitMemberAccess((MemberExpression) methodArgs[0]); - - var inMember = Expression.Convert(methodObject, typeof(object)); - var inLambda = Expression.Lambda>(inMember); - var inGetter = inLambda.Compile(); - - var inArgs = (IEnumerable) inGetter(); - - var inBuilder = new StringBuilder(); - var inFirst = true; - - inBuilder.Append(memberAccess); - inBuilder.Append(" IN ("); - - foreach (var e in inArgs) - { - SqlParameters.Add(e); - if (inFirst) inFirst = false; else inBuilder.Append(","); - inBuilder.Append("@"); - inBuilder.Append(SqlParameters.Count - 1); - } - - inBuilder.Append(")"); - return inBuilder.ToString(); - - //case "Desc": - // return string.Format("{0} DESC", r); - //case "Alias": - //case "As": - // return string.Format("{0} As {1}", r, - // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); - - case "SqlText": - if (m.Method.DeclaringType != typeof(SqlExtensionsStatics)) - goto default; - if (m.Arguments.Count == 2) - { - var n1 = Visit(m.Arguments[0]); - var f = m.Arguments[1]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1); - } - else if (m.Arguments.Count == 3) - { - var n1 = Visit(m.Arguments[0]); - var n2 = Visit(m.Arguments[1]); - var f = m.Arguments[2]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1, n2); - } - else if (m.Arguments.Count == 4) - { - var n1 = Visit(m.Arguments[0]); - var n2 = Visit(m.Arguments[1]); - var n3 = Visit(m.Arguments[3]); - var f = m.Arguments[3]; - if (!(f is Expression> fl)) - throw new NotSupportedException("Expression is not a proper lambda."); - var ff = fl.Compile(); - return ff(n1, n2, n3); - } - else - throw new NotSupportedException("Expression is not a proper lambda."); - - // c# 'x == null' becomes sql 'x IS NULL' which is fine - // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, - // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, - // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback - // value which will be used when values are null - turning the comparison into - // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside - // of x and y range - and if that is not possible, then a manual comparison need - // to be written - // TODO: support SqlNullableEquals with 0 parameters, using the full syntax below - case "SqlNullableEquals": - var compareTo = Visit(m.Arguments[1]); - var fallback = Visit(m.Arguments[2]); - // that would work without a fallback value but is more cumbersome - //return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; - // use a fallback value - return Visited ? string.Empty : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; - - default: - - throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); - - //var s2 = new StringBuilder(); - //foreach (Object e in args) - //{ - // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); - //} - //return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); - } - } - - public virtual string GetQuotedTableName(string tableName) - => GetQuotedName(tableName); - - public virtual string GetQuotedColumnName(string columnName) - => GetQuotedName(columnName); - - public virtual string GetQuotedName(string name) - => Visited ? name : "\"" + name + "\""; - - protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) - { - switch (verb) - { - case nameof(SqlExpressionExtensions.SqlWildcard): - SqlParameters.Add(RemoveQuote(val)!); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "Equals": - case nameof(StringExtensions.InvariantEquals): - case nameof(SqlExpressionExtensions.SqlEquals): - SqlParameters.Add(RemoveQuote(val)!); - return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); - - case "StartsWith": - case nameof(StringExtensions.InvariantStartsWith): - case nameof(SqlExpressionExtensions.SqlStartsWith): - SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "EndsWith": - case nameof(StringExtensions.InvariantEndsWith): - case nameof(SqlExpressionExtensions.SqlEndsWith): - SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - case "Contains": - case nameof(StringExtensions.InvariantContains): - case nameof(SqlExpressionExtensions.SqlContains): - var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); - SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); - return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - - default: - throw new ArgumentOutOfRangeException(nameof(verb)); - } - } - - public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) - { - return paramValue == null - ? string.Empty - : sqlSyntax.EscapeString(paramValue.ToString()!); - } - - protected virtual string? RemoveQuote(string? exp) - { - if (exp.IsNullOrWhiteSpace()) return exp; - - var c = exp![0]; - return (c == '"' || c == '`' || c == '\'') && exp[exp.Length - 1] == c - ? exp.Length == 1 - ? string.Empty - : exp.Substring(1, exp.Length - 2) - : exp; + List exprs = VisitExpressionList(newExpression.Arguments); + return string.Join(",", exprs); } } + + protected virtual string VisitParameter(ParameterExpression? p) => p?.Name ?? string.Empty; + + protected virtual string VisitConstant(ConstantExpression? c) + { + if (c?.Value == null) + { + return "null"; + } + + SqlParameters.Add(c.Value); + + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + } + + protected virtual string VisitUnary(UnaryExpression? u) + { + switch (u?.NodeType) + { + case ExpressionType.Not: + return VisitNot(u.Operand); + default: + return Visit(u?.Operand); + } + } + + protected virtual string VisitNewArray(NewArrayExpression? na) + { + if (na is null) + { + return string.Empty; + } + + List exprs = VisitExpressionList(na.Expressions); + return Visited ? string.Empty : string.Join(",", exprs); + } + + private string VisitNot(Expression exp) + { + var o = Visit(exp); + + // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers + // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // false property , i.e. x => !Trashed + // 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, ")"); + } + } + + private string VisitNotNot(Expression exp) + { + var o = Visit(exp); + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // true property, i.e. x => Trashed + SqlParameters.Add(true); + return Visited ? string.Empty : $"({o} = @{SqlParameters.Count - 1})"; + default: + // could be anything else, such as: x => x.Path.StartsWith("-20") + return Visited ? string.Empty : o; + } + } + + protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression? na) + => VisitExpressionList(na?.Expressions); + + protected virtual string BindOperant(ExpressionType e) + { + switch (e) + { + case ExpressionType.Equal: + return "="; + case ExpressionType.NotEqual: + return "<>"; + case ExpressionType.GreaterThan: + return ">"; + case ExpressionType.GreaterThanOrEqual: + return ">="; + case ExpressionType.LessThan: + return "<"; + case ExpressionType.LessThanOrEqual: + return "<="; + case ExpressionType.AndAlso: + return "AND"; + case ExpressionType.OrElse: + return "OR"; + case ExpressionType.Add: + return "+"; + case ExpressionType.Subtract: + return "-"; + case ExpressionType.Multiply: + return "*"; + case ExpressionType.Divide: + return "/"; + case ExpressionType.Modulo: + return "MOD"; + case ExpressionType.Coalesce: + return "COALESCE"; + default: + return e.ToString(); + } + } + + protected virtual string VisitMethodCall(MethodCallExpression? m) + { + if (m is null) + { + return string.Empty; + } + + // m.Object is the expression that represent the instance for instance method class, or null for static method calls + // m.Arguments is the collection of expressions that represent arguments of the called method + // m.MethodInfo is the method info for the method to be called + + // assume that static methods are extension methods (probably not ok) + // and then, the method object is its first argument - get "safe" object + Expression methodObject = m.Object ?? m.Arguments[0]; + var visitedMethodObject = Visit(methodObject); + + // and then, "safe" arguments are what would come after the first arg + ReadOnlyCollection methodArgs = m.Object == null + ? new ReadOnlyCollection(m.Arguments.Skip(1).ToList()) + : m.Arguments; + + switch (m.Method.Name) + { + case "ToString": + SqlParameters.Add(methodObject.ToString()); + return Visited ? string.Empty : $"@{SqlParameters.Count - 1}"; + + case "ToUpper": + return Visited ? string.Empty : $"upper({visitedMethodObject})"; + + case "ToLower": + return Visited ? string.Empty : $"lower({visitedMethodObject})"; + + case "Contains": + // for 'Contains', it can either be the string.Contains(string) method, or a collection Contains + // method, which would then need to become a SQL IN clause - but beware that string is + // an enumerable of char, and string.Contains(char) is an extension method - but NOT an SQL IN + var isCollectionContains = + ( + m.Object == null && // static (extension?) method + m.Arguments.Count == 2 && // with two args + m.Arguments[0].Type != typeof(string) && // but not for string + TypeHelper.IsTypeAssignableFrom(m.Arguments[0] + .Type) && // first arg being an enumerable + m.Arguments[1].NodeType == ExpressionType.MemberAccess) // second arg being a member access + || + ( + m.Object != null && // instance method + TypeHelper.IsTypeAssignableFrom(m.Object.Type) && // of an enumerable + m.Object.Type != typeof(string) && // but not for string + m.Arguments.Count == 1 && // with 1 arg + m.Arguments[0].NodeType == ExpressionType.MemberAccess); // arg being a member access + + if (isCollectionContains) + { + goto case "SqlIn"; + } + + goto case "Contains**String"; + + case nameof(SqlExpressionExtensions.SqlWildcard): + case "StartsWith": + case "EndsWith": + case "Contains**String": // see "Contains" above + case "Equals": + case nameof(SqlExpressionExtensions.SqlStartsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + case nameof(SqlExpressionExtensions.SqlContains): + case nameof(SqlExpressionExtensions.SqlEquals): + case nameof(StringExtensions.InvariantStartsWith): + case nameof(StringExtensions.InvariantEndsWith): + case nameof(StringExtensions.InvariantContains): + case nameof(StringExtensions.InvariantEquals): + + string compareValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + // if it's a field accessor, we could Visit(methodArgs[0]) and get [a].[b] + // but then, what if we want more, eg .StartsWith(node.Path + ',') ? => not + + // This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + compareValue = getter().ToString()!; + } + else + { + compareValue = methodArgs[0].ToString(); + } + + // default column type + TextColumnType colType = TextColumnType.NVarchar; + + // then check if the col type argument has been passed to the current method (this will be the case for methods like + // SqlContains and other Sql methods) + if (methodArgs.Count > 1) + { + Expression? colTypeArg = + methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); + if (colTypeArg != null) + { + colType = (TextColumnType)((ConstantExpression)colTypeArg).Value!; + } + } + + return HandleStringComparison(visitedMethodObject, compareValue, m.Method.Name, colType); + + case "Replace": + string searchValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + // This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + searchValue = getter().ToString()!; + } + else + { + searchValue = methodArgs[0].ToString(); + } + + if (methodArgs[0].Type != typeof(string) && + TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + string replaceValue; + + if (methodArgs[1].NodeType != ExpressionType.Constant) + { + // This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + UnaryExpression member = Expression.Convert(methodArgs[1], typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + replaceValue = getter().ToString()!; + } + else + { + replaceValue = methodArgs[1].ToString(); + } + + if (methodArgs[1].Type != typeof(string) && + TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + SqlParameters.Add(RemoveQuote(searchValue)!); + SqlParameters.Add(RemoveQuote(replaceValue)!); + + // don't execute if compiled + return Visited + ? string.Empty + : $"replace({visitedMethodObject}, @{SqlParameters.Count - 2}, @{SqlParameters.Count - 1})"; + + // case "Substring": + // var startIndex = Int32.Parse(args[0].ToString()) + 1; + // if (args.Count == 2) + // { + // var length = Int32.Parse(args[1].ToString()); + // return string.Format("substring({0} from {1} for {2})", + // r, + // startIndex, + // length); + // } + // else + // return string.Format("substring({0} from {1})", + // r, + // startIndex); + // case "Round": + // case "Floor": + // case "Ceiling": + // case "Coalesce": + // case "Abs": + // case "Sum": + // return string.Format("{0}({1}{2})", + // m.Method.Name, + // r, + // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); + // case "Concat": + // var s = new StringBuilder(); + // foreach (Object e in args) + // { + // s.AppendFormat(" || {0}", e); + // } + // return string.Format("{0}{1}", r, s); + case "SqlIn": + + if (methodArgs.Count != 1 || methodArgs[0].NodeType != ExpressionType.MemberAccess) + { + throw new NotSupportedException("SqlIn must contain the member being accessed."); + } + + var memberAccess = VisitMemberAccess((MemberExpression)methodArgs[0]); + + UnaryExpression inMember = Expression.Convert(methodObject, typeof(object)); + var inLambda = Expression.Lambda>(inMember); + Func inGetter = inLambda.Compile(); + + var inArgs = (IEnumerable)inGetter(); + + var inBuilder = new StringBuilder(); + var inFirst = true; + + inBuilder.Append(memberAccess); + inBuilder.Append(" IN ("); + + foreach (var e in inArgs) + { + SqlParameters.Add(e); + if (inFirst) + { + inFirst = false; + } + else + { + inBuilder.Append(","); + } + + inBuilder.Append("@"); + inBuilder.Append(SqlParameters.Count - 1); + } + + inBuilder.Append(")"); + return inBuilder.ToString(); + + // case "Desc": + // return string.Format("{0} DESC", r); + // case "Alias": + // case "As": + // return string.Format("{0} As {1}", r, + // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); + case "SqlText": + if (m.Method.DeclaringType != typeof(SqlExtensionsStatics)) + { + goto default; + } + + if (m.Arguments.Count == 2) + { + var n1 = Visit(m.Arguments[0]); + Expression f = m.Arguments[1]; + if (!(f is Expression> fl)) + { + throw new NotSupportedException("Expression is not a proper lambda."); + } + + Func ff = fl.Compile(); + return ff(n1); + } + + if (m.Arguments.Count == 3) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + Expression f = m.Arguments[2]; + if (!(f is Expression> fl)) + { + throw new NotSupportedException("Expression is not a proper lambda."); + } + + Func ff = fl.Compile(); + return ff(n1, n2); + } + + if (m.Arguments.Count == 4) + { + var n1 = Visit(m.Arguments[0]); + var n2 = Visit(m.Arguments[1]); + var n3 = Visit(m.Arguments[3]); + Expression f = m.Arguments[3]; + if (!(f is Expression> fl)) + { + throw new NotSupportedException("Expression is not a proper lambda."); + } + + Func ff = fl.Compile(); + return ff(n1, n2, n3); + } + + throw new NotSupportedException("Expression is not a proper lambda."); + + // c# 'x == null' becomes sql 'x IS NULL' which is fine + // c# 'x == y' becomes sql 'x = @0' which is fine - unless they are nullable types, + // because sql 'x = NULL' is always false and the 'IS NULL' syntax is required, + // so for comparing nullable types, we use x.SqlNullableEquals(y, fb) where fb is a fallback + // value which will be used when values are null - turning the comparison into + // sql 'COALESCE(x,fb) = COALESCE(y,fb)' - of course, fb must be a value outside + // of x and y range - and if that is not possible, then a manual comparison need + // to be written + // TODO: support SqlNullableEquals with 0 parameters, using the full syntax below + case "SqlNullableEquals": + var compareTo = Visit(m.Arguments[1]); + var fallback = Visit(m.Arguments[2]); + + // that would work without a fallback value but is more cumbersome + // return Visited ? string.Empty : $"((({compareTo} is null) AND ({visitedMethodObject} is null)) OR (({compareTo} is not null) AND ({visitedMethodObject} = {compareTo})))"; + // use a fallback value + return Visited + ? string.Empty + : $"(COALESCE({visitedMethodObject},{fallback}) = COALESCE({compareTo},{fallback}))"; + + default: + + throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); + + // var s2 = new StringBuilder(); + // foreach (Object e in args) + // { + // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); + // } + // return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); + } + } + + public virtual string GetQuotedColumnName(string columnName) + => GetQuotedName(columnName); + + public virtual string GetQuotedName(string name) + => Visited ? name : "\"" + name + "\""; + + public virtual string EscapeParam(object paramValue, ISqlSyntaxProvider sqlSyntax) => paramValue == null ? string.Empty : sqlSyntax.EscapeString(paramValue.ToString()!); + + protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) + { + switch (verb) + { + case nameof(SqlExpressionExtensions.SqlWildcard): + SqlParameters.Add(RemoveQuote(val)!); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Equals": + case nameof(StringExtensions.InvariantEquals): + case nameof(SqlExpressionExtensions.SqlEquals): + SqlParameters.Add(RemoveQuote(val)!); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); + + case "StartsWith": + case nameof(StringExtensions.InvariantStartsWith): + case nameof(SqlExpressionExtensions.SqlStartsWith): + SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "EndsWith": + case nameof(StringExtensions.InvariantEndsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + case "Contains": + case nameof(StringExtensions.InvariantContains): + case nameof(SqlExpressionExtensions.SqlContains): + var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); + SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); + return Visited + ? string.Empty + : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + + default: + throw new ArgumentOutOfRangeException(nameof(verb)); + } + } + + protected virtual string? RemoveQuote(string? exp) + { + if (exp.IsNullOrWhiteSpace()) + { + return exp; + } + + var c = exp![0]; + return (c == '"' || c == '`' || c == '\'') && exp[^1] == c + ? exp.Length == 1 + ? string.Empty + : exp.Substring(1, exp.Length - 2) + : exp; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs index afe00a7fe9..4c1044a411 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -1,140 +1,152 @@ -using System; using System.Linq.Expressions; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, +/// based on Umbraco's business logic models. +/// +/// This object is stateful and cannot be re-used to parse an expression. +internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase { - /// - /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, - /// based on Umbraco's business logic models. - /// - /// This object is stateful and cannot be re-used to parse an expression. - internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase + private readonly BaseMapper? _mapper; + private readonly IMapperCollection? _mappers; + + public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, IMapperCollection? mappers) + : base(sqlSyntax) { - private readonly IMapperCollection? _mappers; - private readonly BaseMapper? _mapper; + _mappers = mappers; + _mapper = mappers?[typeof(T)]; // throws if not found + } - public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, IMapperCollection? mappers) - : base(sqlSyntax) + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) { - _mappers = mappers; - _mapper = mappers?[typeof(T)]; // throws if not found - } - - protected override string VisitMemberAccess(MemberExpression? m) - { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null && - m.Expression.NodeType == ExpressionType.Parameter - && m.Expression.Type == typeof(T)) - { - //don't execute if compiled - if (Visited == false) - { - var field = _mapper?.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); - return field!; - } - - //already compiled, return - return string.Empty; - } - - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) - { - //don't execute if compiled - if (Visited == false) - { - var field = _mapper?.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); - return field!; - } - - //already compiled, return - return string.Empty; - } - - if (m.Expression != null - && m.Expression.Type != typeof(T) - && EndsWithConstant(m) == false - && _mappers is not null - && _mappers.TryGetMapper(m.Expression.Type, out var subMapper)) - { - //if this is the case, it means we have a sub expression / nested property access, such as: x.ContentType.Alias == "Test"; - //and since the sub type (x.ContentType) is not the same as x, we need to resolve a mapper for x.ContentType to get it's mapped SQL column - - //don't execute if compiled - if (Visited == false) - { - var field = subMapper.Map(m.Member.Name); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}"); - return field; - } - //already compiled, return - return string.Empty; - } - - // TODO: When m.Expression.NodeType == ExpressionType.Constant and it's an expression like: content => aliases.Contains(content.ContentType.Alias); - // then an SQL parameter will be added for aliases as an array, however in SqlIn on the subclass it will manually add these SqlParameters anyways, - // however the query will still execute because the SQL that is written will only contain the correct indexes of SQL parameters, this would be ignored, - // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've - // only just discovered what it is actually doing. - - // TODO - // in most cases we want to convert the value to a plain object, - // but for in some rare cases, we may want to do it differently, - // for instance a Models.AuditType (an enum) may in some cases - // need to be converted to its string value. - // but - we cannot have specific code here, really - and how would - // we configure this? is it even possible? - /* - var toString = typeof(object).GetMethod("ToString"); - var member = Expression.Call(m, toString); - */ - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - //don't execute if compiled - if (Visited == false) - return $"@{SqlParameters.Count - 1}"; - - //already compiled, return return string.Empty; } - /// - /// Determines if the MemberExpression ends in a Constant value - /// - /// - /// - private static bool EndsWithConstant(MemberExpression m) + if (m.Expression != null && + m.Expression.NodeType == ExpressionType.Parameter + && m.Expression.Type == typeof(T)) { - Expression? expr = m; - - while (expr is MemberExpression) + // don't execute if compiled + if (Visited == false) { - var memberExpr = expr as MemberExpression; - if (memberExpr is not null) + var field = _mapper?.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) { - expr = memberExpr.Expression; + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); } + return field!; } - var constExpr = expr as ConstantExpression; - return constExpr != null; + // already compiled, return + return string.Empty; } + + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) + { + // don't execute if compiled + if (Visited == false) + { + var field = _mapper?.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}."); + } + + return field!; + } + + // already compiled, return + return string.Empty; + } + + if (m.Expression != null + && m.Expression.Type != typeof(T) + && EndsWithConstant(m) == false + && _mappers is not null + && _mappers.TryGetMapper(m.Expression.Type, out BaseMapper? subMapper)) + { + // if this is the case, it means we have a sub expression / nested property access, such as: x.ContentType.Alias == "Test"; + // and since the sub type (x.ContentType) is not the same as x, we need to resolve a mapper for x.ContentType to get it's mapped SQL column + + // don't execute if compiled + if (Visited == false) + { + var field = subMapper.Map(m.Member.Name); + if (field.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException( + $"The mapper returned an empty field for the member name: {m.Member.Name} for type: {m.Expression.Type}"); + } + + return field; + } + + // already compiled, return + return string.Empty; + } + + // TODO: When m.Expression.NodeType == ExpressionType.Constant and it's an expression like: content => aliases.Contains(content.ContentType.Alias); + // then an SQL parameter will be added for aliases as an array, however in SqlIn on the subclass it will manually add these SqlParameters anyways, + // however the query will still execute because the SQL that is written will only contain the correct indexes of SQL parameters, this would be ignored, + // I'm just unsure right now due to time constraints how to make it correct. It won't matter right now and has been working already with this bug but I've + // only just discovered what it is actually doing. + + // TODO + // in most cases we want to convert the value to a plain object, + // but for in some rare cases, we may want to do it differently, + // for instance a Models.AuditType (an enum) may in some cases + // need to be converted to its string value. + // but - we cannot have specific code here, really - and how would + // we configure this? is it even possible? + /* + var toString = typeof(object).GetMethod("ToString"); + var member = Expression.Call(m, toString); + */ + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + // don't execute if compiled + if (Visited == false) + { + return $"@{SqlParameters.Count - 1}"; + } + + // already compiled, return + return string.Empty; + } + + /// + /// Determines if the MemberExpression ends in a Constant value + /// + /// + /// + private static bool EndsWithConstant(MemberExpression m) + { + Expression? expr = m; + + while (expr is MemberExpression) + { + var memberExpr = expr as MemberExpression; + if (memberExpr is not null) + { + expr = memberExpr.Expression; + } + } + + return expr is ConstantExpression constExpr; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs index 87d758ebda..f7aba72bf0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -1,283 +1,321 @@ -using System; -using System.Linq; using System.Linq.Expressions; +using System.Reflection; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of the DTO. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase { - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of the DTO. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + private readonly string? _alias; + private readonly PocoData _pd; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias) + : base(sqlContext.SqlSyntax) { - private readonly PocoData _pd; - private readonly string? _alias; - - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias) - : base(sqlContext.SqlSyntax) - { - _pd = sqlContext.PocoDataFactory.ForType(typeof(TDto)); - _alias = alias; - } - - protected override string VisitMethodCall(MethodCallExpression? m) - { - if (m is null) - { - return string.Empty; - } - var declaring = m.Method.DeclaringType; - if (declaring != typeof (SqlTemplate)) - return base.VisitMethodCall(m); - - if (m.Method.Name != "Arg" && m.Method.Name != "ArgIn") - throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name} is not supported."); - - var parameters = m.Method.GetParameters(); - if (parameters.Length != 1 || parameters[0].ParameterType != typeof (string)) - throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name}({string.Join(", ", parameters.Select(x => x.ParameterType))} is not supported."); - - var arg = m.Arguments[0]; - string? name; - if (arg.NodeType == ExpressionType.Constant) - { - name = arg.ToString(); - } - else - { - // though... we probably should avoid doing this - var member = Expression.Convert(arg, typeof (object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - name = getter().ToString(); - } - - SqlParameters.Add(new SqlTemplate.TemplateArg(RemoveQuote(name))); - - return Visited - ? string.Empty - : $"@{SqlParameters.Count - 1}"; - } - - protected override string VisitMemberAccess(MemberExpression? m) - { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter && m.Expression.Type == typeof(TDto)) - { - return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); - } - - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) - { - return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); - } - - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); - } - - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); - - return tableName + "." + columnName; - } + _pd = sqlContext.PocoDataFactory.ForType(typeof(TDto)); + _alias = alias; } - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of DTO 1. - /// The type of DTO 2. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + protected override string VisitMethodCall(MethodCallExpression? m) { - private readonly PocoData _pocoData1, _pocoData2; - private readonly string? _alias1, _alias2; - private string? _parameterName1, _parameterName2; - - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2) - : base(sqlContext.SqlSyntax) + if (m is null) { - _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof (TDto1)); - _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof (TDto2)); - _alias1 = alias1; - _alias2 = alias2; + return string.Empty; } - protected override string VisitLambda(LambdaExpression? lambda) + Type? declaring = m.Method.DeclaringType; + if (declaring != typeof(SqlTemplate)) { - if (lambda is null) - { - return string.Empty; - } - if (lambda.Parameters.Count == 2) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - } - else - { - _parameterName1 = _parameterName2 = null; - } - return base.VisitLambda(lambda); + return base.VisitMethodCall(m); } - protected override string VisitMemberAccess(MemberExpression? m) + if (m.Method.Name != "Arg" && m.Method.Name != "ArgIn") { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null) - { - if (m.Expression.NodeType == ExpressionType.Parameter) - { - var pex = (ParameterExpression) m.Expression; + throw new NotSupportedException($"Method SqlTemplate.{m.Method.Name} is not supported."); + } - if (pex.Name == _parameterName1) - return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + ParameterInfo[] parameters = m.Method.GetParameters(); + if (parameters.Length != 1 || parameters[0].ParameterType != typeof(string)) + { + throw new NotSupportedException( + $"Method SqlTemplate.{m.Method.Name}({string.Join(", ", parameters.Select(x => x.ParameterType))} is not supported."); + } - if (pex.Name == _parameterName2) - return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); - } - else if (m.Expression.NodeType == ExpressionType.Convert) - { - // here: which _pd should we use?! - throw new NotSupportedException(); - //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); - } - } - - var member = Expression.Convert(m, typeof (object)); + Expression arg = m.Arguments[0]; + string? name; + if (arg.NodeType == ExpressionType.Constant) + { + name = arg.ToString(); + } + else + { + // though... we probably should avoid doing this + UnaryExpression member = Expression.Convert(arg, typeof(object)); var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - // execute if not already compiled - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + Func getter = lambda.Compile(); + name = getter().ToString(); } - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + SqlParameters.Add(new SqlTemplate.TemplateArg(RemoveQuote(name))); - return tableName + "." + columnName; - } + return Visited + ? string.Empty + : $"@{SqlParameters.Count - 1}"; } - /// - /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. - /// - /// The type of DTO 1. - /// The type of DTO 2. - /// The type of DTO 3. - /// This visitor is stateful and cannot be reused. - internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + protected override string VisitMemberAccess(MemberExpression? m) { - private readonly PocoData _pocoData1, _pocoData2, _pocoData3; - private readonly string? _alias1, _alias2, _alias3; - private string? _parameterName1, _parameterName2, _parameterName3; - - public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2, string? alias3) - : base(sqlContext.SqlSyntax) + if (m is null) { - _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); - _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); - _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3)); - _alias1 = alias1; - _alias2 = alias2; - _alias3 = alias3; + return string.Empty; } - protected override string VisitLambda(LambdaExpression? lambda) + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Parameter && + m.Expression.Type == typeof(TDto)) { - if (lambda is null) - { - return string.Empty; - } - if (lambda.Parameters.Count == 3) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - _parameterName3 = lambda.Parameters[2].Name; - } - else if (lambda.Parameters.Count == 2) - { - _parameterName1 = lambda.Parameters[0].Name; - _parameterName2 = lambda.Parameters[1].Name; - } - else - { - _parameterName1 = _parameterName2 = null; - } - return base.VisitLambda(lambda); + return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); } - protected override string VisitMemberAccess(MemberExpression? m) + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) { - if (m is null) - { - return string.Empty; - } - if (m.Expression != null) - { - if (m.Expression.NodeType == ExpressionType.Parameter) - { - var pex = (ParameterExpression)m.Expression; - - if (pex.Name == _parameterName1) - return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); - - if (pex.Name == _parameterName2) - return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); - - if (pex.Name == _parameterName3) - return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3); - } - else if (m.Expression.NodeType == ExpressionType.Convert) - { - // here: which _pd should we use?! - throw new NotSupportedException(); - //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); - } - } - - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - var o = getter(); - - SqlParameters.Add(o); - - // execute if not already compiled - return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name, _alias); } - protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); - var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); - var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); - return tableName + "." + columnName; - } + SqlParameters.Add(o); + + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } + + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + + return tableName + "." + columnName; + } +} + +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of DTO 1. +/// The type of DTO 2. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +{ + private readonly string? _alias1; + private readonly string? _alias2; + private readonly PocoData _pocoData1; + private readonly PocoData _pocoData2; + private string? _parameterName1; + private string? _parameterName2; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2) + : base(sqlContext.SqlSyntax) + { + _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); + _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); + _alias1 = alias1; + _alias2 = alias2; + } + + protected override string VisitLambda(LambdaExpression? lambda) + { + if (lambda is null) + { + return string.Empty; + } + + if (lambda.Parameters.Count == 2) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + } + else + { + _parameterName1 = _parameterName2 = null; + } + + return base.VisitLambda(lambda); + } + + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) + { + return string.Empty; + } + + if (m.Expression != null) + { + if (m.Expression.NodeType == ExpressionType.Parameter) + { + var pex = (ParameterExpression)m.Expression; + + if (pex.Name == _parameterName1) + { + return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + } + + if (pex.Name == _parameterName2) + { + return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); + } + } + else if (m.Expression.NodeType == ExpressionType.Convert) + { + // here: which _pd should we use?! + throw new NotSupportedException(); + + // return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + } + } + + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + // execute if not already compiled + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } + + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + + return tableName + "." + columnName; + } +} + +/// +/// Represents an expression tree parser used to turn strongly typed expressions into SQL statements. +/// +/// The type of DTO 1. +/// The type of DTO 2. +/// The type of DTO 3. +/// This visitor is stateful and cannot be reused. +internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase +{ + private readonly string? _alias1; + private readonly string? _alias2; + private readonly string? _alias3; + private readonly PocoData _pocoData1; + private readonly PocoData _pocoData2; + private readonly PocoData _pocoData3; + private string? _parameterName1; + private string? _parameterName2; + private string? _parameterName3; + + public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string? alias1, string? alias2, string? alias3) + : base(sqlContext.SqlSyntax) + { + _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1)); + _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2)); + _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3)); + _alias1 = alias1; + _alias2 = alias2; + _alias3 = alias3; + } + + protected override string VisitLambda(LambdaExpression? lambda) + { + if (lambda is null) + { + return string.Empty; + } + + if (lambda.Parameters.Count == 3) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + _parameterName3 = lambda.Parameters[2].Name; + } + else if (lambda.Parameters.Count == 2) + { + _parameterName1 = lambda.Parameters[0].Name; + _parameterName2 = lambda.Parameters[1].Name; + } + else + { + _parameterName1 = _parameterName2 = null; + } + + return base.VisitLambda(lambda); + } + + protected override string VisitMemberAccess(MemberExpression? m) + { + if (m is null) + { + return string.Empty; + } + + if (m.Expression != null) + { + if (m.Expression.NodeType == ExpressionType.Parameter) + { + var pex = (ParameterExpression)m.Expression; + + if (pex.Name == _parameterName1) + { + return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1); + } + + if (pex.Name == _parameterName2) + { + return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2); + } + + if (pex.Name == _parameterName3) + { + return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3); + } + } + else if (m.Expression.NodeType == ExpressionType.Convert) + { + // here: which _pd should we use?! + throw new NotSupportedException(); + + // return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name); + } + } + + UnaryExpression member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + Func getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + // execute if not already compiled + return Visited ? string.Empty : "@" + (SqlParameters.Count - 1); + } + + protected virtual string GetFieldName(PocoData pocoData, string name, string? alias) + { + KeyValuePair column = + pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name); + var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName); + var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName); + + return tableName + "." + columnName; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs index f4535f9734..88d1326f44 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/Query.cs @@ -1,100 +1,103 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Linq.Expressions; using System.Text; using NPoco; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying -{ - /// - /// Represents a query builder. - /// - /// A query builder translates Linq queries into Sql queries. - public class Query : IQuery - { - private readonly ISqlContext _sqlContext; - private readonly List> _wheres = new List>(); +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; - public Query(ISqlContext sqlContext) +/// +/// Represents a query builder. +/// +/// A query builder translates Linq queries into Sql queries. +public class Query : IQuery +{ + private readonly ISqlContext _sqlContext; + private readonly List> _wheres = new(); + + public Query(ISqlContext sqlContext) => _sqlContext = sqlContext; + + /// + /// Adds a where clause to the query. + /// + public virtual IQuery Where(Expression>? predicate) + { + if (predicate == null) { - _sqlContext = sqlContext; + return this; } - /// - /// Adds a where clause to the query. - /// - public virtual IQuery Where(Expression> predicate) - { - if (predicate == null) return this; + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(predicate); + _wheres.Add(new Tuple(whereExpression, expressionHelper.GetSqlParameters())); + return this; + } + /// + /// Adds a where-in clause to the query. + /// + public virtual IQuery WhereIn(Expression>? fieldSelector, IEnumerable? values) + { + if (fieldSelector == null) + { + return this; + } + + var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); + var whereExpression = expressionHelper.Visit(fieldSelector); + _wheres.Add(new Tuple(whereExpression + " IN (@values)", new object[] { new { values } })); + return this; + } + + /// + /// Adds a set of OR-ed where clauses to the query. + /// + public virtual IQuery WhereAny(IEnumerable>>? predicates) + { + if (predicates == null) + { + return this; + } + + StringBuilder? sb = null; + List? parameters = null; + Sql? sql = null; + foreach (Expression> predicate in predicates) + { + // see notes in Where() var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); var whereExpression = expressionHelper.Visit(predicate); - _wheres.Add(new Tuple(whereExpression, expressionHelper.GetSqlParameters())); - return this; - } - /// - /// Adds a where-in clause to the query. - /// - public virtual IQuery WhereIn(Expression> fieldSelector, IEnumerable? values) - { - if (fieldSelector == null) return this; - - var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); - var whereExpression = expressionHelper.Visit(fieldSelector); - _wheres.Add(new Tuple(whereExpression + " IN (@values)", new object[] { new { values } })); - return this; - } - - /// - /// Adds a set of OR-ed where clauses to the query. - /// - public virtual IQuery WhereAny(IEnumerable>> predicates) - { - if (predicates == null) return this; - - StringBuilder? sb = null; - List? parameters = null; - Sql? sql = null; - foreach (var predicate in predicates) + if (sb == null) { - // see notes in Where() - var expressionHelper = new ModelToSqlExpressionVisitor(_sqlContext.SqlSyntax, _sqlContext.Mappers); - var whereExpression = expressionHelper.Visit(predicate); - - if (sb == null) - { - sb = new StringBuilder("("); - parameters = new List(); - sql = Sql.BuilderFor(_sqlContext); - } - else - { - sb.Append(" OR "); - sql?.Append(" OR "); - } - - sb.Append(whereExpression); - parameters?.AddRange(expressionHelper.GetSqlParameters()); - sql?.Append(whereExpression, expressionHelper.GetSqlParameters()); + sb = new StringBuilder("("); + parameters = new List(); + sql = Sql.BuilderFor(_sqlContext); + } + else + { + sb.Append(" OR "); + sql?.Append(" OR "); } - if (sb == null) return this; - - sb.Append(")"); - _wheres.Add(Tuple.Create("(" + sql?.SQL + ")", sql?.Arguments)!); + sb.Append(whereExpression); + parameters?.AddRange(expressionHelper.GetSqlParameters()); + sql?.Append(whereExpression, expressionHelper.GetSqlParameters()); + } + if (sb == null) + { return this; } - /// - /// Returns all translated where clauses and their sql parameters - /// - public IEnumerable> GetWhereClauses() - { - return _wheres; - } + sb.Append(")"); + _wheres.Add(Tuple.Create("(" + sql?.SQL + ")", sql?.Arguments)!); + + return this; } + + /// + /// Returns all translated where clauses and their sql parameters + /// + public IEnumerable> GetWhereClauses() => _wheres; } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs index 6abb97a554..5f120194de 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/QueryExtensions.cs @@ -1,28 +1,26 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; + +/// +/// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} +/// there's not much we can do. +/// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. +/// We have to wait till v8 to make this change I suppose. +/// +internal static class QueryExtensions { /// - /// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} there's not much we can do. - /// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. - /// We have to wait till v8 to make this change I suppose. + /// Returns all translated where clauses and their sql parameters /// - internal static class QueryExtensions + /// + public static IEnumerable> GetWhereClauses(this IQuery query) { - /// - /// Returns all translated where clauses and their sql parameters - /// - /// - public static IEnumerable> GetWhereClauses(this IQuery query) + if (query is not Query q) { - var q = query as Query; - if (q == null) - { - throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); - } - return q.GetWhereClauses(); + throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); } + + return q.GetWhereClauses(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs b/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs index 85ccbd02ee..26a4c42bee 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/SqlTranslator.cs @@ -1,35 +1,29 @@ -using System; using NPoco; using Umbraco.Cms.Core.Persistence.Querying; -namespace Umbraco.Cms.Infrastructure.Persistence.Querying -{ - /// - /// Represents the Sql Translator for translating a IQuery object to Sql - /// - /// - public class SqlTranslator - { - private readonly Sql _sql; +namespace Umbraco.Cms.Infrastructure.Persistence.Querying; - public SqlTranslator(Sql sql, IQuery? query) +/// +/// Represents the Sql Translator for translating a IQuery object to Sql +/// +/// +public class SqlTranslator +{ + private readonly Sql _sql; + + public SqlTranslator(Sql sql, IQuery? query) + { + _sql = sql ?? throw new ArgumentNullException(nameof(sql)); + if (query is not null) { - _sql = sql ?? throw new ArgumentNullException(nameof(sql)); - if (query is not null) + foreach (Tuple clause in query.GetWhereClauses()) { - foreach (var clause in query.GetWhereClauses()) - _sql.Where(clause.Item1, clause.Item2); + _sql.Where(clause.Item1, clause.Item2); } } - - public Sql Translate() - { - return _sql; - } - - public override string ToString() - { - return _sql.SQL; - } } + + public Sql Translate() => _sql; + + public override string ToString() => _sql.SQL; } diff --git a/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs b/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs index 3162f58d1e..016bc89684 100644 --- a/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs +++ b/src/Umbraco.Infrastructure/Persistence/RecordPersistenceType.cs @@ -1,9 +1,8 @@ -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public enum RecordPersistenceType { - public enum RecordPersistenceType - { - Insert, - Update, - Delete - } + Insert, + Update, + Delete, } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs index ac07cd19dd..bafd00ee8e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IEntityRepositoryExtended.cs @@ -1,34 +1,31 @@ -using System; -using System.Collections.Generic; using NPoco; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories; + +public interface IEntityRepositoryExtended : IEntityRepository { - public interface IEntityRepositoryExtended : IEntityRepository - { - /// - /// Gets paged entities for a query and a subset of object types - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// A callback providing the ability to customize the generated SQL used to retrieve entities - /// - /// - /// A collection of mixed entity types which would be of type , , , - /// - /// - IEnumerable GetPagedResultsByQuery( - IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null); - } + /// + /// Gets paged entities for a query and a subset of object types + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// A callback providing the ability to customize the generated SQL used to retrieve entities + /// + /// + /// A collection of mixed entity types which would be of type , + /// , , + /// + /// + IEnumerable GetPagedResultsByQuery( + IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs index b94f99723a..eab408823e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditEntryRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -8,125 +5,123 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository { /// - /// Represents the NPoco implementation of . + /// Initializes a new instance of the class. /// - internal class AuditEntryRepository : EntityRepositoryBase, IAuditEntryRepository + public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - /// - /// Initializes a new instance of the class. - /// - public AuditEntryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - } + } - /// - public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + /// + public IEnumerable GetPage(long pageIndex, int pageCount, out long records) + { + Sql sql = Sql() + .Select() + .From() + .OrderByDescending(x => x.EventDateUtc); + + Page page = Database.Page(pageIndex + 1, pageCount, sql); + records = page.TotalItems; + return page.Items.Select(AuditEntryFactory.BuildEntity); + } + + /// + public bool IsAvailable() + { + var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); + return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); + } + + /// + protected override IAuditEntry? PerformGet(int id) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.Id == id); + + AuditEntryDto dto = Database.FirstOrDefault(sql); + return dto == null ? null : AuditEntryFactory.BuildEntity(dto); + } + + /// + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Length == 0) { Sql sql = Sql() .Select() - .From() - .OrderByDescending(x => x.EventDateUtc); + .From(); - Page page = Database.Page(pageIndex + 1, pageCount, sql); - records = page.TotalItems; - return page.Items.Select(AuditEntryFactory.BuildEntity); - } - - /// - public bool IsAvailable() - { - var tables = SqlSyntax.GetTablesInSchema(Database).ToArray(); - return tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry); - } - - /// - protected override IAuditEntry? PerformGet(int id) - { - Sql sql = Sql() - .Select() - .From() - .Where(x => x.Id == id); - - AuditEntryDto dto = Database.FirstOrDefault(sql); - return dto == null ? null : AuditEntryFactory.BuildEntity(dto); - } - - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - if (ids?.Length == 0) - { - Sql sql = Sql() - .Select() - .From(); - - return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); - } - - var entries = new List(); - - foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql() - .Select() - .From() - .WhereIn(x => x.Id, group); - - entries.AddRange(Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity)); - } - - return entries; - } - - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); } - /// - protected override Sql GetBaseQuery(bool isCount) + var entries = new List(); + + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) { - Sql sql = Sql(); - sql = isCount ? sql.SelectCount() : sql.Select(); - sql = sql.From(); - return sql; + Sql sql = Sql() + .Select() + .From() + .WhereIn(x => x.Id, group); + + entries.AddRange(Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity)); } - /// - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; - - /// - protected override IEnumerable GetDeleteClauses() => - throw new NotSupportedException("Audit entries cannot be deleted."); - - /// - protected override void PersistNewItem(IAuditEntry entity) - { - entity.AddingEntity(); - - AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); - Database.Insert(dto); - entity.Id = dto.Id; - entity.ResetDirtyProperties(); - } - - /// - protected override void PersistUpdatedItem(IAuditEntry entity) => - throw new NotSupportedException("Audit entries cannot be updated."); + return entries; } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(AuditEntryFactory.BuildEntity); + } + + /// + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } + + /// + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.AuditEntry}.id = @id"; + + /// + protected override IEnumerable GetDeleteClauses() => + throw new NotSupportedException("Audit entries cannot be deleted."); + + /// + protected override void PersistNewItem(IAuditEntry entity) + { + entity.AddingEntity(); + + AuditEntryDto dto = AuditEntryFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(IAuditEntry entity) => + throw new NotSupportedException("Audit entries cannot be updated."); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index cc5f08d58b..f11e17a236 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,173 +10,181 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class AuditRepository : EntityRepositoryBase, IAuditRepository { - internal class AuditRepository : EntityRepositoryBase, IAuditRepository + public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public AuditRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } - - protected override void PersistNewItem(IAuditItem entity) - { - Database.Insert(new LogDto - { - Comment = entity.Comment, - Datestamp = DateTime.Now, - Header = entity.AuditType.ToString(), - NodeId = entity.Id, - UserId = entity.UserId, - EntityType = entity.EntityType, - Parameters = entity.Parameters - }); - } - - protected override void PersistUpdatedItem(IAuditItem entity) - { - // inserting when updating because we never update a log entry, perhaps this should throw? - Database.Insert(new LogDto - { - Comment = entity.Comment, - Datestamp = DateTime.Now, - Header = entity.AuditType.ToString(), - NodeId = entity.Id, - UserId = entity.UserId, - EntityType = entity.EntityType, - Parameters = entity.Parameters - }); - } - - protected override IAuditItem? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - - var dto = Database.First(sql); - return dto == null - ? null - : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new NotImplementedException(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Cms.Core.Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); - } - - public IEnumerable Get(AuditType type, IQuery query) - { - var sqlClause = GetBaseQuery(false) - .Where("(logHeader=@0)", type.ToString()); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Cms.Core.Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From(); - - if (!isCount) - sql.LeftJoin().On((left, right) => left.UserId == right.Id); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return "id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - throw new NotImplementedException(); - } - - public void CleanLogs(int maximumAgeOfLogsInMinutes) - { - var oldestPermittedLogEntry = DateTime.Now.Subtract(new TimeSpan(0, maximumAgeOfLogsInMinutes, 0)); - - Database.Execute( - "delete from umbracoLog where datestamp < @oldestPermittedLogEntry and logHeader in ('open','system')", - new {oldestPermittedLogEntry = oldestPermittedLogEntry}); - } - - /// - /// Return the audit items as paged result - /// - /// - /// The query coming from the service - /// - /// - /// - /// - /// - /// - /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query or the custom filter - /// so we need to do that here - /// - /// - /// A user supplied custom filter - /// - /// - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, - out long totalRecords, Direction orderDirection, - AuditType[]? auditTypeFilter, - IQuery? customFilter) - { - if (auditTypeFilter == null) auditTypeFilter = Array.Empty(); - - var sql = GetBaseQuery(false); - - var translator = new SqlTranslator(sql, query ?? Query()); - sql = translator.Translate(); - - if (customFilter != null) - foreach (var filterClause in customFilter.GetWhereClauses()) - sql.Where(filterClause.Item1, filterClause.Item2); - - if (auditTypeFilter.Length > 0) - foreach (var type in auditTypeFilter) - sql.Where("(logHeader=@0)", type.ToString()); - - sql = orderDirection == Direction.Ascending - ? sql.OrderBy("Datestamp") - : sql.OrderByDescending("Datestamp"); - - // get page - var page = Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = page.TotalItems; - - var items = page.Items.Select( - dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); - - // map the DateStamp - for (var i = 0; i < items.Count; i++) - items[i].CreateDate = page.Items[i].Datestamp; - - return items; - } } + + public IEnumerable Get(AuditType type, IQuery query) + { + Sql? sqlClause = GetBaseQuery(false) + .Where("(logHeader=@0)", type.ToString()); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } + + public void CleanLogs(int maximumAgeOfLogsInMinutes) + { + DateTime oldestPermittedLogEntry = DateTime.Now.Subtract(new TimeSpan(0, maximumAgeOfLogsInMinutes, 0)); + + Database.Execute( + "delete from umbracoLog where datestamp < @oldestPermittedLogEntry and logHeader in ('open','system')", + new { oldestPermittedLogEntry }); + } + + /// + /// Return the audit items as paged result + /// + /// + /// The query coming from the service + /// + /// + /// + /// + /// + /// + /// Since we currently do not have enum support with our expression parser, we cannot query on AuditType in the query + /// or the custom filter + /// so we need to do that here + /// + /// + /// A user supplied custom filter + /// + /// + public IEnumerable GetPagedResultsByQuery( + IQuery query, + long pageIndex, + int pageSize, + out long totalRecords, + Direction orderDirection, + AuditType[]? auditTypeFilter, + IQuery? customFilter) + { + if (auditTypeFilter == null) + { + auditTypeFilter = Array.Empty(); + } + + Sql sql = GetBaseQuery(false); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + + if (customFilter != null) + { + foreach (Tuple filterClause in customFilter.GetWhereClauses()) + { + sql.Where(filterClause.Item1, filterClause.Item2); + } + } + + if (auditTypeFilter.Length > 0) + { + foreach (AuditType type in auditTypeFilter) + { + sql.Where("(logHeader=@0)", type.ToString()); + } + } + + sql = orderDirection == Direction.Ascending + ? sql.OrderBy("Datestamp") + : sql.OrderByDescending("Datestamp"); + + // get page + Page? page = Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = page.TotalItems; + + var items = page.Items.Select( + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); + + // map the DateStamp + for (var i = 0; i < items.Count; i++) + { + items[i].CreateDate = page.Items[i].Datestamp; + } + + return items; + } + + protected override void PersistNewItem(IAuditItem entity) => + Database.Insert(new LogDto + { + Comment = entity.Comment, + Datestamp = DateTime.Now, + Header = entity.AuditType.ToString(), + NodeId = entity.Id, + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters, + }); + + protected override void PersistUpdatedItem(IAuditItem entity) => + + // inserting when updating because we never update a log entry, perhaps this should throw? + Database.Insert(new LogDto + { + Comment = entity.Comment, + Datestamp = DateTime.Now, + Header = entity.AuditType.ToString(), + NodeId = entity.Id, + UserId = entity.UserId, + EntityType = entity.EntityType, + Parameters = entity.Parameters, + }); + + protected override IAuditItem? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + + LogDto? dto = Database.First(sql); + return dto == null + ? null + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotImplementedException(); + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters)).ToList(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + if (!isCount) + { + sql.LeftJoin().On((left, right) => left.UserId == right.Id); + } + + return sql; + } + + protected override string GetBaseWhereClause() => "id = @id"; + + protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs index 60492773b0..208d0928a3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CacheInstructionRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; @@ -9,65 +6,68 @@ using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class CacheInstructionRepository : ICacheInstructionRepository { - /// - /// Represents the NPoco implementation of . - /// - internal class CacheInstructionRepository : ICacheInstructionRepository + private readonly IScopeAccessor _scopeAccessor; + + public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + private IScope? AmbientScope => _scopeAccessor.AmbientScope; + + /// + public int CountAll() { - private readonly IScopeAccessor _scopeAccessor; + Sql? sql = AmbientScope?.SqlContext.Sql().Select("COUNT(*)") + .From(); - public CacheInstructionRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + return AmbientScope?.Database.ExecuteScalar(sql) ?? 0; + } - /// - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; + /// + public int CountPendingInstructions(int lastId) => + AmbientScope?.Database.ExecuteScalar( + "SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId }) ?? 0; - /// - public int CountAll() - { - Sql? sql = AmbientScope?.SqlContext.Sql().Select("COUNT(*)") - .From(); + /// + public int GetMaxId() => + AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction") ?? 0; - return AmbientScope?.Database.ExecuteScalar(sql) ?? 0; - } + /// + public bool Exists(int id) => AmbientScope?.Database.Exists(id) ?? false; - /// - public int CountPendingInstructions(int lastId) => - AmbientScope?.Database.ExecuteScalar("SELECT SUM(instructionCount) FROM umbracoCacheInstruction WHERE id > @lastId", new { lastId }) ?? 0; + /// + public void Add(CacheInstruction cacheInstruction) + { + CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction); + AmbientScope?.Database.Insert(dto); + } - /// - public int GetMaxId() => - AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction") ?? 0; + /// + public IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve) + { + Sql? sql = AmbientScope?.SqlContext.Sql().SelectAll() + .From() + .Where(dto => dto.Id > lastId) + .OrderBy(dto => dto.Id); + Sql? topSql = sql?.SelectTop(maxNumberToRetrieve); + return AmbientScope?.Database.Fetch(topSql).Select(CacheInstructionFactory.BuildEntity) ?? + Array.Empty(); + } - /// - public bool Exists(int id) => AmbientScope?.Database.Exists(id) ?? false; - - /// - public void Add(CacheInstruction cacheInstruction) - { - CacheInstructionDto dto = CacheInstructionFactory.BuildDto(cacheInstruction); - AmbientScope?.Database.Insert(dto); - } - - /// - public IEnumerable GetPendingInstructions(int lastId, int maxNumberToRetrieve) - { - Sql? sql = AmbientScope?.SqlContext.Sql().SelectAll() - .From() - .Where(dto => dto.Id > lastId) - .OrderBy(dto => dto.Id); - Sql? topSql = sql?.SelectTop(maxNumberToRetrieve); - return AmbientScope?.Database.Fetch(topSql).Select(CacheInstructionFactory.BuildEntity) ?? Array.Empty(); - } - - /// - public void DeleteInstructionsOlderThan(DateTime pruneDate) - { - // Using 2 queries is faster than convoluted joins. - var maxId = AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); - Sql deleteSql = new Sql().Append(@"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", new { pruneDate, maxId }); - AmbientScope?.Database.Execute(deleteSql); - } + /// + public void DeleteInstructionsOlderThan(DateTime pruneDate) + { + // Using 2 queries is faster than convoluted joins. + var maxId = AmbientScope?.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction;"); + Sql deleteSql = + new Sql().Append( + @"DELETE FROM umbracoCacheInstruction WHERE utcStamp < @pruneDate AND id < @maxId", + new { pruneDate, maxId }); + AmbientScope?.Database.Execute(deleteSql); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs index 057fb7e01c..74f3a419e5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ConsentRepository.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; @@ -12,89 +10,74 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the NPoco implementation of . +/// +internal class ConsentRepository : EntityRepositoryBase, IConsentRepository { /// - /// Represents the NPoco implementation of . + /// Initializes a new instance of the class. /// - internal class ConsentRepository : EntityRepositoryBase, IConsentRepository + public ConsentRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - /// - /// Initializes a new instance of the class. - /// - public ConsentRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - /// - protected override IConsent PerformGet(int id) - { - throw new NotSupportedException(); - } + /// + public void ClearCurrent(string source, string context, string action) + { + Sql sql = Sql() + .Update(u => u.Set(x => x.Current, false)) + .Where(x => x.Source == source && x.Context == context && x.Action == action && x.Current); + Database.Execute(sql); + } - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new NotSupportedException(); - } + /// + protected override IConsent PerformGet(int id) => throw new NotSupportedException(); - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = Sql().Select().From(); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate().OrderByDescending(x => x.CreateDate); - return ConsentFactory.BuildEntities(Database.Fetch(sql)); - } + /// + protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotSupportedException(); - /// - protected override Sql GetBaseQuery(bool isCount) - { - throw new NotSupportedException(); - } + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = Sql().Select().From(); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate().OrderByDescending(x => x.CreateDate); + return ConsentFactory.BuildEntities(Database.Fetch(sql)); + } - /// - protected override string GetBaseWhereClause() - { - throw new NotSupportedException(); - } + /// + protected override Sql GetBaseQuery(bool isCount) => throw new NotSupportedException(); - /// - protected override IEnumerable GetDeleteClauses() - { - throw new NotSupportedException(); - } + /// + protected override string GetBaseWhereClause() => throw new NotSupportedException(); - /// - protected override void PersistNewItem(IConsent entity) - { - entity.AddingEntity(); + /// + protected override IEnumerable GetDeleteClauses() => throw new NotSupportedException(); - var dto = ConsentFactory.BuildDto(entity); - Database.Insert(dto); - entity.Id = dto.Id; - entity.ResetDirtyProperties(); - } + /// + protected override void PersistNewItem(IConsent entity) + { + entity.AddingEntity(); - /// - protected override void PersistUpdatedItem(IConsent entity) - { - entity.UpdatingEntity(); + ConsentDto dto = ConsentFactory.BuildDto(entity); + Database.Insert(dto); + entity.Id = dto.Id; + entity.ResetDirtyProperties(); + } - var dto = ConsentFactory.BuildDto(entity); - Database.Update(dto); - entity.ResetDirtyProperties(); + /// + protected override void PersistUpdatedItem(IConsent entity) + { + entity.UpdatingEntity(); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); - } + ConsentDto dto = ConsentFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); - /// - public void ClearCurrent(string source, string context, string action) - { - var sql = Sql() - .Update(u => u.Set(x => x.Current, false)) - .Where(x => x.Source == source && x.Context == context && x.Action == action && x.Current); - Database.Execute(sql); - } + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 651c3fb455..0162ba8d52 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NPoco; @@ -48,6 +45,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// /// /// + /// + /// + /// + /// + /// /// /// 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 /// @@ -81,8 +83,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected abstract Guid NodeObjectTypeId { get; } protected ILanguageRepository LanguageRepository { get; } + protected IDataTypeService DataTypeService { get; } + protected IRelationRepository RelationRepository { get; } + protected IRelationTypeRepository RelationTypeRepository { get; } protected PropertyEditorCollection PropertyEditors { get; } @@ -102,13 +107,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // gets all version ids, current first public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => tsql.Select(x => x.Id) .From() .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) .OrderByDescending(x => x.Current) // current '1' comes before others '0' - .AndByDescending(x => x.VersionDate) // most recent first - ); + .AndByDescending(x => x.VersionDate)); // most recent first + return Database.Fetch(SqlSyntax.SelectTop(template.Sql(nodeId), maxRows)); } @@ -118,34 +123,45 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // TODO: test object node type? // get the version we want to delete - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => - tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId")) - ); - var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => + tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId"))); + ContentVersionDto? versionDto = Database.Fetch(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. + // deletes all versions of an entity, older than a date. public virtual void DeleteVersions(int nodeId, DateTime versionDate) { // TODO: test object node type? // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetVersions, tsql => - tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - ); - var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); - foreach (var versionDto in versionDtos) + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.VersionableRepository.GetVersions, + tsql => + tsql.Select() + .From() + .Where(x => + x.NodeId == SqlTemplate.Arg("nodeId") && + !x.Current && + x.VersionDate < SqlTemplate.Arg("versionDate"))); + List? versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); + foreach (ContentVersionDto versionDto in versionDtos) + { PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + } } // actually deletes a version @@ -164,7 +180,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement ? "-1," : "," + parentId + ","; - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .SelectCount() .From(); @@ -194,7 +210,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// public int CountChildren(int parentId, string? contentTypeAlias = null) { - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .SelectCount() .From(); @@ -224,7 +240,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// public int Count(string? contentTypeAlias = null) { - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .SelectCount() .From(); @@ -256,33 +272,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// protected void SetEntityTags(IContentBase entity, ITagRepository tagRepo, IJsonSerializer serializer) { - foreach (var property in entity.Properties) + foreach (IProperty property in entity.Properties) { - var tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); - if (tagConfiguration == null) continue; // not a tags property + TagConfiguration? tagConfiguration = property.GetTagConfiguration(PropertyEditors, DataTypeService); + if (tagConfiguration == null) + { + continue; // not a tags property + } if (property.PropertyType.VariesByCulture()) { var tags = new List(); - foreach (var pvalue in property.Values) + foreach (IPropertyValue pvalue in property.Values) { - var tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer, pvalue.Culture); var languageId = LanguageRepository.GetIdByIsoCode(pvalue.Culture); - var cultureTags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x, LanguageId = languageId }); + IEnumerable 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 - var tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); + IEnumerable tagsValue = property.GetTagsValue(PropertyEditors, DataTypeService, serializer); // strings + IEnumerable tags = tagsValue.Select(x => new Tag { Group = tagConfiguration.Group, Text = x }); tagRepo.Assign(entity.Id, property.PropertyTypeId, tags); } } } // TODO: should we do it when un-publishing? or? + /// /// Clears tags for an item. /// @@ -296,18 +317,25 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private Sql PreparePageSql(Sql sql, Sql? filterSql, Ordering ordering) { // non-filtering, non-ordering = nothing to do - if (filterSql == null && ordering.IsEmpty) return sql; + if (filterSql == null && ordering.IsEmpty) + { + return sql; + } // preserve original var psql = new Sql(sql.SqlContext, sql.SQL, sql.Arguments); // apply filter if (filterSql != null) + { psql.Append(filterSql); + } // non-sorting, we're done if (ordering.IsEmpty) + { return psql; + } // else apply ordering ApplyOrdering(ref psql, ordering); @@ -315,7 +343,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // 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 - var (dbfield, _) = SqlContext.VisitDto(x => x.NodeId); if (ordering.IsCustomField || !ordering.OrderBy.InvariantEquals("id")) { @@ -337,13 +364,21 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } } } + return psql; } private void ApplyOrdering(ref Sql sql, Ordering ordering) { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(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) @@ -361,32 +396,41 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // 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 sql, Ordering ordering) { // id is invariant if (ordering.OrderBy.InvariantEquals("id")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.NodeId), sql); + } // sort order is invariant if (ordering.OrderBy.InvariantEquals("sortOrder")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.SortOrder), sql); + } // path is invariant if (ordering.OrderBy.InvariantEquals("path")) + { return GetAliasedField(SqlSyntax.GetFieldName(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() + Sql joins = Sql() .InnerJoin("ownerUser").On((node, user) => node.UserId == user.Id, aliasRight: "ownerUser"); // see notes in ApplyOrdering: the field MUST be selected + aliased @@ -400,11 +444,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // 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")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.VersionDate), sql); + } // create date is invariant (we don't keep each culture's creation date) if (ordering.OrderBy.InvariantEquals("createDate")) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.CreateDate), sql); + } // name is variant if (ordering.OrderBy.InvariantEquals("name")) @@ -412,7 +460,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // no culture = can only work on the invariant name // see notes in ApplyOrdering: the field MUST be aliased if (ordering.Culture.IsNullOrWhiteSpace()) + { return GetAliasedField(SqlSyntax.GetFieldName(x => x.Text!), sql); + } // "variantName" alias is defined in DocumentRepository.GetBaseQuery // TODO: what if it is NOT a document but a ... media or whatever? @@ -424,7 +474,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // content type alias is invariant if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) { - var joins = Sql() + Sql joins = Sql() .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); // see notes in ApplyOrdering: the field MUST be selected + aliased @@ -449,7 +499,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement 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 + Sql 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} @@ -471,7 +521,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // create the outer join complete sql fragment var outerJoinTempTable = $@"LEFT OUTER JOIN ({innerSqlString}) AS customPropData - ON customPropData.customPropNodeId = {Cms.Core.Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important! + ON customPropData.customPropNodeId = {Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important! // insert this just above the first WHERE var newSql = InsertBefore(sql.SQL, "WHERE", outerJoinTempTable); @@ -491,16 +541,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // would ensure that items without a value always come last, both in ASC and DESC-ending sorts } - public abstract IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, - Ordering? ordering); + public abstract IEnumerable GetPage(IQuery? query, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering); public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) { var report = new Dictionary(); - var sql = SqlContext.Sql() + Sql sql = SqlContext.Sql() .Select() .From() .Where(x => x.NodeObjectType == NodeObjectTypeId) @@ -508,13 +555,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var nodesToRebuild = new Dictionary>(); var validNodes = new Dictionary(); - var rootIds = new[] {Cms.Core.Constants.System.Root, Cms.Core.Constants.System.RecycleBinContent, Cms.Core.Constants.System.RecycleBinMedia}; + var rootIds = new[] { Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia }; var currentParentIds = new HashSet(rootIds); - var prevParentIds = currentParentIds; + HashSet prevParentIds = currentParentIds; var lastLevel = -1; // use a forward cursor (query) - foreach (var node in Database.Query(sql)) + foreach (NodeDto? node in Database.Query(sql)) { if (node.Level != lastLevel) { @@ -541,7 +588,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId)); AppendNodeToFix(nodesToRebuild, node); } - else if (pathParts.Length == 0) + else if (pathParts.Length == 0) { // invalid path report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); @@ -571,7 +618,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // don't track unless we are configured to fix if (options.FixIssues) + { validNodes.Add(node.NodeId, node); + } } } @@ -582,11 +631,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // iterate all valid nodes to see if these are parents for invalid nodes foreach (var (nodeId, node) in validNodes) { - if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue; + if (!nodesToRebuild.TryGetValue(nodeId, out List? invalidNodes)) + { + continue; + } // now we can try to rebuild the invalid paths. - - foreach (var invalidNode in invalidNodes) + foreach (NodeDto invalidNode in invalidNodes) { invalidNode.Level = (short)(node.Level + 1); invalidNode.Path = node.Path + "," + invalidNode.NodeId; @@ -594,11 +645,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } } - foreach (var node in updated) + foreach (NodeDto node in updated) { Database.Update(node); - if (report.TryGetValue(node.NodeId, out var entry)) + if (report.TryGetValue(node.NodeId, out ContentDataIntegrityReportEntry? entry)) + { entry.Fixed = true; + } } } @@ -607,30 +660,44 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement private static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) { - if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) + if (nodesToRebuild.TryGetValue(node.ParentId, out List? childIds)) + { childIds.Add(node); + } else + { nodesToRebuild[node.ParentId] = new List { node }; + } } // here, filter can be null and ordering cannot - protected IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, + protected IEnumerable GetPage( + IQuery? query, + long pageIndex, + int pageSize, + out long totalRecords, Func, IEnumerable> mapDtos, Sql? filter, Ordering? ordering) { - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } // start with base query, and apply the supplied IQuery - if (query == null) query = Query(); - var sql = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); + if (query == null) + { + query = Query(); + } + + Sql sql = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate(); // sort and filter sql = PreparePageSql(sql, filter, ordering); // get a page of DTOs and the total count - var pagedResult = Database.Page(pageIndex + 1, pageSize, sql); + Page? pagedResult = Database.Page(pageIndex + 1, pageSize, sql); totalRecords = Convert.ToInt32(pagedResult.TotalItems); // map the DTOs and return @@ -641,13 +708,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement where T : class, IContentBase { var versions = new List(); - foreach (var temp in temps) + foreach (TempContent temp in temps) { versions.Add(temp.VersionId); if (temp.PublishedVersionId > 0) + { versions.Add(temp.PublishedVersionId); + } + } + + if (versions.Count == 0) + { + return new Dictionary(); } - 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 @@ -666,7 +739,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // get PropertyDataDto distinct PropertyTypeDto var allPropertyTypeIds = allPropertyDataDtos.Select(x => x.PropertyTypeId).Distinct().ToList(); - var allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => + IEnumerable allPropertyTypeDtos = Database.FetchByGroups(allPropertyTypeIds, Constants.Sql.MaxParameterCount, batch => SqlContext.Sql() .Select(r => r.Select(x => x.DataTypeDto)) .From() @@ -675,15 +748,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // index the types for perfs, and assign to PropertyDataDto var indexedPropertyTypeDtos = allPropertyTypeDtos.ToDictionary(x => x.Id, x => x); - foreach (var a in allPropertyDataDtos) + foreach (PropertyDataDto a in allPropertyDataDtos) + { a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; + } // now we have // - the definitions // - all property data dtos // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems) // and we need to build the proper property collections - return GetPropertyCollections(temps, allPropertyDataDtos); } @@ -696,15 +770,18 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // index PropertyDataDto per versionId for perfs // merge edited and published dtos var indexedPropertyDataDtos = new Dictionary>(); - foreach (var dto in allPropertyDataDtos) + foreach (PropertyDataDto dto in allPropertyDataDtos) { var versionId = dto.VersionId; - if (indexedPropertyDataDtos.TryGetValue(versionId, out var list) == false) + if (indexedPropertyDataDtos.TryGetValue(versionId, out List? list) == false) + { indexedPropertyDataDtos[versionId] = list = new List(); + } + list.Add(dto); } - foreach (var temp in temps) + foreach (TempContent temp in temps) { // compositionProperties is the property types for the entire composition // use an index for perfs @@ -712,25 +789,39 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { continue; } - if (compositionPropertiesIndex.TryGetValue(temp.ContentType.Id, out var compositionProperties) == false) + + if (compositionPropertiesIndex.TryGetValue(temp.ContentType.Id, out IPropertyType[]? 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(); - if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out var propertyDataDtos1)) + if (indexedPropertyDataDtos.TryGetValue(temp.VersionId, out List? propertyDataDtos1)) { propertyDataDtos.AddRange(propertyDataDtos1); - if (temp.VersionId == temp.PublishedVersionId) // dirty corner case + + // dirty corner case + if (temp.VersionId == temp.PublishedVersionId) + { propertyDataDtos.AddRange(propertyDataDtos1.Select(x => x.Clone(-1))); + } } - if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out var propertyDataDtos2)) + + if (temp.VersionId != temp.PublishedVersionId && indexedPropertyDataDtos.TryGetValue(temp.PublishedVersionId, out List? propertyDataDtos2)) + { propertyDataDtos.AddRange(propertyDataDtos2); + } + var properties = PropertyFactory.BuildEntities(compositionProperties, propertyDataDtos, temp.PublishedVersionId, LanguageRepository).ToList(); 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); } @@ -746,7 +837,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected string InsertBefore(string s, string atToken, string insert) { var pos = s.InvariantIndexOf(atToken); - if (pos < 0) throw new Exception($"Could not find token \"{atToken}\"."); + if (pos < 0) + { + throw new Exception($"Could not find token \"{atToken}\"."); + } + return s.Insert(pos, insert); } @@ -775,9 +870,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // // 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().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); + MatchCollection matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL); + Match? match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field)); return match == null ? field : match.Groups[2].Value; } @@ -804,7 +898,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement /// protected void OnUowRefreshedEntity(INotification notification) => _eventAggregator.Publish(notification); - protected void OnUowRemovingEntity(IContentBase entity) => _eventAggregator.Publish(new ScopedEntityRemoveNotification(entity, new EventMessages())); #endregion @@ -879,55 +972,51 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected virtual string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql + SqlTemplate? template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text!, "name")) .From() - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId")) - ); + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); - var sql = template.Sql(NodeObjectTypeId, parentId); - var names = Database.Fetch(sql); + Sql sql = template.Sql(NodeObjectTypeId, parentId); + List? names = Database.Fetch(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 + SqlTemplate? template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql .Select("MAX(sortOrder)") .From() - .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId")) - ); + .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); - var sql = template.Sql(NodeObjectTypeId, parentId); + Sql sql = template.Sql(NodeObjectTypeId, parentId); var sortOrder = Database.ExecuteScalar(sql); - return (sortOrder + 1) ?? first; + return sortOrder + 1 ?? first; } protected virtual NodeDto GetParentNodeDto(int parentId) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql + SqlTemplate? template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql .Select() .From() - .Where(x => x.NodeId == SqlTemplate.Arg("parentId")) - ); + .Where(x => x.NodeId == SqlTemplate.Arg("parentId"))); - var sql = template.Sql(parentId); - var nodeDto = Database.First(sql); + Sql sql = template.Sql(parentId); + NodeDto? nodeDto = Database.First(sql); return nodeDto; } protected virtual int GetReservedId(Guid uniqueId) { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql + SqlTemplate template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql .Select(x => x.NodeId) .From() - .Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation) - ); + .Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)); - var sql = template.Sql(new { uniqueId }); + Sql sql = template.Sql(new { uniqueId }); var id = Database.ExecuteScalar(sql); return id ?? 0; @@ -953,39 +1042,47 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement var trackedRelations = new List(); 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); + // First delete all auto-relations for this entity + RelationRepository.DeleteByParent(entity.Id, Constants.Conventions.RelationTypes.AutomaticRelationTypes); - if (trackedRelations.Count == 0) return; + 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 + // lookup in the DB all INT ids for the GUIDs and chuck into a dictionary var keyToIds = Database.Fetch(Sql().Select(x => x.NodeId, x => x.UniqueId).From().WhereIn(x => x.UniqueId, udiToGuids.Values)) .ToDictionary(x => x.UniqueId, x => x.NodeId); var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty())? .ToDictionary(x => x.Alias, x => x); - var toSave = trackedRelations.Select(rel => + IEnumerable toSave = trackedRelations.Select(rel => { - if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) + if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out IRelationType? relationType)) + { throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); + } - if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) + if (!udiToGuids.TryGetValue(rel.Udi, out Guid 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(); // Save bulk relations RelationRepository.SaveBulk(toSave); - } /// @@ -1001,11 +1098,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected void InsertPropertyValues(TEntity entity, int publishedVersionId, out bool edited, out HashSet? 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) + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + foreach (PropertyDataDto? 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. } @@ -1024,23 +1122,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // 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().Where(x => x.VersionId == versionId).ForUpdate(); - var existingPropData = Database.Fetch(propDataSql); + Sql propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == versionId).ForUpdate(); + List? existingPropData = Database.Fetch(propDataSql); var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string? segment), PropertyDataDto>(); var existingPropDataIds = new List(); - foreach (var p in existingPropData) + foreach (PropertyDataDto? 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) + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures); + + foreach (PropertyDataDto 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)) + if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out PropertyDataDto? propData)) { propertyDataDto.Id = propData.Id; Database.Update(propertyDataDto); @@ -1055,12 +1152,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // 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().WhereIn(x => x.Id, existingPropDataIds)); } - } private class NodeIdKey diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 3a305a6371..72ebd3a79a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -8,409 +5,413 @@ using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -using Enumerable = System.Linq.Enumerable; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Implements . +/// +internal class ContentTypeCommonRepository : IContentTypeCommonRepository { + private const string CacheKey = + "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; + + private readonly AppCaches _appCaches; + private readonly IScopeAccessor _scopeAccessor; + private readonly IShortStringHelper _shortStringHelper; + private readonly ITemplateRepository _templateRepository; + /// - /// Implements . + /// Initializes a new instance of the class. /// - internal class ContentTypeCommonRepository : IContentTypeCommonRepository + public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, + AppCaches appCaches, IShortStringHelper shortStringHelper) { - private const string CacheKey = - "Umbraco.Core.Persistence.Repositories.Implement.ContentTypeCommonRepository::AllTypes"; + _scopeAccessor = scopeAccessor; + _templateRepository = templateRepository; + _appCaches = appCaches; + _shortStringHelper = shortStringHelper; + } - private readonly AppCaches _appCaches; - private readonly IScopeAccessor _scopeAccessor; - private readonly IShortStringHelper _shortStringHelper; - private readonly ITemplateRepository _templateRepository; + private IScope? AmbientScope => _scopeAccessor.AmbientScope; - /// - /// Initializes a new instance of the class. - /// - public ContentTypeCommonRepository(IScopeAccessor scopeAccessor, ITemplateRepository templateRepository, - AppCaches appCaches, IShortStringHelper shortStringHelper) + private IUmbracoDatabase? Database => AmbientScope?.Database; + + private ISqlContext? SqlContext => AmbientScope?.SqlContext; + + // private Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); + // private ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; + // private IQuery Query() => SqlContext.Query(); + + /// + public IEnumerable? GetAllTypes() => + + // use a 5 minutes sliding cache - same as FullDataSet cache policy + _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); + + /// + public void ClearCache() => _appCaches.RuntimeCache.Clear(CacheKey); + + private Sql? Sql() => SqlContext?.Sql(); + + private IEnumerable GetAllTypesInternal() + { + var contentTypes = new Dictionary(); + + // get content types + Sql? sql1 = Sql()? + .Select(r => r.Select(x => x.NodeDto)) + .From() + .InnerJoin().On((ct, n) => ct.NodeId == n.NodeId) + .OrderBy(x => x.NodeId); + + List? contentTypeDtos = Database?.Fetch(sql1); + + // get allowed content types + Sql? sql2 = Sql()? + .Select() + .From() + .OrderBy(x => x.Id); + + List? allowedDtos = Database?.Fetch(sql2); + + if (contentTypeDtos is null) { - _scopeAccessor = scopeAccessor; - _templateRepository = templateRepository; - _appCaches = appCaches; - _shortStringHelper = shortStringHelper; - } - - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; - - private IUmbracoDatabase? Database => AmbientScope?.Database; - - private ISqlContext? SqlContext => AmbientScope?.SqlContext; - //private Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); - //private ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; - //private IQuery Query() => SqlContext.Query(); - - /// - public IEnumerable? GetAllTypes() => - // use a 5 minutes sliding cache - same as FullDataSet cache policy - _appCaches.RuntimeCache.GetCacheItem(CacheKey, GetAllTypesInternal, TimeSpan.FromMinutes(5), true); - - /// - public void ClearCache() => _appCaches.RuntimeCache.Clear(CacheKey); - - private Sql? Sql() => SqlContext?.Sql(); - - private IEnumerable GetAllTypesInternal() - { - var contentTypes = new Dictionary(); - - // get content types - Sql? sql1 = Sql()? - .Select(r => r.Select(x => x.NodeDto)) - .From() - .InnerJoin().On((ct, n) => ct.NodeId == n.NodeId) - .OrderBy(x => x.NodeId); - - List? contentTypeDtos = Database?.Fetch(sql1); - - // get allowed content types - Sql? sql2 = Sql()? - .Select() - .From() - .OrderBy(x => x.Id); - - List? allowedDtos = Database?.Fetch(sql2); - - if (contentTypeDtos is null) - { - return contentTypes.Values; - } - // prepare - // note: same alias could be used for media, content... but always different ids = ok - var aliases = Enumerable.ToDictionary(contentTypeDtos, x => x.NodeId, x => x.Alias); - - // create - var allowedDtoIx = 0; - foreach (ContentTypeDto contentTypeDto in contentTypeDtos) - { - // create content type - IContentTypeComposition contentType; - if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MediaType) - { - contentType = ContentTypeFactory.BuildMediaTypeEntity(_shortStringHelper, contentTypeDto); - } - else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.DocumentType) - { - contentType = ContentTypeFactory.BuildContentTypeEntity(_shortStringHelper, contentTypeDto); - } - else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) - { - contentType = ContentTypeFactory.BuildMemberTypeEntity(_shortStringHelper, contentTypeDto); - } - else - { - throw new PanicException( - $"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); - } - - contentTypes.Add(contentType.Id, contentType); - - // map allowed content types - var allowedContentTypes = new List(); - while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) - { - ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; - if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) - { - continue; - } - - allowedContentTypes.Add(new ContentTypeSort(new Lazy(() => allowedDto.AllowedId), - allowedDto.SortOrder, alias!)); - allowedDtoIx++; - } - - contentType.AllowedContentTypes = allowedContentTypes; - } - - MapTemplates(contentTypes); - MapComposition(contentTypes); - MapGroupsAndProperties(contentTypes); - MapHistoryCleanup(contentTypes); - - // finalize - foreach (IContentTypeComposition contentType in contentTypes.Values) - { - contentType.ResetDirtyProperties(false); - } - return contentTypes.Values; } - private void MapHistoryCleanup(Dictionary contentTypes) + // prepare + // note: same alias could be used for media, content... but always different ids = ok + var aliases = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.Alias); + + // create + var allowedDtoIx = 0; + foreach (ContentTypeDto contentTypeDto in contentTypeDtos) { - // get templates - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ContentTypeId); - - var contentVersionCleanupPolicyDtos = Database?.Fetch(sql1); - - var contentVersionCleanupPolicyDictionary = - contentVersionCleanupPolicyDtos?.ToDictionary(x => x.ContentTypeId); - foreach (IContentTypeComposition c in contentTypes.Values) + // create content type + IContentTypeComposition contentType; + if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MediaType) { - if (!(c is ContentType contentType)) + contentType = ContentTypeFactory.BuildMediaTypeEntity(_shortStringHelper, contentTypeDto); + } + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.DocumentType) + { + contentType = ContentTypeFactory.BuildContentTypeEntity(_shortStringHelper, contentTypeDto); + } + else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) + { + contentType = ContentTypeFactory.BuildMemberTypeEntity(_shortStringHelper, contentTypeDto); + } + else + { + throw new PanicException( + $"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); + } + + contentTypes.Add(contentType.Id, contentType); + + // map allowed content types + var allowedContentTypes = new List(); + while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) + { + ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; + if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) { continue; } - var historyCleanup = new HistoryCleanup(); - - if (contentVersionCleanupPolicyDictionary is not null && contentVersionCleanupPolicyDictionary.TryGetValue(contentType.Id, out var versionCleanup)) - { - historyCleanup.PreventCleanup = versionCleanup.PreventCleanup; - historyCleanup.KeepAllVersionsNewerThanDays = versionCleanup.KeepAllVersionsNewerThanDays; - historyCleanup.KeepLatestVersionPerDayForDays = versionCleanup.KeepLatestVersionPerDayForDays; - } - - contentType.HistoryCleanup = historyCleanup; + allowedContentTypes.Add(new ContentTypeSort( + new Lazy(() => allowedDto.AllowedId), + allowedDto.SortOrder, alias!)); + allowedDtoIx++; } + + contentType.AllowedContentTypes = allowedContentTypes; } - private void MapTemplates(Dictionary contentTypes) + MapTemplates(contentTypes); + MapComposition(contentTypes); + MapGroupsAndProperties(contentTypes); + MapHistoryCleanup(contentTypes); + + // finalize + foreach (IContentTypeComposition contentType in contentTypes.Values) { - // get templates - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ContentTypeNodeId); - - List? templateDtos = Database?.Fetch(sql1); - //var templates = templateRepository.GetMany(templateDtos.Select(x => x.TemplateNodeId).ToArray()).ToDictionary(x => x.Id, x => x); - var allTemplates = _templateRepository.GetMany(); - if (allTemplates is null) - { - return; - } - var templates = Enumerable.ToDictionary(allTemplates, x => x.Id, x => x); - var templateDtoIx = 0; - - foreach (IContentTypeComposition c in contentTypes.Values) - { - if (!(c is IContentType contentType)) - { - continue; - } - - // map allowed templates - var allowedTemplates = new List(); - var defaultTemplateId = 0; - while (templateDtoIx < templateDtos?.Count && - templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) - { - ContentTypeTemplateDto allowedDto = templateDtos[templateDtoIx]; - templateDtoIx++; - if (!templates.TryGetValue(allowedDto.TemplateNodeId, out ITemplate? template)) - { - continue; - } - - allowedTemplates.Add(template); - - if (allowedDto.IsDefault) - { - defaultTemplateId = template.Id; - } - } - - contentType.AllowedTemplates = allowedTemplates; - contentType.DefaultTemplateId = defaultTemplateId; - } + contentType.ResetDirtyProperties(false); } - private void MapComposition(IDictionary contentTypes) + return contentTypes.Values; + } + + private void MapHistoryCleanup(Dictionary contentTypes) + { + // get templates + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ContentTypeId); + + List? contentVersionCleanupPolicyDtos = + Database?.Fetch(sql1); + + var contentVersionCleanupPolicyDictionary = + contentVersionCleanupPolicyDtos?.ToDictionary(x => x.ContentTypeId); + foreach (IContentTypeComposition c in contentTypes.Values) { - // get parent/child - Sql? sql1 = Sql()? - .Select() - .From() - .OrderBy(x => x.ChildId); - - List? compositionDtos = Database?.Fetch(sql1); - - // map - var compositionIx = 0; - foreach (IContentTypeComposition contentType in contentTypes.Values) + if (!(c is ContentType contentType)) { - while (compositionIx < compositionDtos?.Count && - compositionDtos[compositionIx].ChildId == contentType.Id) - { - ContentType2ContentTypeDto parentDto = compositionDtos[compositionIx]; - compositionIx++; - - if (!contentTypes.TryGetValue(parentDto.ParentId, out IContentTypeComposition? parentContentType)) - { - continue; - } - - contentType.AddContentType(parentContentType); - } - } - } - - private void MapGroupsAndProperties(IDictionary contentTypes) - { - Sql? sql1 = Sql()? - .Select() - .From() - .InnerJoin() - .On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) - .OrderBy(x => x.NodeId) - .AndBy(x => x.SortOrder, x => x.Id); - - List? groupDtos = Database?.Fetch(sql1); - - Sql? sql2 = Sql()? - .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) - .AndSelect() - .From() - .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) - .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) - .InnerJoin() - .On((pt, ct) => pt.ContentTypeId == ct.NodeId) - .LeftJoin() - .On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) - .LeftJoin() - .On((pt, mpt) => pt.Id == mpt.PropertyTypeId) - .OrderBy(x => x.NodeId) - .AndBy< - PropertyTypeGroupDto>(x => x.SortOrder, - x => x.Id) // NULLs will come first or last, never mind, we deal with it below - .AndBy(x => x.SortOrder, x => x.Id); - - List? propertyDtos = Database?.Fetch(sql2); - Dictionary builtinProperties = - ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - - var groupIx = 0; - var propertyIx = 0; - foreach (IContentTypeComposition contentType in contentTypes.Values) - { - // only IContentType is publishing - var isPublishing = contentType is IContentType; - - // get group-less properties (in case NULL is ordered first) - var noGroupPropertyTypes = new PropertyTypeCollection(isPublishing); - while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == null) - { - noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); - propertyIx++; - } - - // get groups and their properties - var groupCollection = new PropertyGroupCollection(); - while (groupIx < groupDtos?.Count && groupDtos[groupIx].ContentTypeNodeId == contentType.Id) - { - PropertyGroup group = MapPropertyGroup(groupDtos[groupIx], isPublishing); - groupCollection.Add(group); - groupIx++; - - while (propertyIx < propertyDtos?.Count && - propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) - { - group.PropertyTypes?.Add(MapPropertyType(contentType, propertyDtos[propertyIx], - builtinProperties)); - propertyIx++; - } - } - - contentType.PropertyGroups = groupCollection; - - // get group-less properties (in case NULL is ordered last) - while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && - propertyDtos[propertyIx].PropertyTypeGroupId == null) - { - noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); - propertyIx++; - } - - contentType.NoGroupPropertyTypes = noGroupPropertyTypes; - - // ensure builtin properties - if (contentType is IMemberType memberType) - { - // ensure that property types exist (ok if they already exist) - foreach ((var alias, PropertyType propertyType) in builtinProperties) - { - - var added = memberType.AddPropertyType(propertyType, - Constants.Conventions.Member.StandardPropertiesGroupAlias, - Constants.Conventions.Member.StandardPropertiesGroupName); - - if (added) - { - memberType.SetIsSensitiveProperty(alias, false); - memberType.SetMemberCanEditProperty(alias, false); - memberType.SetMemberCanViewProperty(alias, false); - } - } - } - } - } - - private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) => - new PropertyGroup(new PropertyTypeCollection(isPublishing)) - { - Id = dto.Id, - Key = dto.UniqueId, - Type = (PropertyGroupType)dto.Type, - Name = dto.Text, - Alias = dto.Alias, - SortOrder = dto.SortOrder - }; - - private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, - IDictionary builtinProperties) - { - var groupId = dto.PropertyTypeGroupId; - - var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias!, out PropertyType? propertyType); - ValueStorageType storageType = readonlyStorageType - ? propertyType!.ValueStorageType - : Enum.Parse(dto.DataTypeDto.DbType); - - if (contentType is IMemberType memberType && dto.Alias is not null) - { - memberType.SetIsSensitiveProperty(dto.Alias, dto.IsSensitive); - memberType.SetMemberCanEditProperty(dto.Alias, dto.CanEdit); - memberType.SetMemberCanViewProperty(dto.Alias, dto.ViewOnProfile); + continue; } - return new - PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, - dto.Alias) - { - Description = dto.Description, - DataTypeId = dto.DataTypeId, - DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, - Id = dto.Id, - Key = dto.UniqueId, - Mandatory = dto.Mandatory, - MandatoryMessage = dto.MandatoryMessage, - Name = dto.Name ?? string.Empty, - PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, - SortOrder = dto.SortOrder, - ValidationRegExp = dto.ValidationRegExp, - ValidationRegExpMessage = dto.ValidationRegExpMessage, - Variations = (ContentVariation)dto.Variations, - LabelOnTop = dto.LabelOnTop - }; + var historyCleanup = new HistoryCleanup(); + + if (contentVersionCleanupPolicyDictionary is not null && + contentVersionCleanupPolicyDictionary.TryGetValue( + contentType.Id, + out ContentVersionCleanupPolicyDto? versionCleanup)) + { + historyCleanup.PreventCleanup = versionCleanup.PreventCleanup; + historyCleanup.KeepAllVersionsNewerThanDays = versionCleanup.KeepAllVersionsNewerThanDays; + historyCleanup.KeepLatestVersionPerDayForDays = versionCleanup.KeepLatestVersionPerDayForDays; + } + + contentType.HistoryCleanup = historyCleanup; } } + + private void MapTemplates(Dictionary contentTypes) + { + // get templates + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ContentTypeNodeId); + + List? templateDtos = Database?.Fetch(sql1); + + // var templates = templateRepository.GetMany(templateDtos.Select(x => x.TemplateNodeId).ToArray()).ToDictionary(x => x.Id, x => x); + IEnumerable? allTemplates = _templateRepository.GetMany(); + + var templates = allTemplates.ToDictionary(x => x.Id, x => x); + var templateDtoIx = 0; + + foreach (IContentTypeComposition c in contentTypes.Values) + { + if (!(c is IContentType contentType)) + { + continue; + } + + // map allowed templates + var allowedTemplates = new List(); + var defaultTemplateId = 0; + while (templateDtoIx < templateDtos?.Count && + templateDtos[templateDtoIx].ContentTypeNodeId == contentType.Id) + { + ContentTypeTemplateDto allowedDto = templateDtos[templateDtoIx]; + templateDtoIx++; + if (!templates.TryGetValue(allowedDto.TemplateNodeId, out ITemplate? template)) + { + continue; + } + + allowedTemplates.Add(template); + + if (allowedDto.IsDefault) + { + defaultTemplateId = template.Id; + } + } + + contentType.AllowedTemplates = allowedTemplates; + contentType.DefaultTemplateId = defaultTemplateId; + } + } + + private void MapComposition(IDictionary contentTypes) + { + // get parent/child + Sql? sql1 = Sql()? + .Select() + .From() + .OrderBy(x => x.ChildId); + + List? compositionDtos = Database?.Fetch(sql1); + + // map + var compositionIx = 0; + foreach (IContentTypeComposition contentType in contentTypes.Values) + { + while (compositionIx < compositionDtos?.Count && + compositionDtos[compositionIx].ChildId == contentType.Id) + { + ContentType2ContentTypeDto parentDto = compositionDtos[compositionIx]; + compositionIx++; + + if (!contentTypes.TryGetValue(parentDto.ParentId, out IContentTypeComposition? parentContentType)) + { + continue; + } + + contentType.AddContentType(parentContentType); + } + } + } + + private void MapGroupsAndProperties(IDictionary contentTypes) + { + Sql? sql1 = Sql()? + .Select() + .From() + .InnerJoin() + .On((ptg, ct) => ptg.ContentTypeNodeId == ct.NodeId) + .OrderBy(x => x.NodeId) + .AndBy(x => x.SortOrder, x => x.Id); + + List? groupDtos = Database?.Fetch(sql1); + + Sql? sql2 = Sql()? + .Select(r => r.Select(x => x.DataTypeDto, r1 => r1.Select(x => x.NodeDto))) + .AndSelect() + .From() + .InnerJoin().On((pt, dt) => pt.DataTypeId == dt.NodeId) + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) + .InnerJoin() + .On((pt, ct) => pt.ContentTypeId == ct.NodeId) + .LeftJoin() + .On((pt, ptg) => pt.PropertyTypeGroupId == ptg.Id) + .LeftJoin() + .On((pt, mpt) => pt.Id == mpt.PropertyTypeId) + .OrderBy(x => x.NodeId) + .AndBy< + PropertyTypeGroupDto>( + x => x.SortOrder, + x => x.Id) // NULLs will come first or last, never mind, we deal with it below + .AndBy(x => x.SortOrder, x => x.Id); + + List? propertyDtos = Database?.Fetch(sql2); + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + + var groupIx = 0; + var propertyIx = 0; + foreach (IContentTypeComposition contentType in contentTypes.Values) + { + // only IContentType is publishing + var isPublishing = contentType is IContentType; + + // get group-less properties (in case NULL is ordered first) + var noGroupPropertyTypes = new PropertyTypeCollection(isPublishing); + while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) + { + noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + propertyIx++; + } + + // get groups and their properties + var groupCollection = new PropertyGroupCollection(); + while (groupIx < groupDtos?.Count && groupDtos[groupIx].ContentTypeNodeId == contentType.Id) + { + PropertyGroup group = MapPropertyGroup(groupDtos[groupIx], isPublishing); + groupCollection.Add(group); + groupIx++; + + while (propertyIx < propertyDtos?.Count && + propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == group.Id) + { + group.PropertyTypes?.Add(MapPropertyType(contentType, propertyDtos[propertyIx], + builtinProperties)); + propertyIx++; + } + } + + contentType.PropertyGroups = groupCollection; + + // get group-less properties (in case NULL is ordered last) + while (propertyIx < propertyDtos?.Count && propertyDtos[propertyIx].ContentTypeId == contentType.Id && + propertyDtos[propertyIx].PropertyTypeGroupId == null) + { + noGroupPropertyTypes.Add(MapPropertyType(contentType, propertyDtos[propertyIx], builtinProperties)); + propertyIx++; + } + + contentType.NoGroupPropertyTypes = noGroupPropertyTypes; + + // ensure builtin properties + if (contentType is IMemberType memberType) + { + // ensure that property types exist (ok if they already exist) + foreach ((var alias, PropertyType propertyType) in builtinProperties) + { + var added = memberType.AddPropertyType( + propertyType, + Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); + + if (added) + { + memberType.SetIsSensitiveProperty(alias, false); + memberType.SetMemberCanEditProperty(alias, false); + memberType.SetMemberCanViewProperty(alias, false); + } + } + } + } + } + + private PropertyGroup MapPropertyGroup(PropertyTypeGroupDto dto, bool isPublishing) => + new(new PropertyTypeCollection(isPublishing)) + { + Id = dto.Id, + Key = dto.UniqueId, + Type = (PropertyGroupType)dto.Type, + Name = dto.Text, + Alias = dto.Alias, + SortOrder = dto.SortOrder, + }; + + private PropertyType MapPropertyType(IContentTypeComposition contentType, PropertyTypeCommonDto dto, + IDictionary builtinProperties) + { + var groupId = dto.PropertyTypeGroupId; + + var readonlyStorageType = builtinProperties.TryGetValue(dto.Alias!, out PropertyType? propertyType); + ValueStorageType storageType = readonlyStorageType + ? propertyType!.ValueStorageType + : Enum.Parse(dto.DataTypeDto.DbType); + + if (contentType is IMemberType memberType && dto.Alias is not null) + { + memberType.SetIsSensitiveProperty(dto.Alias, dto.IsSensitive); + memberType.SetMemberCanEditProperty(dto.Alias, dto.CanEdit); + memberType.SetMemberCanViewProperty(dto.Alias, dto.ViewOnProfile); + } + + return new + PropertyType(_shortStringHelper, dto.DataTypeDto.EditorAlias, storageType, readonlyStorageType, + dto.Alias) + { + Description = dto.Description, + DataTypeId = dto.DataTypeId, + DataTypeKey = dto.DataTypeDto.NodeDto.UniqueId, + Id = dto.Id, + Key = dto.UniqueId, + Mandatory = dto.Mandatory, + MandatoryMessage = dto.MandatoryMessage, + Name = dto.Name ?? string.Empty, + PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, + SortOrder = dto.SortOrder, + ValidationRegExp = dto.ValidationRegExp, + ValidationRegExpMessage = dto.ValidationRegExpMessage, + Variations = (ContentVariation)dto.Variations, + LabelOnTop = dto.LabelOnTop, + }; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9e2f0257b6..fd7193e4ae 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,306 +12,306 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository + public ContentTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) { - public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) - { } + } - protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; + protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.DocumentType; + + /// + public IEnumerable GetByQuery(IQuery query) + { + var ints = PerformGetByQuery(query).ToArray(); + return ints.Length > 0 ? GetMany(ints) : Enumerable.Empty(); + } + + /// + /// Gets all property type aliases. + /// + /// + public IEnumerable GetAllPropertyTypeAliases() => + Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); + + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) + { + Sql sql = Sql() + .Select("cmsContentType.alias") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId); + + if (objectTypes.Any()) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + sql = sql.WhereIn(dto => dto.NodeObjectType, objectTypes); } - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job + return Database.Fetch(sql); + } - // TODO: the filtering is highly inefficient as we deep-clone everything - // there should be a way to GetMany(predicate) right from the cache policy! - // and ah, well, this all caching should be refactored + the cache refreshers - // should to repository.Clear() not deal with magic caches by themselves - - protected override IContentType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IContentType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); - - protected override IContentType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - - protected override IEnumerable? GetAllWithFullCachePolicy() + public IEnumerable GetAllContentTypeIds(string[] aliases) + { + if (aliases.Length == 0) { - return CommonRepository.GetAllTypes()?.OfType(); + return Enumerable.Empty(); } - protected override IEnumerable? PerformGetAll(params Guid[]? ids) + Sql sql = Sql() + .Select(x => x.NodeId) + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => aliases.Contains(dto.Alias)); + + return Database.Fetch(sql); + } + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job + + // TODO: the filtering is highly inefficient as we deep-clone everything + // there should be a way to GetMany(predicate) right from the cache policy! + // and ah, well, this all caching should be refactored + the cache refreshers + // should to repository.Clear() not deal with magic caches by themselves + protected override IContentType? PerformGet(int id) + => GetMany().FirstOrDefault(x => x.Id == id); + + protected override IContentType? PerformGet(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id); + + protected override IContentType? PerformGet(string alias) + => GetMany().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + protected override bool PerformExists(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id) != null; + + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + IEnumerable all = GetMany(); + return ids?.Any() ?? false ? all.Where(x => ids.Contains(x.Key)) : all; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate(); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 + ? GetMany(ids).OrderBy(x => x.Name) + : Enumerable.Empty(); + } + + protected IEnumerable PerformGetByQuery(IQuery query) + { + // used by DataTypeService to remove properties + // from content types if they have a deleted data type - see + // notes in DataTypeService.Delete as it's a bit weird + Sql sqlClause = Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.PropertyTypeGroupId) + .InnerJoin() + .On(left => left.DataTypeId, right => right.NodeId); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate() + .OrderBy(x => x.PropertyTypeGroupId); + + return Database + .FetchOneToMany(x => x.PropertyTypeDtos, sql) + .Select(x => x.ContentTypeNodeId).Distinct(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(x => x.NodeId); + + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } + + /// + /// Deletes a content type + /// + /// + /// + /// First checks for children and removes those first + /// + protected override void PersistDeletedItem(IContentType entity) + { + IQuery query = Query().Where(x => x.ParentId == entity.Id); + IEnumerable children = Get(query); + foreach (IContentType child in children) { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; + PersistDeletedItem(child); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate(); - var ids = Database.Fetch(sql).Distinct().ToArray(); + // Before we call the base class methods to run all delete clauses, we need to first + // delete all of the property data associated with this document type. Normally this will + // be done in the ContentTypeService by deleting all associated content first, but in some cases + // like when we switch a document type, there is property data left over that is linked + // to the previous document type. So we need to ensure it's removed. + Sql sql = Sql() + .Select("DISTINCT " + Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid") + .From() + .InnerJoin() + .On(dto => dto.PropertyTypeId, dto => dto.Id) + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.ContentTypeId) + .Where(dto => dto.NodeId == entity.Id); - return ids.Length > 0 ? GetMany(ids)?.OrderBy(x => x.Name) ?? Enumerable.Empty() : Enumerable.Empty(); + // Delete all PropertyData where propertytypeid EXISTS in the subquery above + Database.Execute(SqlSyntax.GetDeleteSubquery(Constants.DatabaseSchema.Tables.PropertyData, "propertytypeid", sql)); + + base.PersistDeletedItem(entity); + } + + protected override void PersistNewItem(IContentType entity) + { + if (string.IsNullOrWhiteSpace(entity.Alias)) + { + var ex = new Exception( + $"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + Logger.LogError( + "ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + entity.Name); + throw ex; } - /// - public IEnumerable GetByQuery(IQuery query) + entity.AddingEntity(); + + PersistNewBaseContentType(entity); + PersistTemplates(entity, false); + PersistHistoryCleanup(entity); + + entity.ResetDirtyProperties(); + } + + protected void PersistTemplates(IContentType entity, bool clearAll) + { + // remove and insert, if required + Database.Delete("WHERE contentTypeNodeId = @Id", new { entity.Id }); + + // we could do it all in foreach if we assume that the default template is an allowed template?? + var defaultTemplateId = entity.DefaultTemplateId; + if (defaultTemplateId > 0) { - var ints = PerformGetByQuery(query).ToArray(); - return ints.Length > 0 ? GetMany(ints) ?? Enumerable.Empty() : Enumerable.Empty(); - } - - protected IEnumerable PerformGetByQuery(IQuery query) - { - // used by DataTypeService to remove properties - // from content types if they have a deleted data type - see - // notes in DataTypeService.Delete as it's a bit weird - - var sqlClause = Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.PropertyTypeGroupId) - .InnerJoin() - .On(left => left.DataTypeId, right => right.NodeId); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.PropertyTypeGroupId); - - return Database - .FetchOneToMany(x => x.PropertyTypeDtos, sql) - .Select(x => x.ContentTypeNodeId).Distinct(); - } - - /// - /// Gets all property type aliases. - /// - /// - public IEnumerable GetAllPropertyTypeAliases() - { - return Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); - } - - /// - /// Gets all content type aliases - /// - /// - /// If this list is empty, it will return all content type aliases for media, members and content, otherwise - /// it will only return content type aliases for the object types specified - /// - /// - public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) - { - var sql = Sql() - .Select("cmsContentType.alias") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId); - - if (objectTypes.Any()) + Database.Insert(new ContentTypeTemplateDto { - sql = sql.WhereIn(dto => dto.NodeObjectType, objectTypes); - } - - return Database.Fetch(sql); + ContentTypeNodeId = entity.Id, TemplateNodeId = defaultTemplateId, IsDefault = true, + }); } - public IEnumerable GetAllContentTypeIds(string[] aliases) + foreach (ITemplate template in entity.AllowedTemplates?.Where(x => x.Id != defaultTemplateId) ?? + Array.Empty()) { - if (aliases.Length == 0) return Enumerable.Empty(); - - var sql = Sql() - .Select(x => x.NodeId) - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => aliases.Contains(dto.Alias)); - - return Database.Fetch(sql); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(x => x.NodeId); - - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsDocumentType WHERE contentTypeNodeId = @id"); - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; - } - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DocumentType; - - /// - /// Deletes a content type - /// - /// - /// - /// First checks for children and removes those first - /// - protected override void PersistDeletedItem(IContentType entity) - { - var query = Query().Where(x => x.ParentId == entity.Id); - var children = Get(query); - if (children is not null) + Database.Insert(new ContentTypeTemplateDto { - foreach (var child in children) - { - PersistDeletedItem(child); - } - } + ContentTypeNodeId = entity.Id, TemplateNodeId = template.Id, IsDefault = false, + }); + } + } - //Before we call the base class methods to run all delete clauses, we need to first - // delete all of the property data associated with this document type. Normally this will - // be done in the ContentTypeService by deleting all associated content first, but in some cases - // like when we switch a document type, there is property data left over that is linked - // to the previous document type. So we need to ensure it's removed. - var sql = Sql() - .Select("DISTINCT " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + ".propertytypeid") - .From() - .InnerJoin() - .On(dto => dto.PropertyTypeId, dto => dto.Id) - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.ContentTypeId) - .Where(dto => dto.NodeId == entity.Id); + protected override void PersistUpdatedItem(IContentType entity) + { + ValidateAlias(entity); - //Delete all PropertyData where propertytypeid EXISTS in the subquery above - Database.Execute(SqlSyntax.GetDeleteSubquery(Cms.Core.Constants.DatabaseSchema.Tables.PropertyData, "propertytypeid", sql)); + // Updates Modified date + entity.UpdatingEntity(); - base.PersistDeletedItem(entity); + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; } - protected override void PersistNewItem(IContentType entity) + PersistUpdatedBaseContentType(entity); + PersistTemplates(entity, true); + PersistHistoryCleanup(entity); + + entity.ResetDirtyProperties(); + } + + private void PersistHistoryCleanup(IContentType entity) + { + // historyCleanup property is not mandatory for api endpoint, handle the case where it's not present. + // DocumentTypeSave doesn't handle this for us like ContentType constructors do. + if (entity is IContentTypeWithHistoryCleanup entityWithHistoryCleanup) { - if (string.IsNullOrWhiteSpace(entity.Alias)) + var dto = new ContentVersionCleanupPolicyDto { - var ex = new Exception($"ContentType '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - Logger.LogError("ContentType '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", entity.Name); - throw ex; - } - - entity.AddingEntity(); - - PersistNewBaseContentType(entity); - PersistTemplates(entity, false); - PersistHistoryCleanup(entity); - - entity.ResetDirtyProperties(); - } - - protected void PersistTemplates(IContentType entity, bool clearAll) - { - // remove and insert, if required - Database.Delete("WHERE contentTypeNodeId = @Id", new { Id = entity.Id }); - - // we could do it all in foreach if we assume that the default template is an allowed template?? - var defaultTemplateId = entity.DefaultTemplateId; - if (defaultTemplateId > 0) - { - Database.Insert(new ContentTypeTemplateDto - { - ContentTypeNodeId = entity.Id, - TemplateNodeId = defaultTemplateId, - IsDefault = true - }); - } - foreach (var template in entity.AllowedTemplates?.Where(x => x != null && x.Id != defaultTemplateId) ?? Array.Empty()) - { - Database.Insert(new ContentTypeTemplateDto - { - ContentTypeNodeId = entity.Id, - TemplateNodeId = template.Id, - IsDefault = false - }); - } - } - - protected override void PersistUpdatedItem(IContentType entity) - { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - PersistUpdatedBaseContentType(entity); - PersistTemplates(entity, true); - PersistHistoryCleanup(entity); - - entity.ResetDirtyProperties(); - } - - private void PersistHistoryCleanup(IContentType entity) - { - // historyCleanup property is not mandatory for api endpoint, handle the case where it's not present. - // DocumentTypeSave doesn't handle this for us like ContentType constructors do. - if (entity is IContentTypeWithHistoryCleanup entityWithHistoryCleanup) - { - ContentVersionCleanupPolicyDto dto = new ContentVersionCleanupPolicyDto() - { - ContentTypeId = entity.Id, - Updated = DateTime.Now, - PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, - KeepAllVersionsNewerThanDays = entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, - KeepLatestVersionPerDayForDays = entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays - }; - Database.InsertOrUpdate(dto); - } - + ContentTypeId = entity.Id, + Updated = DateTime.Now, + PreventCleanup = entityWithHistoryCleanup.HistoryCleanup?.PreventCleanup ?? false, + KeepAllVersionsNewerThanDays = + entityWithHistoryCleanup.HistoryCleanup?.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = + entityWithHistoryCleanup.HistoryCleanup?.KeepLatestVersionPerDayForDays, + }; + Database.InsertOrUpdate(dto); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 086542d307..3e92a4ae7f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Globalization; -using System.Linq; +using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -16,1480 +14,1578 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represent an abstract Repository for ContentType based repositories +/// +/// Exposes shared functionality +/// +internal abstract class ContentTypeRepositoryBase : EntityRepositoryBase, + IReadRepository + where TEntity : class, IContentTypeComposition { - /// - /// Represent an abstract Repository for ContentType based repositories - /// - /// Exposes shared functionality - /// - internal abstract class ContentTypeRepositoryBase : EntityRepositoryBase, - IReadRepository - where TEntity : class, IContentTypeComposition + private readonly IShortStringHelper _shortStringHelper; + + protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger> logger, IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger) { - private readonly IShortStringHelper _shortStringHelper; + _shortStringHelper = shortStringHelper; + CommonRepository = commonRepository; + LanguageRepository = languageRepository; + } - protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger> logger, IContentTypeCommonRepository commonRepository, - ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger) + protected IContentTypeCommonRepository CommonRepository { get; } + + protected ILanguageRepository LanguageRepository { get; } + + protected abstract bool SupportsPublishing { get; } + + /// + /// Gets the node object type for the repository's entity + /// + protected abstract Guid NodeObjectTypeId { get; } + + /// + /// Gets an Entity by Id + /// + /// + /// + public TEntity? Get(Guid id) => PerformGet(id); + + /// + /// Gets all entities of the specified type + /// + /// + /// + /// + /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same + /// signature as the main GetAll when there are no parameters + /// + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + PerformGetAll(ids) ?? Enumerable.Empty(); + + /// + /// Boolean indicating whether an Entity with the specified Id exists + /// + /// + /// + public bool Exists(Guid id) => PerformExists(id); + + public IEnumerable> Move(TEntity moving, EntityContainer? container) + { + var parentId = Constants.System.Root; + if (container != null) { - _shortStringHelper = shortStringHelper; - CommonRepository = commonRepository; - LanguageRepository = languageRepository; - } - - protected IContentTypeCommonRepository CommonRepository { get; } - protected ILanguageRepository LanguageRepository { get; } - protected abstract bool SupportsPublishing { get; } - - /// - /// Gets the node object type for the repository's entity - /// - protected abstract Guid NodeObjectTypeId { get; } - - public IEnumerable> Move(TEntity moving, EntityContainer container) - { - var parentId = Cms.Core.Constants.System.Root; - if (container != null) + // check path + if (string.Format(",{0},", container.Path).IndexOf( + string.Format(",{0},", moving.Id), + StringComparison.Ordinal) > -1) { - // check path - if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", moving.Id), - StringComparison.Ordinal) > -1) - throw new DataOperationException(MoveOperationStatusType - .FailedNotAllowedByPath); - - parentId = container.Id; + throw new DataOperationException(MoveOperationStatusType + .FailedNotAllowedByPath); } - // track moved entities - var moveInfo = new List> {new MoveEventInfo(moving, moving.Path, parentId)}; - - - // get the level delta (old pos to new pos) - var levelDelta = container == null - ? 1 - moving.Level - : container.Level + 1 - moving.Level; - - // move to parent (or -1), update path, save - moving.ParentId = parentId; - var movingPath = moving.Path + ","; // save before changing - moving.Path = (container == null ? Cms.Core.Constants.System.RootString : container.Path) + "," + moving.Id; - moving.Level = container == null ? 1 : container.Level + 1; - Save(moving); - - //update all descendants, update in order of level - var descendants = Get(Query().Where(type => type.Path.StartsWith(movingPath))); - var paths = new Dictionary(); - paths[moving.Id] = moving.Path; - - if (descendants is null) - { - return moveInfo; - } - foreach (var descendant in descendants.OrderBy(x => x.Level)) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; - descendant.Level += levelDelta; - - Save(descendant); - } - - return moveInfo; + parentId = container.Id; } - protected override IEnumerable PerformGetAll(params int[]? ids) + // track moved entities + var moveInfo = new List> { new(moving, moving.Path, parentId) }; + + // get the level delta (old pos to new pos) + var levelDelta = container == null + ? 1 - moving.Level + : container.Level + 1 - moving.Level; + + // move to parent (or -1), update path, save + moving.ParentId = parentId; + var movingPath = moving.Path + ","; // save before changing + moving.Path = (container == null ? Constants.System.RootString : container.Path) + "," + moving.Id; + moving.Level = container == null ? 1 : container.Level + 1; + Save(moving); + + // update all descendants, update in order of level + IEnumerable descendants = Get(Query().Where(type => type.Path.StartsWith(movingPath))); + var paths = new Dictionary { - var result = GetAllWithFullCachePolicy(); + [moving.Id] = moving.Path, + }; - // By default the cache policy will always want everything - // even GetMany(ids) gets everything and filters afterwards, - // however if we are using No Cache, we must still be able to support - // collections of Ids, so this is to work around that: - if (ids?.Any() ?? false) - { - return result?.Where(x => ids.Contains(x.Id)) ?? Enumerable.Empty(); - } + foreach (TEntity descendant in descendants.OrderBy(x => x.Level)) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - return result ?? Enumerable.Empty();; + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + + Save(descendant); } - protected abstract IEnumerable? GetAllWithFullCachePolicy(); + return moveInfo; + } - protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, - string propertyTypeAlias) + /// + /// Gets an Entity by alias + /// + /// + /// + public TEntity? Get(string alias) => PerformGet(alias); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + IEnumerable? result = GetAllWithFullCachePolicy(); + + // By default the cache policy will always want everything + // even GetMany(ids) gets everything and filters afterwards, + // however if we are using No Cache, we must still be able to support + // collections of Ids, so this is to work around that: + if (ids?.Any() ?? false) { - return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, propertyTypeAlias); + return result?.Where(x => ids.Contains(x.Id)) ?? Enumerable.Empty(); } - protected override void PersistDeletedItem(TEntity entity) - { - base.PersistDeletedItem(entity); - CommonRepository.ClearCache(); // always - } + return result ?? Enumerable.Empty(); + } - protected void PersistNewBaseContentType(IContentTypeComposition entity) - { - ValidateVariations(entity); + protected abstract IEnumerable? GetAllWithFullCachePolicy(); - var dto = ContentTypeFactory.BuildContentTypeDto(entity); + protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, + string propertyTypeAlias) => + new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, propertyTypeAlias); - //Cannot add a duplicate content type - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType + protected override void PersistDeletedItem(TEntity entity) + { + base.PersistDeletedItem(entity); + CommonRepository.ClearCache(); // always + } + + protected void PersistNewBaseContentType(IContentTypeComposition entity) + { + ValidateVariations(entity); + + ContentTypeDto dto = ContentTypeFactory.BuildContentTypeDto(entity); + + // Cannot add a duplicate content type + var exists = Database.ExecuteScalar( + @"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias AND umbracoNode.nodeObjectType = @objectType", - new {alias = entity.Alias, objectType = NodeObjectTypeId}); - if (exists > 0) + new { alias = entity.Alias, objectType = NodeObjectTypeId }); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); + } + + // Logic for setting Path, Level and SortOrder + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + var level = parent.Level + 1; + var sortOrder = + Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + + // Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) + ? Convert.ToInt32(Database.Insert(nodeDto)) + : Database.Update(nodeDto); + + // Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + // Update entity with correct values + entity.Id = nodeDto.NodeId; // Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // Insert new ContentType entry + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + // Insert ContentType composition in new table + foreach (IContentTypeComposition composition in entity.ContentTypeComposition) + { + if (composition.Id == entity.Id) { - throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); + continue; // Just to ensure that we aren't creating a reference to ourself. } - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new {ParentId = entity.ParentId}); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar( - "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new {ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId}); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) - ? Convert.ToInt32(Database.Insert(nodeDto)) - : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Insert new ContentType entry - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Insert ContentType composition in new table - foreach (var composition in entity.ContentTypeComposition) + if (composition.HasIdentity) { - if (composition.Id == entity.Id) - continue; //Just to ensure that we aren't creating a reference to ourself. - - if (composition.HasIdentity) - { - Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); - } - else - { - //Fallback for ContentTypes with no identity - var contentTypeDto = - Database.FirstOrDefault("WHERE alias = @Alias", - new {Alias = composition.Alias}); - if (contentTypeDto != null) - { - Database.Insert(new ContentType2ContentTypeDto - { - ParentId = contentTypeDto.NodeId, ChildId = entity.Id - }); - } - } + Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); } - - if (entity.AllowedContentTypes is not null) + else { - //Insert collection of allowed content types - foreach (var allowedContentType in entity.AllowedContentTypes) + // Fallback for ContentTypes with no identity + ContentTypeDto? contentTypeDto = + Database.FirstOrDefault( + "WHERE alias = @Alias", + new { composition.Alias }); + if (contentTypeDto != null) { - Database.Insert(new ContentTypeAllowedContentTypeDto + Database.Insert(new ContentType2ContentTypeDto { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder + ParentId = contentTypeDto.NodeId, + ChildId = entity.Id, }); } } + } - //Insert Tabs - foreach (var propertyGroup in entity.PropertyGroups) + if (entity.AllowedContentTypes is not null) + { + // Insert collection of allowed content types + foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) { - var tabDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, nodeDto.NodeId); - var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); - propertyGroup.Id = primaryKey; //Set Id on PropertyGroup - - //Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group - //unless the PropertyGroupId has already been changed. - if (propertyGroup.PropertyTypes is not null) + Database.Insert(new ContentTypeAllowedContentTypeDto { - foreach (var propertyType in propertyGroup.PropertyTypes) + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder, + }); + } + } + + // Insert Tabs + foreach (PropertyGroup propertyGroup in entity.PropertyGroups) + { + PropertyTypeGroupDto tabDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, nodeDto.NodeId); + var primaryKey = Convert.ToInt32(Database.Insert(tabDto)); + propertyGroup.Id = primaryKey; // Set Id on PropertyGroup + + // Ensure that the PropertyGroup's Id is set on the PropertyTypes within a group + // unless the PropertyGroupId has already been changed. + if (propertyGroup.PropertyTypes is not null) + { + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) + { + if (propertyType.IsPropertyDirty("PropertyGroupId") == false) { - if (propertyType.IsPropertyDirty("PropertyGroupId") == false) - { - var tempGroup = propertyGroup; - propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); - } + PropertyGroup tempGroup = propertyGroup; + propertyType.PropertyGroupId = new Lazy(() => tempGroup.Id); } } } - - //Insert PropertyTypes - foreach (var propertyType in entity.PropertyTypes) - { - var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default(int); - //If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) - { - AssignDataTypeFromPropertyEditor(propertyType); - } - - var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType, nodeDto.NodeId); - int typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); - propertyType.Id = typePrimaryKey; //Set Id on new PropertyType - - //Update the current PropertyType with correct PropertyEditorAlias and DatabaseType - var dataTypeDto = - Database.FirstOrDefault("WHERE nodeId = @Id", new {Id = propertyTypeDto.DataTypeId}); - propertyType.PropertyEditorAlias = dataTypeDto.EditorAlias; - propertyType.ValueStorageType = dataTypeDto.DbType.EnumParse(true); - } - - CommonRepository.ClearCache(); // always } - protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) + // Insert PropertyTypes + foreach (IPropertyType propertyType in entity.PropertyTypes) { - CorrectPropertyTypeVariations(entity); - ValidateVariations(entity); + var tabId = propertyType.PropertyGroupId != null ? propertyType.PropertyGroupId.Value : default; - var dto = ContentTypeFactory.BuildContentTypeDto(entity); + // If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) + { + AssignDataTypeFromPropertyEditor(propertyType); + } - // ensure the alias is not used already - var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType + PropertyTypeDto propertyTypeDto = + PropertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType, nodeDto.NodeId); + var typePrimaryKey = Convert.ToInt32(Database.Insert(propertyTypeDto)); + propertyType.Id = typePrimaryKey; // Set Id on new PropertyType + + // Update the current PropertyType with correct PropertyEditorAlias and DatabaseType + DataTypeDto? dataTypeDto = + Database.FirstOrDefault("WHERE nodeId = @Id", new { Id = propertyTypeDto.DataTypeId }); + propertyType.PropertyEditorAlias = dataTypeDto.EditorAlias; + propertyType.ValueStorageType = dataTypeDto.DbType.EnumParse(true); + } + + CommonRepository.ClearCache(); // always + } + + protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) + { + CorrectPropertyTypeVariations(entity); + ValidateVariations(entity); + + ContentTypeDto dto = ContentTypeFactory.BuildContentTypeDto(entity); + + // ensure the alias is not used already + var exists = Database.ExecuteScalar( + @"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias AND umbracoNode.nodeObjectType = @objectType AND umbracoNode.id <> @id", - new {id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId}); - if (exists > 0) + new { id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId }); + if (exists > 0) + { + throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); + } + + // repository should be write-locked when doing this, so we are safe from race-conds + // handle (update) the node + NodeDto nodeDto = dto.NodeDto; + Database.Update(nodeDto); + + // we NEED this: updating, so the .PrimaryKey already exists, but the entity does + // not carry it and therefore the dto does not have it yet - must get it from db, + // look up ContentType entry to get PrimaryKey for updating the DTO + ContentTypeDto? dtoPk = Database.First("WHERE nodeId = @Id", new { entity.Id }); + dto.PrimaryKey = dtoPk.PrimaryKey; + Database.Update(dto); + + // handle (delete then recreate) compositions + Database.Delete("WHERE childContentTypeId = @Id", new { entity.Id }); + foreach (IContentTypeComposition composition in entity.ContentTypeComposition) + { + Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); + } + + // removing a ContentType from a composition (U4-1690) + // 1. Find content based on the current ContentType: entity.Id + // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) + // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one + if (entity.RemovedContentTypes.Any()) + { + // TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? + + // find Content based on the current ContentType + Sql sql = Sql() + .SelectAll() + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) + .Where(x => x.ContentTypeId == entity.Id); + List? contentDtos = Database.Fetch(sql); + + // loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition + foreach (var key in entity.RemovedContentTypes) { - throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); - } + // find PropertyTypes for the removed ContentType + List? propertyTypes = + Database.Fetch("WHERE contentTypeId = @Id", new { Id = key }); - // repository should be write-locked when doing this, so we are safe from race-conds - // handle (update) the node - var nodeDto = dto.NodeDto; - Database.Update(nodeDto); - - // we NEED this: updating, so the .PrimaryKey already exists, but the entity does - // not carry it and therefore the dto does not have it yet - must get it from db, - // look up ContentType entry to get PrimaryKey for updating the DTO - var dtoPk = Database.First("WHERE nodeId = @Id", new {entity.Id}); - dto.PrimaryKey = dtoPk.PrimaryKey; - Database.Update(dto); - - // handle (delete then recreate) compositions - Database.Delete("WHERE childContentTypeId = @Id", new {entity.Id}); - foreach (var composition in entity.ContentTypeComposition) - Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); - - // removing a ContentType from a composition (U4-1690) - // 1. Find content based on the current ContentType: entity.Id - // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) - // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - if (entity.RemovedContentTypes.Any()) - { - // TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? - - // find Content based on the current ContentType - var sql = Sql() - .SelectAll() - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == Cms.Core.Constants.ObjectTypes.Document) - .Where(x => x.ContentTypeId == entity.Id); - var contentDtos = Database.Fetch(sql); - - // loop through all tracked keys, which corresponds to the ContentTypes that has been removed from the composition - foreach (var key in entity.RemovedContentTypes) + // loop through the Content that is based on the current ContentType in order to remove the Properties that are + // based on the PropertyTypes that belong to the removed ContentType. + foreach (ContentDto? contentDto in contentDtos) { - // find PropertyTypes for the removed ContentType - var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {Id = key}); - // loop through the Content that is based on the current ContentType in order to remove the Properties that are - // based on the PropertyTypes that belong to the removed ContentType. - foreach (var contentDto in contentDtos) + // TODO: This could be done with bulk SQL statements + foreach (PropertyTypeDto? propertyType in propertyTypes) { - // TODO: This could be done with bulk SQL statements - foreach (var propertyType in propertyTypes) - { - var nodeId = contentDto.NodeId; - var propertyTypeId = propertyType.Id; - var propertySql = Sql() - .Select(x => x.Id) - .From() - .InnerJoin() - .On((left, right) => left.PropertyTypeId == right.Id) - .InnerJoin() - .On((left, right) => left.VersionId == right.Id) - .Where(x => x.NodeId == nodeId) - .Where(x => x.Id == propertyTypeId); + var nodeId = contentDto.NodeId; + var propertyTypeId = propertyType.Id; + Sql propertySql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin() + .On((left, right) => left.PropertyTypeId == right.Id) + .InnerJoin() + .On((left, right) => left.VersionId == right.Id) + .Where(x => x.NodeId == nodeId) + .Where(x => x.Id == propertyTypeId); - // finally delete the properties that match our criteria for removing a ContentType from the composition - Database.Delete(new Sql("WHERE id IN (" + propertySql.SQL + ")", - propertySql.Arguments)); - } + // finally delete the properties that match our criteria for removing a ContentType from the composition + Database.Delete(new Sql( + "WHERE id IN (" + propertySql.SQL + ")", + propertySql.Arguments)); } } } + } - // delete the allowed content type entries before re-inserting the collection of allowed content types - Database.Delete("WHERE Id = @Id", new {entity.Id}); - if (entity.AllowedContentTypes is not null) + // delete the allowed content type entries before re-inserting the collection of allowed content types + Database.Delete("WHERE Id = @Id", new { entity.Id }); + if (entity.AllowedContentTypes is not null) + { + foreach (ContentTypeSort allowedContentType in entity.AllowedContentTypes) { - foreach (var allowedContentType in entity.AllowedContentTypes) + Database.Insert(new ContentTypeAllowedContentTypeDto { - Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); - } + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder, + }); } + } - // Delete property types ... by excepting entries from db with entries from collections. - // We check if the entity's own PropertyTypes has been modified and then also check - // any of the property groups PropertyTypes has been modified. - // This specifically tells us if any property type collections have changed. - if (entity.IsPropertyDirty("NoGroupPropertyTypes") || - entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + // Delete property types ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyTypes has been modified and then also check + // any of the property groups PropertyTypes has been modified. + // This specifically tells us if any property type collections have changed. + if (entity.IsPropertyDirty("NoGroupPropertyTypes") || + entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + { + List? dbPropertyTypes = + Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id }); + IEnumerable dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); + IEnumerable entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); + IEnumerable propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); + foreach (var propertyTypeId in propertyTypeToDeleteIds) { - var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {entity.Id}); - var dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); - var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); - var propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); - foreach (var propertyTypeId in propertyTypeToDeleteIds) - DeletePropertyType(entity.Id, propertyTypeId); + DeletePropertyType(entity.Id, propertyTypeId); } + } - // Delete tabs ... by excepting entries from db with entries from collections. - // We check if the entity's own PropertyGroups has been modified. - // This specifically tells us if the property group collections have changed. - List? orphanPropertyTypeIds = null; - if (entity.IsPropertyDirty("PropertyGroups")) - { - // TODO: we used to try to propagate tabs renaming downstream, relying on ParentId, but - // 1) ParentId makes no sense (if a tab can be inherited from multiple composition - // types) so we would need to figure things out differently, visiting downstream - // content types and looking for tabs with the same name... - // 2) It was not deployable as changing a content type changes other content types - // that was not deterministic, because it would depend on the order of the changes. - // That last point could be fixed if (1) is fixed, but then it still is an issue with - // deploy because changing a content type changes other content types that are not - // dependencies but dependents, and then what? - // - // So... for the time being, all renaming propagation is disabled. We just don't do it. - - // (all gone) - - // delete tabs that do not exist anymore - // get the tabs that are currently existing (in the db), get the tabs that we want, - // now, and derive the tabs that we want to delete - var existingPropertyGroups = Database - .Fetch("WHERE contentTypeNodeId = @id", new {id = entity.Id}) - .Select(x => x.Id) - .ToList(); - var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); - var groupsToDelete = existingPropertyGroups - .Except(newPropertyGroups) - .ToArray(); - - // delete the tabs - if (groupsToDelete.Length > 0) - { - // if the tab contains properties, take care of them - // - move them to 'generic properties' so they remain consistent - // - keep track of them, later on we'll figure out what to do with them - // see http://issues.umbraco.org/issue/U4-8663 - orphanPropertyTypeIds = Database.Fetch("WHERE propertyTypeGroupId IN (@ids)", - new {ids = groupsToDelete}) - .Select(x => x.Id).ToList(); - Database.Update( - "SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId IN (@ids)", - new {ids = groupsToDelete}); - - // now we can delete the tabs - Database.Delete("WHERE id IN (@ids)", new {ids = groupsToDelete}); - } - } - - // insert or update groups, assign properties - foreach (var propertyGroup in entity.PropertyGroups) - { - // insert or update group - var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); - var groupId = propertyGroup.HasIdentity - ? Database.Update(groupDto) - : Convert.ToInt32(Database.Insert(groupDto)); - if (propertyGroup.HasIdentity == false) - propertyGroup.Id = groupId; - else - groupId = propertyGroup.Id; - - // assign properties to the group - // (all of them, even those that have .IsPropertyDirty("PropertyGroupId") == true, - // because it should have been set to this group anyways and better be safe) - if (propertyGroup.PropertyTypes is not null) - { - foreach (var propertyType in propertyGroup.PropertyTypes) - { - propertyType.PropertyGroupId = new Lazy(() => groupId); - } - } - } - - //check if the content type variation has been changed - var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); - var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; - var newContentTypeVariation = entity.Variations; - var contentTypeVariationChanging = - contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; - if (contentTypeVariationChanging) - { - MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); - Clear301Redirects(entity); - ClearScheduledPublishing(entity); - } - - // collect property types that have a dirty variation - List? propertyTypeVariationDirty = null; - - // note: this only deals with *local* property types, we're dealing w/compositions later below - foreach (var propertyType in entity.PropertyTypes) - { - // track each property individually - if (propertyType.IsPropertyDirty("Variations")) - { - // allocate the list only when needed - if (propertyTypeVariationDirty == null) - propertyTypeVariationDirty = new List(); - - propertyTypeVariationDirty.Add(propertyType); - } - } - - // figure out dirty property types that have actually changed - // before we insert or update properties, so we can read the old variations - var propertyTypeVariationChanges = propertyTypeVariationDirty != null - ? GetPropertyVariationChanges(propertyTypeVariationDirty) - : null; - - // deal with composition property types - // add changes for property types obtained via composition, which change due - // to this content type variations change - if (contentTypeVariationChanging) - { - // must use RawComposedPropertyTypes here: only those types that are obtained - // via composition, with their original variations (ie not filtered by this - // content type variations - we need this true value to make decisions. - - propertyTypeVariationChanges = propertyTypeVariationChanges ?? - new Dictionary(); - - foreach (var composedPropertyType in entity.GetOriginalComposedPropertyTypes()) - { - if (composedPropertyType.Variations == ContentVariation.Nothing) continue; - - // Determine target variation of the composed property type. - // The composed property is only considered culture variant when the base content type is also culture variant. - // The composed property is only considered segment variant when the base content type is also segment variant. - // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture - var target = newContentTypeVariation & composedPropertyType.Variations; - // Determine the previous variation - // We have to compare with the old content type variation because the composed property might already have changed - // Example: A property with variations in an element type with variations is used in a document without - // when you enable variations the property has already enabled variations from the element type, - // but it's still a change from nothing because the document did not have variations, but it does now. - var from = oldContentTypeVariation & composedPropertyType.Variations; - - propertyTypeVariationChanges[composedPropertyType.Id] = (from, target); - } - } - - // insert or update properties - // all of them, no-group and in-groups - foreach (var propertyType in entity.PropertyTypes) - { - // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) - AssignDataTypeFromPropertyEditor(propertyType); - - // validate the alias - ValidateAlias(propertyType); - - // insert or update property - var groupId = propertyType.PropertyGroupId?.Value ?? default; - var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); - var typeId = propertyType.HasIdentity - ? Database.Update(propertyTypeDto) - : Convert.ToInt32(Database.Insert(propertyTypeDto)); - if (propertyType.HasIdentity == false) - propertyType.Id = typeId; - else - typeId = propertyType.Id; - - // not an orphan anymore - orphanPropertyTypeIds?.Remove(typeId); - } - - // must restrict property data changes to impacted content types - if changing a composing - // type, some composed types (those that do not vary) are not impacted and should be left - // unchanged + // Delete tabs ... by excepting entries from db with entries from collections. + // We check if the entity's own PropertyGroups has been modified. + // This specifically tells us if the property group collections have changed. + List? orphanPropertyTypeIds = null; + if (entity.IsPropertyDirty("PropertyGroups")) + { + // TODO: we used to try to propagate tabs renaming downstream, relying on ParentId, but + // 1) ParentId makes no sense (if a tab can be inherited from multiple composition + // types) so we would need to figure things out differently, visiting downstream + // content types and looking for tabs with the same name... + // 2) It was not deployable as changing a content type changes other content types + // that was not deterministic, because it would depend on the order of the changes. + // That last point could be fixed if (1) is fixed, but then it still is an issue with + // deploy because changing a content type changes other content types that are not + // dependencies but dependents, and then what? // - // getting 'all' from the cache policy is prone to race conditions - fast but dangerous - //var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); - var all = PerformGetAll(); + // So... for the time being, all renaming propagation is disabled. We just don't do it. - var impacted = GetImpactedContentTypes(entity, all); + // (all gone) - // if some property types have actually changed, move their variant data - if (propertyTypeVariationChanges?.Count > 0) - MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); + // delete tabs that do not exist anymore + // get the tabs that are currently existing (in the db), get the tabs that we want, + // now, and derive the tabs that we want to delete + var existingPropertyGroups = Database + .Fetch("WHERE contentTypeNodeId = @id", new { id = entity.Id }) + .Select(x => x.Id) + .ToList(); + var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); + var groupsToDelete = existingPropertyGroups + .Except(newPropertyGroups) + .ToArray(); - // deal with orphan properties: those that were in a deleted tab, - // and have not been re-mapped to another tab or to 'generic properties' - if (orphanPropertyTypeIds != null) - foreach (var id in orphanPropertyTypeIds) - DeletePropertyType(entity.Id, id); - - CommonRepository.ClearCache(); // always - } - - /// - /// Corrects the property type variations for the given entity - /// to make sure the property type variation is compatible with the - /// variation set on the entity itself. - /// - /// Entity to correct properties for - private void CorrectPropertyTypeVariations(IContentTypeComposition entity) - { - // Update property variations based on the content type variation - foreach (var propertyType in entity.PropertyTypes) + // delete the tabs + if (groupsToDelete.Length > 0) { - // Determine variation for the property type. - // The property is only considered culture variant when the base content type is also culture variant. - // The property is only considered segment variant when the base content type is also segment variant. - // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture - propertyType.Variations = entity.Variations & propertyType.Variations; + // if the tab contains properties, take care of them + // - move them to 'generic properties' so they remain consistent + // - keep track of them, later on we'll figure out what to do with them + // see http://issues.umbraco.org/issue/U4-8663 + orphanPropertyTypeIds = Database.Fetch( + "WHERE propertyTypeGroupId IN (@ids)", + new { ids = groupsToDelete }) + .Select(x => x.Id).ToList(); + Database.Update( + "SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId IN (@ids)", + new { ids = groupsToDelete }); + + // now we can delete the tabs + Database.Delete("WHERE id IN (@ids)", new { ids = groupsToDelete }); } } - /// - /// Ensures that no property types are flagged for a variance that is not supported by the content type itself - /// - /// The entity for which the property types will be validated - private void ValidateVariations(IContentTypeComposition entity) + // insert or update groups, assign properties + foreach (PropertyGroup propertyGroup in entity.PropertyGroups) { - foreach (var prop in entity.PropertyTypes) + // insert or update group + PropertyTypeGroupDto groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); + var groupId = propertyGroup.HasIdentity + ? Database.Update(groupDto) + : Convert.ToInt32(Database.Insert(groupDto)); + if (propertyGroup.HasIdentity == false) { - // The variation of a property is only allowed if all its variation flags - // are also set on the entity itself. It cannot set anything that is not also set by the content type. - // For example, when entity.Variations is set to Culture a property cannot be set to Segment. - var isValid = entity.Variations.HasFlag(prop.Variations); - if (!isValid) - throw new InvalidOperationException( - $"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); - } - } - - private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, - IEnumerable? all) - { - if (all is null) - { - return Enumerable.Empty(); - } - var impact = new List(); - var set = new List {contentType}; - - var tree = new Dictionary>(); - foreach (var x in all) - foreach (var y in x.ContentTypeComposition) - { - if (!tree.TryGetValue(y.Id, out var list)) - list = tree[y.Id] = new List(); - list.Add(x); - } - - var nset = new List(); - do - { - impact.AddRange(set); - - foreach (var x in set) - { - if (!tree.TryGetValue(x.Id, out var list)) continue; - nset.AddRange(list.Where(y => y.VariesByCulture())); - } - - set = nset; - nset = new List(); - } while (set.Count > 0); - - return impact; - } - - // gets property types that have actually changed, and the corresponding changes - // returns null if no property type has actually changed - private Dictionary? - GetPropertyVariationChanges(IEnumerable propertyTypes) - { - var propertyTypesL = propertyTypes.ToList(); - - // select the current variations (before the change) from database - var selectCurrentVariations = Sql() - .Select(x => x.Id, x => x.Variations) - .From() - .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); - - var oldVariations = Database.Dictionary(selectCurrentVariations); - - // build a dictionary of actual changes - Dictionary? changes = null; - - foreach (var propertyType in propertyTypesL) - { - // new property type, ignore - if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) - continue; - var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly - - // only those property types that *actually* changed - var newVariation = propertyType.Variations; - if (oldVariation == newVariation) - continue; - - // allocate the dictionary only when needed - if (changes == null) - changes = new Dictionary(); - - changes[propertyType.Id] = (oldVariation, newVariation); - } - - return changes; - } - - /// - /// Clear any redirects associated with content for a content type - /// - private void Clear301Redirects(IContentTypeComposition contentType) - { - //first clear out any existing property data that might already exists under the default lang - var sqlSelect = Sql().Select(x => x.UniqueId) - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - var sqlDelete = Sql() - .Delete() - .WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), - sqlSelect); - - Database.Execute(sqlDelete); - } - - /// - /// Clear any scheduled publishing associated with content for a content type - /// - private void ClearScheduledPublishing(IContentTypeComposition contentType) - { - // TODO: Fill this in when scheduled publishing is enabled for variants - } - - /// - /// Gets the default language identifier. - /// - private int GetDefaultLanguageId() - { - var selectDefaultLanguageId = Sql() - .Select(x => x.Id) - .From() - .Where(x => x.IsDefault); - - return Database.First(selectDefaultLanguageId); - } - - /// - /// Moves variant data for property type variation changes. - /// - private void MovePropertyTypeVariantData( - IDictionary propertyTypeChanges, - IEnumerable impacted) - { - var defaultLanguageId = GetDefaultLanguageId(); - var impactedL = impacted.Select(x => x.Id).ToList(); - - //Group by the "To" variation so we can bulk update in the correct batches - foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value)) - { - var propertyTypeIds = grouping.Select(x => x.Key).ToList(); - var (FromVariation, ToVariation) = grouping.Key; - - var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture); - var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture); - - if (!fromCultureEnabled && toCultureEnabled) - { - // Culture has been enabled - CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); - CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - } - else if (fromCultureEnabled && !toCultureEnabled) - { - // Culture has been disabled - CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); - CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - } - } - } - - /// - /// Moves variant data for a content type variation change. - /// - private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, - ContentVariation toVariation) - { - var defaultLanguageId = GetDefaultLanguageId(); - - var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture); - var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture); - - if (cultureIsNotEnabled && cultureWillBeEnabled) - { - //move the names - //first clear out any existing names that might already exists under the default lang - //there's 2x tables to update - - //clear out the versionCultureVariation table - var sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin() - .On(x => x.Id, x => x.VersionId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - var sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); - - Database.Execute(sqlDelete); - - //clear out the documentCultureVariation table - sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); - - Database.Execute(sqlDelete); - - //now we need to insert names into these 2 tables based on the invariant data - - //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang - var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLanguageId}") //default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - - Database.Execute(sqlInsert); - - //insert rows into the documentCultureVariation table - cols = Sql().ColumnsForInsert(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) - .AndSelect(x => x.Text) - .Append($", 1, {defaultLanguageId}") //make Available + default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - - Database.Execute(sqlInsert); + propertyGroup.Id = groupId; } else { - //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. - - //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( - // if we want these SQL statements back, look into GIT history + groupId = propertyGroup.Id; } - } - /// - private void CopyTagData( - int? sourceLanguageId, - int? targetLanguageId, - IReadOnlyCollection propertyTypeIds, - IReadOnlyCollection? contentTypeIds = null) - { - // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers - - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); - - // delete existing relations (for target language) - // do *not* delete existing tags - - var sqlSelectTagsToDelete = Sql() - .Select(x => x.Id) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); - - if (contentTypeIds != null) - sqlSelectTagsToDelete - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectTagsToDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); - - var sqlDeleteRelations = Sql() - .Delete() - .WhereIn(x => x.TagId, sqlSelectTagsToDelete); - - Database.Execute(sqlDeleteRelations); - - // do *not* delete the tags - they could be used by other content types / property types - /* - var sqlDeleteTag = Sql() - .Delete() - .WhereIn(x => x.Id, sqlTagToDelete); - Database.Execute(sqlDeleteTag); - */ - - // copy tags from source language to target language - // target tags may exist already, so we have to check for existence here - // - // select tags to insert: tags pointed to by a relation ship, for proper property/content types, - // and of source language, and where we cannot left join to an existing tag with same text, - // group and languageId - - var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; - var sqlSelectTagsToInsert = Sql() - .SelectDistinct(x => x.Text, x => x.Group) - .Append(", " + targetLanguageIdS) - .From(); - - sqlSelectTagsToInsert - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .LeftJoin("xtags") - .On( - (tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && - xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); - - if (contentTypeIds != null) - sqlSelectTagsToInsert - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectTagsToInsert - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .WhereNull(x => x.Id, "xtags") // ie, not exists - .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); - - var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); - var sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); - - Database.Execute(sqlInsertTags); - - // create relations to new tags - // any existing relations have been deleted above, no need to check for existence here - // - // select node id and property type id from existing relations to tags of source language, - // for proper property/content types, and select new tag id from tags, with matching text, - // and group, but for the target language - - var sqlSelectRelationsToInsert = Sql() - .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) - .AndSelect("otag", x => x.Id) - .From() - .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) - .InnerJoin("otag") - .On( - (tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && - otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); - - if (contentTypeIds != null) - sqlSelectRelationsToInsert - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectRelationsToInsert - .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - var relationColumnsToInsert = Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); - var sqlInsertRelations = Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})").Append(sqlSelectRelationsToInsert); - - Database.Execute(sqlInsertRelations); - - // delete original relations - *not* the tags - all of them - // cannot really "go back" with relations, would have to do it with property values - - sqlSelectTagsToDelete = Sql() - .Select(x => x.Id) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); - - if (contentTypeIds != null) - sqlSelectTagsToDelete - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - sqlSelectTagsToDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds) - .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); - - sqlDeleteRelations = Sql() - .Delete() - .WhereIn(x => x.TagId, sqlSelectTagsToDelete); - - Database.Execute(sqlDeleteRelations); - - // no - /* - var sqlDeleteTag = Sql() - .Delete() - .WhereIn(x => x.Id, sqlTagToDelete); - Database.Execute(sqlDeleteTag); - */ - } - - /// - /// Copies property data from one language to another. - /// - /// The source language (can be null ie invariant). - /// The target language (can be null ie invariant) - /// The property type identifiers. - /// The content type identifiers. - private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, - IReadOnlyCollection propertyTypeIds, IReadOnlyCollection? contentTypeIds = null) - { - // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers - // - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); - - //first clear out any existing property data that might already exists under the target language - var sqlDelete = Sql() - .Delete(); - - // not ok for SqlCe (no JOIN in DELETE) - //if (contentTypeIds != null) - // sqlDelete - // .From() - // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) - // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); - - Sql? inSql = null; - if (contentTypeIds != null) + // assign properties to the group + // (all of them, even those that have .IsPropertyDirty("PropertyGroupId") == true, + // because it should have been set to this group anyways and better be safe) + if (propertyGroup.PropertyTypes is not null) { - inSql = Sql() - .Select(x => x.Id) - .From() - .InnerJoin() - .On((cversion, c) => cversion.NodeId == c.NodeId) - .WhereIn(x => x.ContentTypeId, contentTypeIds); - sqlDelete.WhereIn(x => x.VersionId, inSql); + foreach (IPropertyType propertyType in propertyGroup.PropertyTypes) + { + propertyType.PropertyGroupId = new Lazy(() => groupId); + } + } + } + + // check if the content type variation has been changed + var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); + var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; + ContentVariation newContentTypeVariation = entity.Variations; + var contentTypeVariationChanging = + contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; + if (contentTypeVariationChanging) + { + MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); + Clear301Redirects(entity); + ClearScheduledPublishing(entity); + } + + // collect property types that have a dirty variation + List? propertyTypeVariationDirty = null; + + // note: this only deals with *local* property types, we're dealing w/compositions later below + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // track each property individually + if (propertyType.IsPropertyDirty("Variations")) + { + // allocate the list only when needed + if (propertyTypeVariationDirty == null) + { + propertyTypeVariationDirty = new List(); + } + + propertyTypeVariationDirty.Add(propertyType); + } + } + + // figure out dirty property types that have actually changed + // before we insert or update properties, so we can read the old variations + Dictionary? propertyTypeVariationChanges = + propertyTypeVariationDirty != null + ? GetPropertyVariationChanges(propertyTypeVariationDirty) + : null; + + // deal with composition property types + // add changes for property types obtained via composition, which change due + // to this content type variations change + if (contentTypeVariationChanging) + { + // must use RawComposedPropertyTypes here: only those types that are obtained + // via composition, with their original variations (ie not filtered by this + // content type variations - we need this true value to make decisions. + propertyTypeVariationChanges ??= new Dictionary(); + + foreach (IPropertyType composedPropertyType in entity.GetOriginalComposedPropertyTypes()) + { + if (composedPropertyType.Variations == ContentVariation.Nothing) + { + continue; + } + + // Determine target variation of the composed property type. + // The composed property is only considered culture variant when the base content type is also culture variant. + // The composed property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + ContentVariation target = newContentTypeVariation & composedPropertyType.Variations; + + // Determine the previous variation + // We have to compare with the old content type variation because the composed property might already have changed + // Example: A property with variations in an element type with variations is used in a document without + // when you enable variations the property has already enabled variations from the element type, + // but it's still a change from nothing because the document did not have variations, but it does now. + ContentVariation from = oldContentTypeVariation & composedPropertyType.Variations; + + propertyTypeVariationChanges[composedPropertyType.Id] = (from, target); + } + } + + // insert or update properties + // all of them, no-group and in-groups + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) + { + AssignDataTypeFromPropertyEditor(propertyType); } - sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + // validate the alias + ValidateAlias(propertyType); - sqlDelete - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + // insert or update property + var groupId = propertyType.PropertyGroupId?.Value ?? default; + PropertyTypeDto propertyTypeDto = + PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); + var typeId = propertyType.HasIdentity + ? Database.Update(propertyTypeDto) + : Convert.ToInt32(Database.Insert(propertyTypeDto)); + if (propertyType.HasIdentity == false) + { + propertyType.Id = typeId; + } + else + { + typeId = propertyType.Id; + } - // see note above, not ok for SqlCe - //if (contentTypeIds != null) - // sqlDelete - // .WhereIn(x => x.ContentTypeId, contentTypeIds); + // not an orphan anymore + orphanPropertyTypeIds?.Remove(typeId); + } + + // must restrict property data changes to impacted content types - if changing a composing + // type, some composed types (those that do not vary) are not impacted and should be left + // unchanged + // + // getting 'all' from the cache policy is prone to race conditions - fast but dangerous + // var all = ((FullDataSetRepositoryCachePolicy)CachePolicy).GetAllCached(PerformGetAll); + IEnumerable? all = PerformGetAll(); + + IEnumerable impacted = GetImpactedContentTypes(entity, all); + + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges?.Count > 0) + { + MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); + } + + // deal with orphan properties: those that were in a deleted tab, + // and have not been re-mapped to another tab or to 'generic properties' + if (orphanPropertyTypeIds != null) + { + foreach (var id in orphanPropertyTypeIds) + { + DeletePropertyType(entity.Id, id); + } + } + + CommonRepository.ClearCache(); // always + } + + protected void ValidateAlias(IPropertyType pt) + { + if (string.IsNullOrWhiteSpace(pt.Alias)) + { + var ex = new InvalidOperationException( + $"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + + Logger.LogError( + "Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + pt.Name); + + throw ex; + } + } + + private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) => + (!pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue) + || (!pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue) + || (pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue) + || (pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue) + || (pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue); + + /// + /// Corrects the property type variations for the given entity + /// to make sure the property type variation is compatible with the + /// variation set on the entity itself. + /// + /// Entity to correct properties for + private void CorrectPropertyTypeVariations(IContentTypeComposition entity) + { + // Update property variations based on the content type variation + foreach (IPropertyType propertyType in entity.PropertyTypes) + { + // Determine variation for the property type. + // The property is only considered culture variant when the base content type is also culture variant. + // The property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + propertyType.Variations = entity.Variations & propertyType.Variations; + } + } + + /// + /// Ensures that no property types are flagged for a variance that is not supported by the content type itself + /// + /// The entity for which the property types will be validated + private void ValidateVariations(IContentTypeComposition entity) + { + foreach (IPropertyType prop in entity.PropertyTypes) + { + // The variation of a property is only allowed if all its variation flags + // are also set on the entity itself. It cannot set anything that is not also set by the content type. + // For example, when entity.Variations is set to Culture a property cannot be set to Segment. + var isValid = entity.Variations.HasFlag(prop.Variations); + if (!isValid) + { + throw new InvalidOperationException( + $"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); + } + } + } + + private IEnumerable GetImpactedContentTypes( + IContentTypeComposition contentType, + IEnumerable? all) + { + if (all is null) + { + return Enumerable.Empty(); + } + + var impact = new List(); + var set = new List { contentType }; + + var tree = new Dictionary>(); + foreach (IContentTypeComposition x in all) + { + foreach (IContentTypeComposition y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out List? list)) + { + list = tree[y.Id] = new List(); + } + + list.Add(x); + } + } + + var nset = new List(); + do + { + impact.AddRange(set); + + foreach (IContentTypeComposition x in set) + { + if (!tree.TryGetValue(x.Id, out List? list)) + { + continue; + } + + nset.AddRange(list.Where(y => y.VariesByCulture())); + } + + set = nset; + nset = new List(); + } + while (set.Count > 0); + + return impact; + } + + // gets property types that have actually changed, and the corresponding changes + // returns null if no property type has actually changed + private Dictionary? + GetPropertyVariationChanges(IEnumerable propertyTypes) + { + var propertyTypesL = propertyTypes.ToList(); + + // select the current variations (before the change) from database + Sql selectCurrentVariations = Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); + + Dictionary? oldVariations = Database.Dictionary(selectCurrentVariations); + + // build a dictionary of actual changes + Dictionary? changes = null; + + foreach (IPropertyType propertyType in propertyTypesL) + { + // new property type, ignore + if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) + { + continue; + } + + var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly + + // only those property types that *actually* changed + ContentVariation newVariation = propertyType.Variations; + if (oldVariation == newVariation) + { + continue; + } + + // allocate the dictionary only when needed + if (changes == null) + { + changes = new Dictionary(); + } + + changes[propertyType.Id] = (oldVariation, newVariation); + } + + return changes; + } + + /// + /// Clear any redirects associated with content for a content type + /// + private void Clear301Redirects(IContentTypeComposition contentType) + { + // first clear out any existing property data that might already exists under the default lang + Sql sqlSelect = Sql().Select(x => x.UniqueId) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + Sql sqlDelete = Sql() + .Delete() + .WhereIn( + (Expression>)(x => x.ContentKey), + sqlSelect); + + Database.Execute(sqlDelete); + } + + /// + /// Clear any scheduled publishing associated with content for a content type + /// + private void ClearScheduledPublishing(IContentTypeComposition contentType) + { + // TODO: Fill this in when scheduled publishing is enabled for variants + } + + /// + /// Gets the default language identifier. + /// + private int GetDefaultLanguageId() + { + Sql selectDefaultLanguageId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + return Database.First(selectDefaultLanguageId); + } + + /// + /// Moves variant data for property type variation changes. + /// + private void MovePropertyTypeVariantData( + IDictionary propertyTypeChanges, + IEnumerable impacted) + { + var defaultLanguageId = GetDefaultLanguageId(); + var impactedL = impacted.Select(x => x.Id).ToList(); + + // Group by the "To" variation so we can bulk update in the correct batches + foreach (IGrouping<(ContentVariation FromVariation, ContentVariation ToVariation), + KeyValuePair> grouping in + propertyTypeChanges.GroupBy(x => x.Value)) + { + var propertyTypeIds = grouping.Select(x => x.Key).ToList(); + (ContentVariation FromVariation, ContentVariation ToVariation) = grouping.Key; + + var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture); + var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture); + + if (!fromCultureEnabled && toCultureEnabled) + { + // Culture has been enabled + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); + CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); + } + else if (fromCultureEnabled && !toCultureEnabled) + { + // Culture has been disabled + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); + CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); + } + } + } + + /// + /// Moves variant data for a content type variation change. + /// + private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, + ContentVariation toVariation) + { + var defaultLanguageId = GetDefaultLanguageId(); + + var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture); + var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture); + + if (cultureIsNotEnabled && cultureWillBeEnabled) + { + // move the names + // first clear out any existing names that might already exists under the default lang + // there's 2x tables to update + + // clear out the versionCultureVariation table + Sql sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin() + .On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + Sql sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); Database.Execute(sqlDelete); - //now insert all property data into the target language that exists under the source language - var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; - var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append(", " + targetLanguageIdS) //default language ID - .From(); + // clear out the documentCultureVariation table + sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); - if (contentTypeIds != null) - sqlSelectData - .InnerJoin() - .On((pdata, cversion) => pdata.VersionId == cversion.Id) - .InnerJoin() - .On((cversion, c) => cversion.NodeId == c.NodeId); + Database.Execute(sqlDelete); - sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + // now we need to insert names into these 2 tables based on the invariant data - sqlSelectData - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - if (contentTypeIds != null) - sqlSelectData - .WhereIn(x => x.ContentTypeId, contentTypeIds); - - var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + // insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.Name, + x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) + .Append($", {defaultLanguageId}") // default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + Sql? sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})") + .Append(sqlSelect); Database.Execute(sqlInsert); - // when copying from Culture, keep the original values around in case we want to go back - // when copying from Nothing, kill the original values, we don't want them around - if (sourceLanguageId == null) - { - sqlDelete = Sql() - .Delete(); + // insert rows into the documentCultureVariation table + cols = Sql().ColumnsForInsert(x => x.NodeId, x => x.Edited, x => x.Published, + x => x.Name, x => x.Available, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) + .AndSelect(x => x.Text) + .Append($", 1, {defaultLanguageId}") // make Available + default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - if (contentTypeIds != null) - sqlDelete.WhereIn(x => x.VersionId, inSql); - - sqlDelete - .Where(x => x.LanguageId == null) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - Database.Execute(sqlDelete); - } - } - - /// - /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are changed - /// - /// - /// - /// - /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false - /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each - /// property, culture and current/published version. - /// - private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, - IReadOnlyCollection? contentTypeIds = null) - { - var defaultLang = LanguageRepository.GetDefaultId(); - - //This will build up a query to get the property values of both the current and the published version so that we can check - //based on the current variance of each item to see if it's 'edited' value should be true/false. - - var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); - if (whereInArgsCount > Constants.Sql.MaxParameterCount) - throw new NotSupportedException("Too many property/content types."); - - var propertySql = Sql() - .Select() - .AndSelect(x => x.NodeId, x => x.Current) - .AndSelect(x => x.Published) - .AndSelect(x => x.Variations) - .From() - .InnerJoin() - .On((left, right) => left.Id == right.VersionId) - .InnerJoin() - .On((left, right) => left.Id == right.PropertyTypeId); - - if (contentTypeIds != null) - { - propertySql.InnerJoin() - .On((c, cversion) => c.NodeId == cversion.NodeId); - } - - propertySql.LeftJoin() - .On((docversion, cversion) => cversion.Id == docversion.Id) - .Where((docversion, cversion) => - cversion.Current || docversion.Published) - .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - if (contentTypeIds != null) - { - propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); - } - - propertySql - .OrderBy(x => x.NodeId) - .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); - - //keep track of this node/lang to mark or unmark a culture as edited - var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); - //keep track of which node to mark or unmark as edited - var editedDocument = new Dictionary(); - var nodeId = -1; - var propertyTypeId = -1; - - PropertyValueVersionDto? pubRow = null; - - //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. - //Published data will always come before Current data based on the version id sort. - //There will only be one published row (max) and one current row per property. - foreach (var row in Database.Query(propertySql)) - { - //make sure to reset on each node/property change - if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) - { - nodeId = row.NodeId; - propertyTypeId = row.PropertyTypeId; - pubRow = null; - } - - if (row.Published) - pubRow = row; - - if (row.Current) - { - var propVariations = (ContentVariation)row.Variations; - - //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited - if (!propVariations.VariesByCulture() && row.LanguageId.HasValue - || propVariations.VariesByCulture() && !row.LanguageId.HasValue) - { - //Flag this as not edited for this node/lang if the key doesn't exist - if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) - editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); - - //mark as false if the item doesn't exist, else coerce to true - editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) - ? (edited |= false) - : false; - } - else if (pubRow == null) - { - //this would mean that that this property is 'edited' since there is no published version - editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; - editedDocument[row.NodeId] = true; - } - //compare the property values, if they differ from versions then flag the current version as edited - else if (IsPropertyValueChanged(pubRow, row)) - { - //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang - editedLanguageVersions[ - (row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; - editedDocument[row.NodeId] = true; - } - - //reset - pubRow = null; - } - } - - // lookup all matching rows in umbracoDocumentCultureVariation - // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) - var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); - var nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); - var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) - .SelectMany(group => - { - var sql = Sql().Select().From() - .WhereIn(x => x.LanguageId, languageIds) - .WhereIn(x => x.NodeId, group); - - return Database.Fetch(sql); - }) - .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), - x => x); //convert to dictionary with the same key type - - var toUpdate = new List(); - foreach (var ev in editedLanguageVersions) - { - if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) - { - //check if it needs updating - if (docVariations.Edited != ev.Value) - { - docVariations.Edited = ev.Value; - toUpdate.Add(docVariations); - } - } - else if (ev.Key.langId.HasValue) - { - //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level - throw new PanicException( - $"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); - } - } - - //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false - foreach (var editValue in toUpdate.GroupBy(x => x.Edited)) - { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.Id, editValue.Select(x => x.Id))); - } - - //Now bulk update the umbracoDocument table - foreach (var editValue in editedDocument.GroupBy(x => x.Value)) - { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); - } - } - - private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) - { - return !pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue - || !pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue - || pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue - || pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue - || pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue; - } - - private class NameCompareDto - { - public int NodeId { get; set; } - public int CurrentVersion { get; set; } - public int LanguageId { get; set; } - public string? CurrentName { get; set; } - public string? PublishedName { get; set; } - public int? PublishedVersion { get; set; } - public int Id { get; set; } // the Id of the DocumentCultureVariationDto - public bool Edited { get; set; } - } - - private class PropertyValueVersionDto - { - public int VersionId { get; set; } - public int PropertyTypeId { get; set; } - public int? LanguageId { get; set; } - public string? Segment { get; set; } - public int? IntValue { get; set; } - - private decimal? _decimalValue; - - [Column("decimalValue")] - public decimal? DecimalValue - { - get => _decimalValue; - set => _decimalValue = value?.Normalize(); - } - - public DateTime? DateValue { get; set; } - public string? VarcharValue { get; set; } - public string? TextValue { get; set; } - - public int NodeId { get; set; } - public bool Current { get; set; } - public bool Published { get; set; } - - public byte Variations { get; set; } - } - - private void DeletePropertyType(int contentTypeId, int propertyTypeId) - { - // first clear dependencies - Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); - Database.Delete("WHERE propertyTypeId = @Id", new {Id = propertyTypeId}); - - // then delete the property type - Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", - new {Id = contentTypeId, PropertyTypeId = propertyTypeId}); - } - - protected void ValidateAlias(IPropertyType pt) - { - if (string.IsNullOrWhiteSpace(pt.Alias)) - { - var ex = new InvalidOperationException( - $"Property Type '{pt.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - - Logger.LogError( - "Property Type '{PropertyTypeName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - pt.Name); - - throw ex; - } - } - - protected void ValidateAlias(TEntity entity) - { - if (string.IsNullOrWhiteSpace(entity.Alias)) - { - var ex = new InvalidOperationException( - $"{typeof(TEntity).Name} '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); - - Logger.LogError( - "{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", - typeof(TEntity).Name, - entity.Name); - - throw ex; - } - } - - /// - /// Try to set the data type id based on its ControlId - /// - /// - private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType) - { - //we cannot try to assign a data type of it's empty - if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) - { - var sql = Sql() - .Select(dt => dt.Select(x => x.NodeDto)) - .From() - .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) - .Where("propertyEditorAlias = @propertyEditorAlias", - new {propertyEditorAlias = propertyType.PropertyEditorAlias}) - .OrderBy(typeDto => typeDto.NodeId); - var datatype = Database.FirstOrDefault(sql); - //we cannot assign a data type if one was not found - if (datatype != null) - { - propertyType.DataTypeId = datatype.NodeId; - propertyType.DataTypeKey = datatype.NodeDto.UniqueId; - } - else - { - Logger.LogWarning( - "Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", - propertyType.Alias, propertyType.PropertyEditorAlias); - } - } - } - - protected abstract TEntity? PerformGet(Guid id); - protected abstract TEntity? PerformGet(string alias); - protected abstract IEnumerable? PerformGetAll(params Guid[]? ids); - protected abstract bool PerformExists(Guid id); - - /// - /// Gets an Entity by alias - /// - /// - /// - public TEntity? Get(string alias) - { - return PerformGet(alias); - } - - /// - /// Gets an Entity by Id - /// - /// - /// - public TEntity? Get(Guid id) - { - return PerformGet(id); - } - - /// - /// Gets all entities of the specified type - /// - /// - /// - /// - /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same signature as the main GetAll when there are no parameters - /// - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return PerformGetAll(ids) ?? Enumerable.Empty(); - } - - /// - /// Boolean indicating whether an Entity with the specified Id exists - /// - /// - /// - public bool Exists(Guid id) - { - return PerformExists(id); - } - - public string GetUniqueAlias(string alias) - { - // alias is unique across ALL content types! - var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); - var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType -INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id -WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", - new {pattern = alias + "%", objectType = NodeObjectTypeId}); - var i = 1; - string test; - while (aliases.Contains(test = alias + i)) i++; - return test; - } - - /// - public bool HasContainerInPath(string contentPath) - { - var ids = contentPath.Split(Constants.CharArrays.Comma) - .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray(); - return HasContainerInPath(ids); - } - - /// - public bool HasContainerInPath(params int[] ids) - { - var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType -INNER JOIN {Cms.Core.Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Cms.Core.Constants.DatabaseSchema.Tables.Content}.contentTypeId -WHERE {Cms.Core.Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", - new {ids, isContainer = true}); - return Database.ExecuteScalar(sql) > 0; - } - - /// - /// Returns true or false depending on whether content nodes have been created based on the provided content type id. - /// - public bool HasContentNodes(int id) - { - var sql = new Sql( - $"SELECT CASE WHEN EXISTS (SELECT * FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END", - new {id}); - return Database.ExecuteScalar(sql) == 1; - } - - protected override IEnumerable GetDeleteClauses() - { - // in theory, services should have ensured that content items of the given content type - // have been deleted and therefore PropertyData has been cleared, so PropertyData - // is included here just to be 100% sure since it has a FK on cmsPropertyType. - - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id", - "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id", - "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id", - "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + - " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyType + - " WHERE contentTypeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup + - " WHERE contenttypeNodeId = @id" - }; - return list; + Database.Execute(sqlInsert); } } + + /// + private void CopyTagData( + int? sourceLanguageId, + int? targetLanguageId, + IReadOnlyCollection propertyTypeIds, + IReadOnlyCollection? contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } + + // delete existing relations (for target language) + // do *not* delete existing tags + Sql sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + { + sqlSelectTagsToDelete + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + Sql sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // do *not* delete the tags - they could be used by other content types / property types + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + + // copy tags from source language to target language + // target tags may exist already, so we have to check for existence here + // + // select tags to insert: tags pointed to by a relation ship, for proper property/content types, + // and of source language, and where we cannot left join to an existing tag with same text, + // group and languageId + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + Sql sqlSelectTagsToInsert = Sql() + .SelectDistinct(x => x.Text, x => x.Group) + .Append(", " + targetLanguageIdS) + .From(); + + sqlSelectTagsToInsert + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .LeftJoin("xtags") + .On( + (tag, xtag) => tag.Text == xtag.Text && tag.Group == xtag.Group && + xtag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "xtags"); + + if (contentTypeIds != null) + { + sqlSelectTagsToInsert + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectTagsToInsert + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .WhereNull(x => x.Id, "xtags") // ie, not exists + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + + var cols = Sql().ColumnsForInsert(x => x.Text, x => x.Group, x => x.LanguageId); + Sql? sqlInsertTags = Sql($"INSERT INTO {TagDto.TableName} ({cols})").Append(sqlSelectTagsToInsert); + + Database.Execute(sqlInsertTags); + + // create relations to new tags + // any existing relations have been deleted above, no need to check for existence here + // + // select node id and property type id from existing relations to tags of source language, + // for proper property/content types, and select new tag id from tags, with matching text, + // and group, but for the target language + Sql sqlSelectRelationsToInsert = Sql() + .SelectDistinct(x => x.NodeId, x => x.PropertyTypeId) + .AndSelect("otag", x => x.Id) + .From() + .InnerJoin().On((rel, tag) => rel.TagId == tag.Id) + .InnerJoin("otag") + .On( + (tag, otag) => tag.Text == otag.Text && tag.Group == otag.Group && + otag.LanguageId.SqlNullableEquals(targetLanguageId, -1), aliasRight: "otag"); + + if (contentTypeIds != null) + { + sqlSelectRelationsToInsert + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectRelationsToInsert + .Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + var relationColumnsToInsert = + Sql().ColumnsForInsert(x => x.NodeId, x => x.PropertyTypeId, x => x.TagId); + Sql? sqlInsertRelations = + Sql($"INSERT INTO {TagRelationshipDto.TableName} ({relationColumnsToInsert})") + .Append(sqlSelectRelationsToInsert); + + Database.Execute(sqlInsertRelations); + + // delete original relations - *not* the tags - all of them + // cannot really "go back" with relations, would have to do it with property values + sqlSelectTagsToDelete = Sql() + .Select(x => x.Id) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId); + + if (contentTypeIds != null) + { + sqlSelectTagsToDelete + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + sqlSelectTagsToDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds) + .Where(x => !x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + sqlDeleteRelations = Sql() + .Delete() + .WhereIn(x => x.TagId, sqlSelectTagsToDelete); + + Database.Execute(sqlDeleteRelations); + + // no + /* + var sqlDeleteTag = Sql() + .Delete() + .WhereIn(x => x.Id, sqlTagToDelete); + Database.Execute(sqlDeleteTag); + */ + } + + /// + /// Copies property data from one language to another. + /// + /// The source language (can be null ie invariant). + /// The target language (can be null ie invariant) + /// The property type identifiers. + /// The content type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, + IReadOnlyCollection propertyTypeIds, IReadOnlyCollection? contentTypeIds = null) + { + // note: important to use SqlNullableEquals for nullable types, cannot directly compare language identifiers + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } + + // first clear out any existing property data that might already exists under the target language + Sql sqlDelete = Sql() + .Delete(); + + // not ok for SqlCe (no JOIN in DELETE) + // if (contentTypeIds != null) + // sqlDelete + // .From() + // .InnerJoin().On((pdata, cversion) => pdata.VersionId == cversion.Id) + // .InnerJoin().On((cversion, c) => cversion.NodeId == c.NodeId); + Sql? inSql = null; + if (contentTypeIds != null) + { + inSql = Sql() + .Select(x => x.Id) + .From() + .InnerJoin() + .On((cversion, c) => cversion.NodeId == c.NodeId) + .WhereIn(x => x.ContentTypeId, contentTypeIds); + sqlDelete.WhereIn(x => x.VersionId, inSql); + } + + sqlDelete.Where(x => x.LanguageId.SqlNullableEquals(targetLanguageId, -1)); + + sqlDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + // see note above, not ok for SqlCe + // if (contentTypeIds != null) + // sqlDelete + // .WhereIn(x => x.ContentTypeId, contentTypeIds); + Database.Execute(sqlDelete); + + // now insert all property data into the target language that exists under the source language + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; + var cols = Sql().ColumnsForInsert(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, + x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, + x => x.LanguageId); + Sql sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, + x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, + x => x.TextValue) + .Append(", " + targetLanguageIdS) // default language ID + .From(); + + if (contentTypeIds != null) + { + sqlSelectData + .InnerJoin() + .On((pdata, cversion) => pdata.VersionId == cversion.Id) + .InnerJoin() + .On((cversion, c) => cversion.NodeId == c.NodeId); + } + + sqlSelectData.Where(x => x.LanguageId.SqlNullableEquals(sourceLanguageId, -1)); + + sqlSelectData + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + { + sqlSelectData + .WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + Sql? sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); + + Database.Execute(sqlInsert); + + // when copying from Culture, keep the original values around in case we want to go back + // when copying from Nothing, kill the original values, we don't want them around + if (sourceLanguageId == null) + { + sqlDelete = Sql() + .Delete(); + + if (contentTypeIds != null) + { + sqlDelete.WhereIn(x => x.VersionId, inSql); + } + + sqlDelete + .Where(x => x.LanguageId == null) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + Database.Execute(sqlDelete); + } + } + + /// + /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are + /// changed + /// + /// + /// + /// + /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true + /// when it should be false + /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values + /// stored for each + /// property, culture and current/published version. + /// + private void RenormalizeDocumentEditedFlags( + IReadOnlyCollection propertyTypeIds, + IReadOnlyCollection? contentTypeIds = null) + { + var defaultLang = LanguageRepository.GetDefaultId(); + + // This will build up a query to get the property values of both the current and the published version so that we can check + // based on the current variance of each item to see if it's 'edited' value should be true/false. + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > Constants.Sql.MaxParameterCount) + { + throw new NotSupportedException("Too many property/content types."); + } + + Sql propertySql = Sql() + .Select() + .AndSelect(x => x.NodeId, x => x.Current) + .AndSelect(x => x.Published) + .AndSelect(x => x.Variations) + .From() + .InnerJoin() + .On((left, right) => left.Id == right.VersionId) + .InnerJoin() + .On((left, right) => left.Id == right.PropertyTypeId); + + if (contentTypeIds != null) + { + propertySql.InnerJoin() + .On((c, cversion) => c.NodeId == cversion.NodeId); + } + + propertySql.LeftJoin() + .On((docversion, cversion) => cversion.Id == docversion.Id) + .Where((docversion, cversion) => + cversion.Current || docversion.Published) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + { + propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + propertySql + .OrderBy(x => x.NodeId) + .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); + + // keep track of this node/lang to mark or unmark a culture as edited + var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); + + // keep track of which node to mark or unmark as edited + var editedDocument = new Dictionary(); + var nodeId = -1; + var propertyTypeId = -1; + + PropertyValueVersionDto? pubRow = null; + + // This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + // Published data will always come before Current data based on the version id sort. + // There will only be one published row (max) and one current row per property. + foreach (PropertyValueVersionDto? row in Database.Query(propertySql)) + { + // make sure to reset on each node/property change + if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) + { + nodeId = row.NodeId; + propertyTypeId = row.PropertyTypeId; + pubRow = null; + } + + if (row.Published) + { + pubRow = row; + } + + if (row.Current) + { + var propVariations = (ContentVariation)row.Variations; + + // if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited + if ((!propVariations.VariesByCulture() && row.LanguageId.HasValue) + || (propVariations.VariesByCulture() && !row.LanguageId.HasValue)) + { + // Flag this as not edited for this node/lang if the key doesn't exist + if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) + { + editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); + } + + // mark as false if the item doesn't exist, else coerce to true + editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) + ? edited |= false + : false; + } + else if (pubRow == null) + { + // this would mean that that this property is 'edited' since there is no published version + editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + + // compare the property values, if they differ from versions then flag the current version as edited + else if (IsPropertyValueChanged(pubRow, row)) + { + // Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang + editedLanguageVersions[ + (row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + + // reset + pubRow = null; + } + } + + // lookup all matching rows in umbracoDocumentCultureVariation + // fetch in batches to account for maximum parameter count (distinct languages can't exceed 2000) + var languageIds = editedLanguageVersions.Keys.Select(x => x.langId).Distinct().ToArray(); + IEnumerable nodeIds = editedLanguageVersions.Keys.Select(x => x.nodeId).Distinct(); + var docCultureVariationsToUpdate = nodeIds.InGroupsOf(Constants.Sql.MaxParameterCount - languageIds.Length) + .SelectMany(group => + { + Sql sql = Sql().Select().From() + .WhereIn(x => x.LanguageId, languageIds) + .WhereIn(x => x.NodeId, group); + + return Database.Fetch(sql); + }) + .ToDictionary( + x => (x.NodeId, (int?)x.LanguageId), + x => x); // convert to dictionary with the same key type + + var toUpdate = new List(); + foreach (KeyValuePair<(int nodeId, int? langId), bool> ev in editedLanguageVersions) + { + if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out DocumentCultureVariationDto? docVariations)) + { + // check if it needs updating + if (docVariations.Edited != ev.Value) + { + docVariations.Edited = ev.Value; + toUpdate.Add(docVariations); + } + } + else if (ev.Key.langId.HasValue) + { + // This should never happen! If a property culture is flagged as edited then the culture must exist at the document level + throw new PanicException( + $"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); + } + } + + // Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false + foreach (IGrouping editValue in toUpdate.GroupBy(x => x.Edited)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, editValue.Select(x => x.Id))); + } + + // Now bulk update the umbracoDocument table + foreach (IGrouping> editValue in editedDocument.GroupBy(x => x.Value)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); + } + } + + private void DeletePropertyType(int contentTypeId, int propertyTypeId) + { + // first clear dependencies + Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId }); + Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId }); + + // then delete the property type + Database.Delete( + "WHERE contentTypeId = @Id AND id = @PropertyTypeId", + new { Id = contentTypeId, PropertyTypeId = propertyTypeId }); + } + + protected void ValidateAlias(TEntity entity) + { + if (string.IsNullOrWhiteSpace(entity.Alias)) + { + var ex = new InvalidOperationException( + $"{typeof(TEntity).Name} '{entity.Name}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias."); + + Logger.LogError( + "{EntityTypeName} '{EntityName}' cannot have an empty Alias. This is most likely due to invalid characters stripped from the Alias.", + typeof(TEntity).Name, + entity.Name); + + throw ex; + } + } + + protected abstract TEntity? PerformGet(Guid id); + + /// + /// Try to set the data type id based on its ControlId + /// + /// + private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType) + { + // we cannot try to assign a data type of it's empty + if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false) + { + Sql sql = Sql() + .Select(dt => dt.Select(x => x.NodeDto)) + .From() + .InnerJoin().On((dt, n) => dt.NodeId == n.NodeId) + .Where( + "propertyEditorAlias = @propertyEditorAlias", + new { propertyEditorAlias = propertyType.PropertyEditorAlias }) + .OrderBy(typeDto => typeDto.NodeId); + DataTypeDto? datatype = Database.FirstOrDefault(sql); + + // we cannot assign a data type if one was not found + if (datatype != null) + { + propertyType.DataTypeId = datatype.NodeId; + propertyType.DataTypeKey = datatype.NodeDto.UniqueId; + } + else + { + Logger.LogWarning( + "Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}", + propertyType.Alias, propertyType.PropertyEditorAlias); + } + } + } + + protected abstract TEntity? PerformGet(string alias); + + protected abstract IEnumerable? PerformGetAll(params Guid[]? ids); + + protected abstract bool PerformExists(Guid id); + + public string GetUniqueAlias(string alias) + { + // alias is unique across ALL content types! + var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); + List? aliases = Database.Fetch( + @"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", + new { pattern = alias + "%", objectType = NodeObjectTypeId }); + var i = 1; + string test; + while (aliases.Contains(test = alias + i)) + { + i++; + } + + return test; + } + + public bool HasContainerInPath(string contentPath) + { + var ids = contentPath.Split(Constants.CharArrays.Comma) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)).ToArray(); + return HasContainerInPath(ids); + } + + public bool HasContainerInPath(params int[] ids) + { + var sql = new Sql( + $@"SELECT COUNT(*) FROM cmsContentType +INNER JOIN {Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Constants.DatabaseSchema.Tables.Content}.contentTypeId +WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", + new { ids, isContainer = true }); + return Database.ExecuteScalar(sql) > 0; + } + + /// + /// Returns true or false depending on whether content nodes have been created based on the provided content type id. + /// + public bool HasContentNodes(int id) + { + var sql = new Sql( + $"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END", + new { id }); + return Database.ExecuteScalar(sql) == 1; + } + + protected override IEnumerable GetDeleteClauses() + { + // in theory, services should have ensured that content items of the given content type + // have been deleted and therefore PropertyData has been cleared, so PropertyData + // is included here just to be 100% sure since it has a FK on cmsPropertyType. + var list = new List + { + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE Id = @id", + "DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id", + "DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id", + "DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType + + " WHERE contentTypeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup + + " WHERE contenttypeNodeId = @id", + }; + return list; + } + + private class NameCompareDto + { + public int NodeId { get; set; } + + public int CurrentVersion { get; set; } + + public int LanguageId { get; set; } + + public string? CurrentName { get; set; } + + public string? PublishedName { get; set; } + + public int? PublishedVersion { get; set; } + + public int Id { get; set; } // the Id of the DocumentCultureVariationDto + + public bool Edited { get; set; } + } + + private class PropertyValueVersionDto + { + private decimal? _decimalValue; + + public int VersionId { get; set; } + + public int PropertyTypeId { get; set; } + + public int? LanguageId { get; set; } + + public string? Segment { get; set; } + + public int? IntValue { get; set; } + + [Column("decimalValue")] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + public DateTime? DateValue { get; set; } + + public string? VarcharValue { get; set; } + + public string? TextValue { get; set; } + + public int NodeId { get; set; } + + public bool Current { get; set; } + + public bool Published { get; set; } + + public byte Variations { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 6ca327dfab..04c60261ea 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; -using System.IO; using System.IO.Compression; -using System.Linq; using System.Xml.Linq; using Microsoft.Extensions.Options; using NPoco; @@ -19,739 +15,735 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Extensions; using File = System.IO.File; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +public class CreatedPackageSchemaRepository : ICreatedPackagesRepository { - /// - public class CreatedPackageSchemaRepository : ICreatedPackagesRepository + private readonly IContentService _contentService; + private readonly IContentTypeService _contentTypeService; + private readonly string _createdPackagesFolderPath; + private readonly IDataTypeService _dataTypeService; + private readonly IFileService _fileService; + private readonly FileSystems _fileSystems; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ILocalizationService _localizationService; + private readonly IMacroService _macroService; + private readonly MediaFileManager _mediaFileManager; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IEntityXmlSerializer _serializer; + private readonly string _tempFolderPath; + private readonly IUmbracoDatabase? _umbracoDatabase; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public CreatedPackageSchemaRepository( + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IHostingEnvironment hostingEnvironment, + IOptions globalSettings, + FileSystems fileSystems, + IEntityXmlSerializer serializer, + IDataTypeService dataTypeService, + ILocalizationService localizationService, + IFileService fileService, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IContentService contentService, + MediaFileManager mediaFileManager, + IMacroService macroService, + IContentTypeService contentTypeService, + string? mediaFolderPath = null, + string? tempFolderPath = null) { - private readonly PackageDefinitionXmlParser _xmlParser; - private readonly IUmbracoDatabase? _umbracoDatabase; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly FileSystems _fileSystems; - private readonly IEntityXmlSerializer _serializer; - private readonly IDataTypeService _dataTypeService; - private readonly ILocalizationService _localizationService; - private readonly IFileService _fileService; - private readonly IMediaService _mediaService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IContentService _contentService; - private readonly MediaFileManager _mediaFileManager; - private readonly IMacroService _macroService; - private readonly IContentTypeService _contentTypeService; - private readonly string _tempFolderPath; - private readonly string _createdPackagesFolderPath; + _umbracoDatabase = umbracoDatabaseFactory.CreateDatabase(); + _hostingEnvironment = hostingEnvironment; + _fileSystems = fileSystems; + _serializer = serializer; + _dataTypeService = dataTypeService; + _localizationService = localizationService; + _fileService = fileService; + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + _contentService = contentService; + _mediaFileManager = mediaFileManager; + _macroService = macroService; + _contentTypeService = contentTypeService; + _xmlParser = new PackageDefinitionXmlParser(); + _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; + _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; + } - /// - /// Initializes a new instance of the class. - /// - public CreatedPackageSchemaRepository( - IUmbracoDatabaseFactory umbracoDatabaseFactory, - IHostingEnvironment hostingEnvironment, - IOptions globalSettings, - FileSystems fileSystems, - IEntityXmlSerializer serializer, - IDataTypeService dataTypeService, - ILocalizationService localizationService, - IFileService fileService, - IMediaService mediaService, - IMediaTypeService mediaTypeService, - IContentService contentService, - MediaFileManager mediaFileManager, - IMacroService macroService, - IContentTypeService contentTypeService, - string? mediaFolderPath = null, - string? tempFolderPath = null) + public IEnumerable GetAll() + { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Select() + .From() + .OrderBy(x => x.Id); + + var packageDefinitions = new List(); + + List xmlSchemas = _umbracoDatabase.Fetch(query); + foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) { - _umbracoDatabase = umbracoDatabaseFactory.CreateDatabase(); - _hostingEnvironment = hostingEnvironment; - _fileSystems = fileSystems; - _serializer = serializer; - _dataTypeService = dataTypeService; - _localizationService = localizationService; - _fileService = fileService; - _mediaService = mediaService; - _mediaTypeService = mediaTypeService; - _contentService = contentService; - _mediaFileManager = mediaFileManager; - _macroService = macroService; - _contentTypeService = contentTypeService; - _xmlParser = new PackageDefinitionXmlParser(); - _createdPackagesFolderPath = mediaFolderPath ?? Constants.SystemDirectories.CreatedPackages; - _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData + "/PackageFiles"; - } - - public IEnumerable GetAll() - { - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Select() - .From() - .OrderBy(x => x.Id); - - var packageDefinitions = new List(); - - List xmlSchemas = _umbracoDatabase.Fetch(query); - foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) - { - var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); - if (packageDefinition is not null) - { - packageDefinition.Id = packageSchema.Id; - packageDefinition.Name = packageSchema.Name; - packageDefinition.PackageId = packageSchema.PackageId; - packageDefinitions.Add(packageDefinition); - } - } - - return packageDefinitions; - } - - public PackageDefinition? GetById(int id) - { - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Select() - .From() - .Where(x => x.Id == id); - List schemaDtos = _umbracoDatabase.Fetch(query); - - if (schemaDtos.IsCollectionEmpty()) - { - return null; - } - - var packageSchema = schemaDtos.First(); var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); if (packageDefinition is not null) { packageDefinition.Id = packageSchema.Id; packageDefinition.Name = packageSchema.Name; packageDefinition.PackageId = packageSchema.PackageId; + packageDefinitions.Add(packageDefinition); } - - return packageDefinition; } - public void Delete(int id) + return packageDefinitions; + } + + public PackageDefinition? GetById(int id) + { + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Select() + .From() + .Where(x => x.Id == id); + List schemaDtos = _umbracoDatabase.Fetch(query); + + if (schemaDtos.IsCollectionEmpty()) { - // Delete package snapshot - var packageDef = GetById(id); - if (File.Exists(packageDef?.PackagePath)) - { - File.Delete(packageDef.PackagePath); - } - - Sql query = new Sql(_umbracoDatabase!.SqlContext) - .Delete() - .Where(x => x.Id == id); - - _umbracoDatabase.Execute(query); + return null; } - public bool SavePackage(PackageDefinition definition) + CreatedPackageSchemaDto packageSchema = schemaDtos.First(); + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + if (packageDefinition is not null) { - if (definition == null) - { - throw new NullReferenceException("PackageDefinition cannot be null when saving"); - } + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + } - if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) - { - return false; - } + return packageDefinition; + } - // Ensure it's valid - ValidatePackage(definition); + public void Delete(int id) + { + // Delete package snapshot + PackageDefinition? packageDef = GetById(id); + if (File.Exists(packageDef?.PackagePath)) + { + File.Delete(packageDef.PackagePath); + } + Sql query = new Sql(_umbracoDatabase!.SqlContext) + .Delete() + .Where(x => x.Id == id); - if (definition.Id == default) - { - // Create dto from definition - var dto = new CreatedPackageSchemaDto() - { - Name = definition.Name, - Value = _xmlParser.ToXml(definition).ToString(), - UpdateDate = DateTime.Now, - PackageId = Guid.NewGuid() - }; + _umbracoDatabase.Execute(query); + } - // Set the ids, we have to save in database first to get the Id - _umbracoDatabase!.Insert(dto); - definition.Id = dto.Id; - } + public bool SavePackage(PackageDefinition? definition) + { + if (definition == null) + { + throw new NullReferenceException("PackageDefinition cannot be null when saving"); + } - // Save snapshot locally, we do this to the updated packagePath - ExportPackage(definition); + if (string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) + { + return false; + } + + // Ensure it's valid + ValidatePackage(definition); + + if (definition.Id == default) + { // Create dto from definition - var updatedDto = new CreatedPackageSchemaDto() + var dto = new CreatedPackageSchemaDto { Name = definition.Name, Value = _xmlParser.ToXml(definition).ToString(), - Id = definition.Id, - PackageId = definition.PackageId, - UpdateDate = DateTime.Now + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid(), }; - _umbracoDatabase?.Update(updatedDto); - return true; + // Set the ids, we have to save in database first to get the Id + _umbracoDatabase!.Insert(dto); + definition.Id = dto.Id; } - public string ExportPackage(PackageDefinition definition) + // Save snapshot locally, we do this to the updated packagePath + ExportPackage(definition); + + // Create dto from definition + var updatedDto = new CreatedPackageSchemaDto { - // Ensure it's valid - ValidatePackage(definition); + Name = definition.Name, + Value = _xmlParser.ToXml(definition).ToString(), + Id = definition.Id, + PackageId = definition.PackageId, + UpdateDate = DateTime.Now, + }; + _umbracoDatabase?.Update(updatedDto); - // Create a folder for building this package - var temporaryPath = _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString())); - Directory.CreateDirectory(temporaryPath); + return true; + } - try + public string ExportPackage(PackageDefinition definition) + { + // Ensure it's valid + ValidatePackage(definition); + + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(Path.Combine(_tempFolderPath, Guid.NewGuid().ToString())); + Directory.CreateDirectory(temporaryPath); + + try + { + // Init package file + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + + // Info section + root.Add(GetPackageInfoXml(definition)); + + PackageDocumentsAndTags(definition, root); + PackageDocumentTypes(definition, root); + PackageMediaTypes(definition, root); + PackageTemplates(definition, root); + PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem!); + PackageMacros(definition, root); + PackageDictionaryItems(definition, root); + PackageLanguages(definition, root); + PackageDataTypes(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); + + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) { - // Init package file - XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); - - // Info section - root.Add(GetPackageInfoXml(definition)); - - PackageDocumentsAndTags(definition, root); - PackageDocumentTypes(definition, root); - PackageMediaTypes(definition, root); - PackageTemplates(definition, root); - PackageStylesheets(definition, root); - PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem!); - PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", _fileSystems.PartialViewsFileSystem!); - PackageMacros(definition, root); - PackageDictionaryItems(definition, root); - PackageLanguages(definition, root); - PackageDataTypes(definition, root); - Dictionary mediaFiles = PackageMedia(definition, root); - - string fileName; - string tempPackagePath; - if (mediaFiles.Count > 0) + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) { - fileName = "package.zip"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) - using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) { - ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); - using (Stream entryStream = packageXmlEntry.Open()) - { - compiledPackageXml.Save(entryStream); - } + compiledPackageXml.Save(entryStream); + } - foreach (KeyValuePair mediaFile in mediaFiles) + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) { - var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; - ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); - using (Stream entryStream = mediaEntry.Open()) - using (mediaFile.Value) - { - mediaFile.Value.Seek(0, SeekOrigin.Begin); - mediaFile.Value.CopyTo(entryStream); - } + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); } } } + } + else + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } + } + + var directoryName = + _hostingEnvironment.MapPathContentRoot(Path.Combine( + _createdPackagesFolderPath, + definition.Name.Replace(' ', '_'))); + Directory.CreateDirectory(directoryName); + + var finalPackagePath = Path.Combine(directoryName, fileName); + + // Clean existing files + foreach (var packagePath in new[] { definition.PackagePath, finalPackagePath }) + { + if (File.Exists(packagePath)) + { + File.Delete(packagePath); + } + } + + // Move to final package path + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; + + return finalPackagePath; + } + finally + { + // Clean up + Directory.Delete(temporaryPath, true); + } + } + + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); + + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } + + private XDocument CreateCompiledPackageXml(out XElement root) + { + root = new XElement("umbPackage"); + var compiledPackageXml = new XDocument(root); + return compiledPackageXml; + } + + private void ValidatePackage(PackageDefinition definition) + { + // Ensure it's valid + var context = new ValidationContext(definition, null, null); + var results = new List(); + var isValid = Validator.TryValidateObject(definition, context, results); + if (!isValid) + { + throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + + string.Join(", ", results.Select(x => x.ErrorMessage))); + } + } + + private void PackageDataTypes(PackageDefinition definition, XContainer root) + { + var dataTypes = new XElement("DataTypes"); + foreach (var dtId in definition.DataTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDataType? dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) + { + continue; + } + + dataTypes.Add(_serializer.Serialize(dataType)); + } + + root.Add(dataTypes); + } + + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) + { + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ILanguage? lang = _localizationService.GetLanguageById(outInt); + if (lang == null) + { + continue; + } + + languages.Add(_serializer.Serialize(lang)); + } + + root.Add(languages); + } + + private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + { + var rootDictionaryItems = new XElement("DictionaryItems"); + var items = new Dictionary(); + + foreach (var dictionaryId in definition.DictionaryItems) + { + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDictionaryItem? di = _localizationService.GetDictionaryItemById(outInt); + + if (di == null) + { + continue; + } + + items[di.Key] = (di, _serializer.Serialize(di, false)); + } + + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) + { + foreach (Guid key in items.Keys.ToList()) + { + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; + + if (!dictionaryItem.ParentId.HasValue) + { + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + } else { - fileName = "package.xml"; - tempPackagePath = Path.Combine(temporaryPath, fileName); - - using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) { - compiledPackageXml.Save(fileStream); + // we've processed this parent element already so we can just append this xml child to it + AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, serializedDictionaryValue); } - } - - var directoryName = _hostingEnvironment.MapPathContentRoot(Path.Combine(_createdPackagesFolderPath, definition.Name.Replace(' ', '_'))); - Directory.CreateDirectory(directoryName); - - var finalPackagePath = Path.Combine(directoryName, fileName); - - // Clean existing files - foreach (var packagePath in new[] - { - definition.PackagePath, - finalPackagePath - }) - { - if (File.Exists(packagePath)) + else if (items.ContainsKey(dictionaryItem.ParentId.Value)) { - File.Delete(packagePath); - } - } - - // Move to final package path - File.Move(tempPackagePath, finalPackagePath); - - definition.PackagePath = finalPackagePath; - - return finalPackagePath; - } - finally - { - // Clean up - Directory.Delete(temporaryPath, true); - } - } - - private XDocument CreateCompiledPackageXml(out XElement root) - { - root = new XElement("umbPackage"); - var compiledPackageXml = new XDocument(root); - return compiledPackageXml; - } - - private void ValidatePackage(PackageDefinition definition) - { - // Ensure it's valid - var context = new ValidationContext(definition, serviceProvider: null, items: null); - var results = new List(); - var isValid = Validator.TryValidateObject(definition, context, results); - if (!isValid) - { - throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + - string.Join(", ", results.Select(x => x.ErrorMessage))); - } - } - - private void PackageDataTypes(PackageDefinition definition, XContainer root) - { - var dataTypes = new XElement("DataTypes"); - foreach (var dtId in definition.DataTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IDataType? dataType = _dataTypeService.GetDataType(outInt); - if (dataType == null) - { - continue; - } - - dataTypes.Add(_serializer.Serialize(dataType)); - } - - root.Add(dataTypes); - } - - private void PackageLanguages(PackageDefinition definition, XContainer root) - { - var languages = new XElement("Languages"); - foreach (var langId in definition.Languages) - { - if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - ILanguage? lang = _localizationService.GetLanguageById(outInt); - if (lang == null) - { - continue; - } - - languages.Add(_serializer.Serialize(lang)); - } - - root.Add(languages); - } - - private void PackageDictionaryItems(PackageDefinition definition, XContainer root) - { - var rootDictionaryItems = new XElement("DictionaryItems"); - var items = new Dictionary(); - - foreach (var dictionaryId in definition.DictionaryItems) - { - if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IDictionaryItem? di = _localizationService.GetDictionaryItemById(outInt); - - if (di == null) - { - continue; - } - - items[di.Key] = (di, _serializer.Serialize(di, false)); - } - - // organize them in hierarchy ... - var itemCount = items.Count; - var processed = new Dictionary(); - while (processed.Count < itemCount) - { - foreach (Guid key in items.Keys.ToList()) - { - (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; - - if (!dictionaryItem.ParentId.HasValue) - { - // if it has no parent, its definitely just at the root - AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + // we know the parent exists in the dictionary but + // we haven't processed it yet so we'll leave it for the next loop } else { - if (processed.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we've processed this parent element already so we can just append this xml child to it - AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, - serializedDictionaryValue); - } - else if (items.ContainsKey(dictionaryItem.ParentId.Value)) - { - // we know the parent exists in the dictionary but - // we haven't processed it yet so we'll leave it for the next loop - continue; - } - else - { - // in this case, the parent of this item doesn't exist in our collection, we have no - // choice but to add it to the root. - AppendDictionaryElement(rootDictionaryItems, items, processed, key, - serializedDictionaryValue); - } + // in this case, the parent of this item doesn't exist in our collection, we have no + // choice but to add it to the root. + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); } } } + } - root.Add(rootDictionaryItems); + root.Add(rootDictionaryItems); - static void AppendDictionaryElement(XElement rootDictionaryItems, - Dictionary items, - Dictionary processed, Guid key, XElement serializedDictionaryValue) + static void AppendDictionaryElement( + XElement rootDictionaryItems, + Dictionary items, + Dictionary processed, + Guid key, + XElement serializedDictionaryValue) + { + // track it + processed.Add(key, serializedDictionaryValue); + + // append it + rootDictionaryItems.Add(serializedDictionaryValue); + + // remove it so its not re-processed + items.Remove(key); + } + } + + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) + { + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - // track it - processed.Add(key, serializedDictionaryValue); + continue; + } - // append it - rootDictionaryItems.Add(serializedDictionaryValue); + XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); + if (macroXml is null) + { + continue; + } - // remove it so its not re-processed - items.Remove(key); + macros.Add(macroXml); + packagedMacros.Add(macro!); + } + + root.Add(macros); + + // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) + IEnumerable views = packagedMacros + .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) + .Select(x => + x.MacroSource[Constants.SystemDirectories.MacroPartials.Length..].Replace('/', '\\')); + PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem!); + } + + private void PackageStylesheets(PackageDefinition definition, XContainer root) + { + var stylesheetsXml = new XElement("Stylesheets"); + foreach (var stylesheet in definition.Stylesheets) + { + if (stylesheet.IsNullOrWhiteSpace()) + { + continue; + } + + XElement? xml = GetStylesheetXml(stylesheet, true); + if (xml != null) + { + stylesheetsXml.Add(xml); } } - private void PackageMacros(PackageDefinition definition, XContainer root) + root.Add(stylesheetsXml); + } + + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) { - var packagedMacros = new List(); - var macros = new XElement("Macros"); - foreach (var macroId in definition.Macros) + if (file.IsNullOrWhiteSpace()) { - if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt)) - { - continue; - } - - XElement? macroXml = GetMacroXml(outInt, out IMacro? macro); - if (macroXml is null) - { - continue; - } - - macros.Add(macroXml); - packagedMacros.Add(macro!); + continue; } - root.Add(macros); - - // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) - IEnumerable views = packagedMacros - .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) - .Select(x => - x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); - PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem!); - } - - private void PackageStylesheets(PackageDefinition definition, XContainer root) - { - var stylesheetsXml = new XElement("Stylesheets"); - foreach (var stylesheet in definition.Stylesheets) + if (!fileSystem.FileExists(file)) { - if (stylesheet.IsNullOrWhiteSpace()) - { - continue; - } - - XElement? xml = GetStylesheetXml(stylesheet, true); - if (xml != null) - { - stylesheetsXml.Add(xml); - } + throw new InvalidOperationException("No file found with path " + file); } - root.Add(stylesheetsXml); + using Stream stream = fileSystem.OpenFile(file); + + using (var reader = new StreamReader(stream)) + { + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); + } } - private void PackageStaticFiles( - IEnumerable filePaths, - XContainer root, - string containerName, - string elementName, - IFileSystem fileSystem) + root.Add(scriptsXml); + } + + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) { - var scriptsXml = new XElement(containerName); - foreach (var file in filePaths) + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) { - if (file.IsNullOrWhiteSpace()) - { - continue; - } + continue; + } - if (!fileSystem.FileExists(file)) - { - throw new InvalidOperationException("No file found with path " + file); - } + ITemplate? template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; + } - using Stream? stream = fileSystem.OpenFile(file); - if (stream is not null) + templatesXml.Add(_serializer.Serialize(template)); + } + + root.Add(templatesXml); + } + + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + { + var contentTypes = new HashSet(); + var docTypesXml = new XElement("DocumentTypes"); + foreach (var dtId in definition.DocumentTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IContentType? contentType = _contentTypeService.Get(outInt); + if (contentType == null) + { + continue; + } + + AddDocumentType(contentType, contentTypes); + } + + foreach (IContentType contentType in contentTypes) + { + docTypesXml.Add(_serializer.Serialize(contentType)); + } + + root.Add(docTypesXml); + } + + private void PackageMediaTypes(PackageDefinition definition, XContainer root) + { + var mediaTypes = new HashSet(); + var mediaTypesXml = new XElement("MediaTypes"); + foreach (var mediaTypeId in definition.MediaTypes) + { + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IMediaType? mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) + { + continue; + } + + AddMediaType(mediaType, mediaTypes); + } + + foreach (IMediaType mediaType in mediaTypes) + { + mediaTypesXml.Add(_serializer.Serialize(mediaType)); + } + + root.Add(mediaTypesXml); + } + + private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + { + // Documents and tags + if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse( + definition.ContentNodeId, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var contentNodeId)) + { + if (contentNodeId > 0) + { + // load content from umbraco. + IContent? content = _contentService.GetById(contentNodeId); + if (content != null) { - using (var reader = new StreamReader(stream)) - { - var fileContents = reader.ReadToEnd(); - scriptsXml.Add( + XElement contentXml = definition.ContentLoadChildNodes + ? content.ToDeepXml(_serializer) + : content.ToXml(_serializer); + + // Create the Documents/DocumentSet node + root.Add( + new XElement( + "Documents", new XElement( - elementName, - new XAttribute("path", file), - new XCData(fileContents))); - } - } - } - - root.Add(scriptsXml); - } - - private void PackageTemplates(PackageDefinition definition, XContainer root) - { - var templatesXml = new XElement("Templates"); - foreach (var templateId in definition.Templates) - { - if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - ITemplate? template = _fileService.GetTemplate(outInt); - if (template == null) - { - continue; - } - - templatesXml.Add(_serializer.Serialize(template)); - } - - root.Add(templatesXml); - } - - private void PackageDocumentTypes(PackageDefinition definition, XContainer root) - { - var contentTypes = new HashSet(); - var docTypesXml = new XElement("DocumentTypes"); - foreach (var dtId in definition.DocumentTypes) - { - if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IContentType? contentType = _contentTypeService.Get(outInt); - if (contentType == null) - { - continue; - } - - AddDocumentType(contentType, contentTypes); - } - - foreach (IContentType contentType in contentTypes) - { - docTypesXml.Add(_serializer.Serialize(contentType)); - } - - root.Add(docTypesXml); - } - - private void PackageMediaTypes(PackageDefinition definition, XContainer root) - { - var mediaTypes = new HashSet(); - var mediaTypesXml = new XElement("MediaTypes"); - foreach (var mediaTypeId in definition.MediaTypes) - { - if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) - { - continue; - } - - IMediaType? mediaType = _mediaTypeService.Get(outInt); - if (mediaType == null) - { - continue; - } - - AddMediaType(mediaType, mediaTypes); - } - - foreach (IMediaType mediaType in mediaTypes) - { - mediaTypesXml.Add(_serializer.Serialize(mediaType)); - } - - root.Add(mediaTypesXml); - } - - private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) - { - // Documents and tags - if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, - NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) - { - if (contentNodeId > 0) - { - // load content from umbraco. - IContent? content = _contentService.GetById(contentNodeId); - if (content != null) - { - var contentXml = definition.ContentLoadChildNodes - ? content.ToDeepXml(_serializer) - : content.ToXml(_serializer); - - // Create the Documents/DocumentSet node - - root.Add( - new XElement( - "Documents", - new XElement( - "DocumentSet", - new XAttribute("importMode", "root"), - contentXml))); - } + "DocumentSet", + new XAttribute("importMode", "root"), + contentXml))); } } } + } - private Dictionary PackageMedia(PackageDefinition definition, XElement root) + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); + + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) { - var mediaStreams = new Dictionary(); + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath!)); - // callback that occurs on each serialized media item - void OnSerializedMedia(IMedia media, XElement xmlMedia) - { - // get the media file path and store that separately in the XML. - // the media file path is different from the URL and is specifically - // extracted using the property editor for this media file and the current media file system. - Stream? mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); - if (mediaStream != null) - { - xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath!)); - - // add the stream to our outgoing stream - mediaStreams.Add(mediaFilePath!, mediaStream); - } - } - - IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - - var mediaXml = new XElement( - "MediaItems", - medias.Select(media => - { - XElement serializedMedia = _serializer.Serialize( - media, - definition.MediaLoadChildNodes, - OnSerializedMedia); - - return new XElement("MediaSet", serializedMedia); - })); - - root.Add(mediaXml); - - return mediaStreams; + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath!, mediaStream); } - /// - /// Gets a macros xml node - /// - private XElement? GetMacroXml(int macroId, out IMacro? macro) - { - macro = _macroService.GetById(macroId); - if (macro == null) - { - return null; - } + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); - XElement xml = _serializer.Serialize(macro); - return xml; + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => + { + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); + + return new XElement("MediaSet", serializedMedia); + })); + + root.Add(mediaXml); + + return mediaStreams; + } + + /// + /// Gets a macros xml node + /// + private XElement? GetMacroXml(int macroId, out IMacro? macro) + { + macro = _macroService.GetById(macroId); + if (macro == null) + { + return null; } - /// - /// Converts a umbraco stylesheet to a package xml node - /// - /// The path of the stylesheet. - /// if set to true [include properties]. - private XElement? GetStylesheetXml(string path, bool includeProperties) + XElement xml = _serializer.Serialize(macro); + return xml; + } + + /// + /// Converts a umbraco stylesheet to a package xml node + /// + /// The path of the stylesheet. + /// if set to true [include properties]. + private XElement? GetStylesheetXml(string path, bool includeProperties) + { + if (string.IsNullOrWhiteSpace(path)) { - if (string.IsNullOrWhiteSpace(path)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); - } - - IStylesheet? stylesheet = _fileService.GetStylesheet(path); - if (stylesheet == null) - { - return null; - } - - return _serializer.Serialize(stylesheet, includeProperties); + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); } - private void AddDocumentType(IContentType dt, HashSet dtl) + IStylesheet? stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) { - if (dt.ParentId > 0) - { - IContentType? parent = _contentTypeService.Get(dt.ParentId); - if (parent != null) - { - AddDocumentType(parent, dtl); - } - } + return null; + } - if (!dtl.Contains(dt)) + return _serializer.Serialize(stylesheet, includeProperties); + } + + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) + { + IContentType? parent = _contentTypeService.Get(dt.ParentId); + if (parent != null) { - dtl.Add(dt); + AddDocumentType(parent, dtl); } } - private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + if (!dtl.Contains(dt)) { - if (mediaType.ParentId > 0) - { - IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); - if (parent != null) - { - AddMediaType(parent, mediaTypes); - } - } + dtl.Add(dt); + } + } - if (!mediaTypes.Contains(mediaType)) + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) + { + IMediaType? parent = _mediaTypeService.Get(mediaType.ParentId); + if (parent != null) { - mediaTypes.Add(mediaType); + AddMediaType(parent, mediaTypes); } } - private static XElement GetPackageInfoXml(PackageDefinition definition) + if (!mediaTypes.Contains(mediaType)) { - var info = new XElement("info"); - - // Package info - var package = new XElement("package"); - package.Add(new XElement("name", definition.Name)); - info.Add(package); - return info; + mediaTypes.Add(mediaType); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs index f8fc9e14be..e9aaf82e87 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeContainerRepository.cs @@ -1,14 +1,18 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DataTypeContainerRepository : EntityContainerRepository, IDataTypeContainerRepository { - internal class DataTypeContainerRepository : EntityContainerRepository, IDataTypeContainerRepository + public DataTypeContainerRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DataTypeContainer) { - public DataTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.DataTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs index 7ca3e8c3c3..9f9d685552 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -23,331 +19,329 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class DataTypeRepository : EntityRepositoryBase, IDataTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class DataTypeRepository : EntityRepositoryBase, IDataTypeRepository + private readonly ILogger _dataTypeLogger; + private readonly PropertyEditorCollection _editors; + private readonly IConfigurationEditorJsonSerializer _serializer; + + public DataTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + PropertyEditorCollection editors, + ILogger logger, + ILoggerFactory loggerFactory, + IConfigurationEditorJsonSerializer serializer) + : base(scopeAccessor, cache, logger) { - private readonly PropertyEditorCollection _editors; - private readonly IConfigurationEditorJsonSerializer _serializer; - private readonly ILogger _dataTypeLogger; + _editors = editors; + _serializer = serializer; + _dataTypeLogger = loggerFactory.CreateLogger(); + } - public DataTypeRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - PropertyEditorCollection editors, - ILogger logger, - ILoggerFactory loggerFactory, - IConfigurationEditorJsonSerializer serializer) - : base(scopeAccessor, cache, logger) + protected Guid NodeObjectTypeId => Constants.ObjectTypes.DataType; + + public IEnumerable> Move(IDataType toMove, EntityContainer? container) + { + var parentId = -1; + if (container != null) { - _editors = editors; - _serializer = serializer; - _dataTypeLogger = loggerFactory.CreateLogger(); - } - - #region Overrides of RepositoryBase - - protected override IDataType? PerformGet(int id) - { - return GetMany(id)?.FirstOrDefault(); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var dataTypeSql = GetBaseQuery(false); - - if (ids?.Any() ?? false) + // Check on paths + if (string.Format(",{0},", container.Path) + .IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { - dataTypeSql.Where("umbracoNode.id in (@ids)", new { ids }); - } - else - { - dataTypeSql.Where(x => x.NodeObjectType == NodeObjectTypeId); + throw new DataOperationException( + MoveOperationStatusType.FailedNotAllowedByPath); } - var dtos = Database.Fetch(dataTypeSql); - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + parentId = container.Id; } - protected override IEnumerable PerformGetByQuery(IQuery query) + // used to track all the moved entities to be given to the event + var moveInfo = new List> { new(toMove, toMove.Path, parentId) }; + + var origPath = toMove.Path; + + // do the move to a new parent + toMove.ParentId = parentId; + + // set the updated path + toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); + + // schedule it for updating in the transaction + Save(toMove); + + // update all descendants from the original path, update in order of level + IEnumerable descendants = + Get(Query().Where(type => type.Path.StartsWith(origPath + ","))); + + IDataType lastParent = toMove; + if (descendants is not null) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(r => r.Select(x => x.NodeDto)); - - sql - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - return Array.Empty(); - } - - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DataType; - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IDataType entity) - { - entity.AddingEntity(); - - //ensure a datatype has a unique name before creating it - entity.Name = EnsureUniqueNodeName(entity.Name)!; - - // TODO: should the below be removed? - //Cannot add a duplicate data type - var existsSql = Sql() - .SelectCount() - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .Where(x => x.Text == entity.Name); - var exists = Database.ExecuteScalar(existsSql) > 0; - if (exists) + foreach (IDataType descendant in descendants.OrderBy(x => x.Level)) { - throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.ParentId = lastParent.Id; + descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); + + // schedule it for updating in the transaction + Save(descendant); } - - var dto = DataTypeFactory.BuildDto(entity, _serializer); - - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - - //Create the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(IDataType entity) + return moveInfo; + } + + public IReadOnlyDictionary> FindUsages(int id) + { + if (id == default) { - - entity.Name = EnsureUniqueNodeName(entity.Name, entity.Id)!; - - //Cannot change to a duplicate alias - var existsSql = Sql() - .SelectCount() - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .Where(x => x.Text == entity.Name && x.NodeId != entity.Id); - var exists = Database.ExecuteScalar(existsSql) > 0; - if (exists) - { - throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); - } - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - var dto = DataTypeFactory.BuildDto(entity, _serializer); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.NodeDto; - Database.Update(nodeDto); - Database.Update(dto); - - entity.ResetDirtyProperties(); + return new Dictionary>(); } - protected override void PersistDeletedItem(IDataType entity) - { - //Remove Notifications - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + Sql sql = Sql() + .Select(ct => ct.Select(node => node.NodeDto)) + .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) + .From() + .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) + .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) + .Where(pt => pt.DataTypeId == id) + .OrderBy(node => node.NodeId) + .AndBy(pt => pt.Alias); - //Remove Permissions - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + List? dtos = + Database.FetchOneToMany(ct => ct.PropertyTypes, sql); - //Remove associated tags - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + return dtos.ToDictionary( + x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType!.Value), x.NodeDto.UniqueId) + .EnsureClosed(), + x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); + } - //PropertyTypes containing the DataType being deleted - var propertyTypeDtos = Database.Fetch("WHERE dataTypeId = @Id", new { Id = entity.Id }); - //Go through the PropertyTypes and delete referenced PropertyData before deleting the PropertyType - foreach (var dto in propertyTypeDtos) - { - Database.Delete("WHERE propertytypeid = @Id", new { Id = dto.Id }); - Database.Delete("WHERE id = @Id", new { Id = dto.Id }); - } + #region Overrides of RepositoryBase - //Delete Content specific data - Database.Delete("WHERE nodeId = @Id", new { Id = entity.Id }); + protected override IDataType? PerformGet(int id) => GetMany(id).FirstOrDefault(); - //Delete (base) node data - Database.Delete("WHERE uniqueID = @Id", new { Id = entity.Key }); - - entity.DeleteDate = DateTime.Now; - } - - #endregion - - public IEnumerable> Move(IDataType toMove, EntityContainer? container) - { - var parentId = -1; - if (container != null) - { - // Check on paths - if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); - } - parentId = container.Id; - } - - //used to track all the moved entities to be given to the event - var moveInfo = new List> - { - new MoveEventInfo(toMove, toMove.Path, parentId) - }; - - var origPath = toMove.Path; - - //do the move to a new parent - toMove.ParentId = parentId; - - //set the updated path - toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); - - //schedule it for updating in the transaction - Save(toMove); - - //update all descendants from the original path, update in order of level - var descendants = Get(Query().Where(type => type.Path.StartsWith(origPath + ","))); - - var lastParent = toMove; - if (descendants is not null) - { - foreach (var descendant in descendants.OrderBy(x => x.Level)) - { - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); - - descendant.ParentId = lastParent.Id; - descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); - - //schedule it for updating in the transaction - Save(descendant); - } - } - - return moveInfo; - } - - public IReadOnlyDictionary> FindUsages(int id) - { - if (id == default) - return new Dictionary>(); - - var sql = Sql() - .Select(ct => ct.Select(node => node.NodeDto)) - .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) - .From() - .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) - .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) - .Where(pt => pt.DataTypeId == id) - .OrderBy(node => node.NodeId) - .AndBy(pt => pt.Alias); - - var dtos = Database.FetchOneToMany(ct => ct.PropertyTypes, sql); - - return dtos.ToDictionary( - x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType!.Value), x.NodeDto.UniqueId).EnsureClosed(), - x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); - } - - private string? EnsureUniqueNodeName(string? nodeName, int id = 0) - { - var template = SqlContext.Templates.Get(Cms.Core.Constants.SqlTemplates.DataTypeRepository.EnsureUniqueNodeName, tsql => tsql + private string? EnsureUniqueNodeName(string? nodeName, int id = 0) + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.DataTypeRepository.EnsureUniqueNodeName, + tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType"))); - var sql = template.Sql(NodeObjectTypeId); - var names = Database.Fetch(sql); + Sql sql = template.Sql(NodeObjectTypeId); + List? names = Database.Fetch(sql); - return SimilarNodeName.GetUniqueName(names, id, nodeName); - } - - - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.ContentType)] - private class ContentTypeReferenceDto : ContentTypeDto - { - [ResultColumn] - [Reference(ReferenceType.Many)] - public List PropertyTypes { get; set; } = null!; - } - - [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType)] - private class PropertyTypeReferenceDto - { - [Column("ptAlias")] - public string? Alias { get; set; } - - [Column("ptName")] - public string? Name { get; set; } - } + return SimilarNodeName.GetUniqueName(names, id, nodeName); } + + [TableName(Constants.DatabaseSchema.Tables.ContentType)] + private class ContentTypeReferenceDto : ContentTypeDto + { + [ResultColumn] + [Reference(ReferenceType.Many)] + public List PropertyTypes { get; } = null!; + } + + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + private class PropertyTypeReferenceDto + { + [Column("ptAlias")] + public string? Alias { get; set; } + + [Column("ptName")] + public string? Name { get; set; } + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql dataTypeSql = GetBaseQuery(false); + + if (ids?.Any() ?? false) + { + dataTypeSql.Where("umbracoNode.id in (@ids)", new { ids }); + } + else + { + dataTypeSql.Where(x => x.NodeObjectType == NodeObjectTypeId); + } + + List? dtos = Database.Fetch(dataTypeSql); + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => DataTypeFactory.BuildEntity(x, _editors, _dataTypeLogger, _serializer)).ToArray(); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(r => r.Select(x => x.NodeDto)); + + sql + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() => "umbracoNode.id = @id"; + + protected override IEnumerable GetDeleteClauses() => Array.Empty(); + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IDataType entity) + { + entity.AddingEntity(); + + // ensure a datatype has a unique name before creating it + entity.Name = EnsureUniqueNodeName(entity.Name)!; + + // TODO: should the below be removed? + // Cannot add a duplicate data type + Sql existsSql = Sql() + .SelectCount() + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .Where(x => x.Text == entity.Name); + var exists = Database.ExecuteScalar(existsSql) > 0; + if (exists) + { + throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); + } + + DataTypeDto dto = DataTypeFactory.BuildDto(entity, _serializer); + + // Logic for setting Path, Level and SortOrder + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + var level = parent.Level + 1; + var sortOrder = + Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + + // Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + // Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + Database.Update(nodeDto); + + // Update entity with correct values + entity.Id = nodeDto.NodeId; // Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IDataType entity) + { + entity.Name = EnsureUniqueNodeName(entity.Name, entity.Id)!; + + // Cannot change to a duplicate alias + Sql existsSql = Sql() + .SelectCount() + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .Where(x => x.Text == entity.Name && x.NodeId != entity.Id); + var exists = Database.ExecuteScalar(existsSql) > 0; + if (exists) + { + throw new DuplicateNameException("A data type with the name " + entity.Name + " already exists"); + } + + // Updates Modified date + entity.UpdatingEntity(); + + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + } + + DataTypeDto dto = DataTypeFactory.BuildDto(entity, _serializer); + + // Updates the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + Database.Update(nodeDto); + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + protected override void PersistDeletedItem(IDataType entity) + { + // Remove Notifications + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // Remove Permissions + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // Remove associated tags + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // PropertyTypes containing the DataType being deleted + List? propertyTypeDtos = + Database.Fetch("WHERE dataTypeId = @Id", new { entity.Id }); + + // Go through the PropertyTypes and delete referenced PropertyData before deleting the PropertyType + foreach (PropertyTypeDto? dto in propertyTypeDtos) + { + Database.Delete("WHERE propertytypeid = @Id", new { dto.Id }); + Database.Delete("WHERE id = @Id", new { dto.Id }); + } + + // Delete Content specific data + Database.Delete("WHERE nodeId = @Id", new { entity.Id }); + + // Delete (base) node data + Database.Delete("WHERE uniqueID = @Id", new { Id = entity.Key }); + + entity.DeleteDate = DateTime.Now; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index 7f39bb3ee6..08b110ac41 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,383 +12,358 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement -{ - /// - /// Represents a repository for doing CRUD operations for - /// - internal class DictionaryRepository : EntityRepositoryBase, IDictionaryRepository - { - private readonly ILoggerFactory _loggerFactory; +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; - public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, ILoggerFactory loggerFactory) - : base(scopeAccessor, cache, logger) +/// +/// Represents a repository for doing CRUD operations for +/// +internal class DictionaryRepository : EntityRepositoryBase, IDictionaryRepository +{ + private readonly ILoggerFactory _loggerFactory; + + public DictionaryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + ILoggerFactory loggerFactory) + : base(scopeAccessor, cache, logger) => + _loggerFactory = loggerFactory; + + public IDictionaryItem? Get(Guid uniqueId) + { + var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return uniqueIdRepo.Get(uniqueId); + } + + public IDictionaryItem? Get(string key) + { + var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return keyRepo.Get(key); + } + + public Dictionary GetDictionaryItemKeyMap() + { + var columns = new[] { "key", "id" }.Select(x => (object)SqlSyntax.GetQuotedColumnName(x)).ToArray(); + Sql sql = Sql().Select(columns).From(); + return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); + } + + public IEnumerable GetDictionaryItemDescendants(Guid? parentId) + { + // This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive + // lookup to get descendants. Currently this is the most efficient way to do it + Func>> getItemsFromParents = guids => { - _loggerFactory = loggerFactory; + return guids.InGroupsOf(Constants.Sql.MaxParameterCount) + .Select(group => + { + Sql sqlClause = GetBaseQuery(false) + .Where(x => x.Parent != null) + .WhereIn(x => x.Parent, group); + + var translator = new SqlTranslator(sqlClause, Query()); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.UniqueId); + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + }); + }; + + IEnumerable> childItems = parentId.HasValue == false + ? new[] { GetRootDictionaryItems() } + : getItemsFromParents(new[] { parentId.Value }); + + return childItems.SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())) + .SelectMany(items => items); + } + + protected override IRepositoryCachePolicy CreateCachePolicy() + { + var options = new RepositoryCachePolicyOptions + { + // allow zero to be cached + GetAllCacheAllowZeroCount = true, + }; + + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); + } + + protected IDictionaryItem ConvertFromDto(DictionaryDto dto) + { + IDictionaryItem entity = DictionaryItemFactory.BuildEntity(dto); + + entity.Translations = dto.LanguageTextDtos.EmptyNull() + .Where(x => x.LanguageId > 0) + .Select(x => DictionaryTranslationFactory.BuildEntity(x, dto.UniqueId)) + .ToList(); + + return entity; + } + + #region Overrides of RepositoryBase + + protected override IDictionaryItem? PerformGet(int id) + { + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { id }) + .OrderBy(x => x.UniqueId); + + DictionaryDto? dto = Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .FirstOrDefault(); + + if (dto == null) + { + return null; } - protected override IRepositoryCachePolicy CreateCachePolicy() + IDictionaryItem entity = ConvertFromDto(dto); + + // reset dirty initial properties (U4-1946) + ((EntityBase)entity).ResetDirtyProperties(false); + + return entity; + } + + private IEnumerable GetRootDictionaryItems() + { + IQuery query = Query().Where(x => x.ParentId == null); + return Get(query); + } + + private class DictionaryItemKeyIdDto + { + public string Key { get; } = null!; + + public Guid Id { get; set; } + } + + private class DictionaryByUniqueIdRepository : SimpleGetRepository + { + private readonly DictionaryRepository _dictionaryRepository; + + public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, + AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _dictionaryRepository = dictionaryRepository; + + protected override IEnumerable PerformFetch(Sql sql) => + Database + .FetchOneToMany(x => x.LanguageTextDtos, sql); + + protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); + + protected override string GetBaseWhereClause() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " = @id"; + + protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => + _dictionaryRepository.ConvertFromDto(dto); + + protected override object GetBaseWhereClauseArguments(Guid id) => new { id }; + + protected override string GetWhereInClauseForGetAll() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; + + protected override IRepositoryCachePolicy CreateCachePolicy() { var options = new RepositoryCachePolicyOptions { - //allow zero to be cached - GetAllCacheAllowZeroCount = true + // allow zero to be cached + GetAllCacheAllowZeroCount = true, }; - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); } + } - #region Overrides of RepositoryBase + private class DictionaryByKeyRepository : SimpleGetRepository + { + private readonly DictionaryRepository _dictionaryRepository; - protected override IDictionaryItem? PerformGet(int id) + public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, + AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _dictionaryRepository = dictionaryRepository; + + protected override IEnumerable PerformFetch(Sql sql) => + Database + .FetchOneToMany(x => x.LanguageTextDtos, sql); + + protected override Sql GetBaseQuery(bool isCount) => _dictionaryRepository.GetBaseQuery(isCount); + + protected override string GetBaseWhereClause() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " = @id"; + + protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) => + _dictionaryRepository.ConvertFromDto(dto); + + protected override object GetBaseWhereClauseArguments(string? id) => new { id }; + + protected override string GetWhereInClauseForGetAll() => + "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; + + protected override IRepositoryCachePolicy CreateCachePolicy() { - var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { id = id }) - .OrderBy(x => x.UniqueId); - - var dto = Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .FirstOrDefault(); - - if (dto == null) - return null; - - var entity = ConvertFromDto(dto); - - // reset dirty initial properties (U4-1946) - ((EntityBase)entity).ResetDirtyProperties(false); - - return entity; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); - if (ids?.Any() ?? false) + var options = new RepositoryCachePolicyOptions { - sql.WhereIn(x => x.PrimaryKey, ids); - } + // allow zero to be cached + GetAllCacheAllowZeroCount = true, + }; - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .Select(ConvertFromDto); + return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + options); + } + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.PrimaryKey > 0); + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.PrimaryKey, ids); } - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql.OrderBy(x => x.UniqueId); + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + } - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql) - .Select(ConvertFromDto); + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.UniqueId); + + return Database + .FetchOneToMany(x => x.LanguageTextDtos, sql) + .Select(ConvertFromDto); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) + { + sql.SelectCount() + .From(); + } + else + { + sql.SelectAll() + .From() + .LeftJoin() + .On(left => left.UniqueId, right => right.UniqueId); } - #endregion + return sql; + } - #region Overrides of EntityRepositoryBase + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.DictionaryEntry}.pk = @id"; - protected override Sql GetBaseQuery(bool isCount) + protected override IEnumerable GetDeleteClauses() => new List(); + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IDictionaryItem entity) + { + var dictionaryItem = (DictionaryItem)entity; + + dictionaryItem.AddingEntity(); + + foreach (IDictionaryTranslation translation in dictionaryItem.Translations) { - var sql = Sql(); - if (isCount) + translation.Value = translation.Value.ToValidXmlString(); + } + + DictionaryDto dto = DictionaryItemFactory.BuildDto(dictionaryItem); + + var id = Convert.ToInt32(Database.Insert(dto)); + dictionaryItem.Id = id; + + foreach (IDictionaryTranslation translation in dictionaryItem.Translations) + { + LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, dictionaryItem.Key); + translation.Id = Convert.ToInt32(Database.Insert(textDto)); + translation.Key = dictionaryItem.Key; + } + + dictionaryItem.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IDictionaryItem entity) + { + entity.UpdatingEntity(); + + foreach (IDictionaryTranslation translation in entity.Translations) + { + translation.Value = translation.Value.ToValidXmlString(); + } + + DictionaryDto dto = DictionaryItemFactory.BuildDto(entity); + + Database.Update(dto); + + foreach (IDictionaryTranslation translation in entity.Translations) + { + LanguageTextDto textDto = DictionaryTranslationFactory.BuildDto(translation, entity.Key); + if (translation.HasIdentity) { - sql.SelectCount() - .From(); + Database.Update(textDto); } else { - sql.SelectAll() - .From() - .LeftJoin() - .On(left => left.UniqueId, right => right.UniqueId); - } - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.DictionaryEntry}.pk = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - return new List(); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IDictionaryItem entity) - { - var dictionaryItem = ((DictionaryItem) entity); - - dictionaryItem.AddingEntity(); - - foreach (var translation in dictionaryItem.Translations) - translation.Value = translation.Value.ToValidXmlString(); - - var dto = DictionaryItemFactory.BuildDto(dictionaryItem); - - var id = Convert.ToInt32(Database.Insert(dto)); - dictionaryItem.Id = id; - - foreach (var translation in dictionaryItem.Translations) - { - var textDto = DictionaryTranslationFactory.BuildDto(translation, dictionaryItem.Key); translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = dictionaryItem.Key; - } - - dictionaryItem.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IDictionaryItem entity) - { - entity.UpdatingEntity(); - - foreach (var translation in entity.Translations) - translation.Value = translation.Value.ToValidXmlString(); - - var dto = DictionaryItemFactory.BuildDto(entity); - - Database.Update(dto); - - foreach (var translation in entity.Translations) - { - var textDto = DictionaryTranslationFactory.BuildDto(translation, entity.Key); - if (translation.HasIdentity) - { - Database.Update(textDto); - } - else - { - translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = entity.Key; - } - } - - entity.ResetDirtyProperties(); - - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); - } - - protected override void PersistDeletedItem(IDictionaryItem entity) - { - RecursiveDelete(entity.Key); - - Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); - Database.Delete("WHERE id = @Id", new { Id = entity.Key }); - - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); - - entity.DeleteDate = DateTime.Now; - } - - private void RecursiveDelete(Guid parentId) - { - var list = Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); - foreach (var dto in list) - { - RecursiveDelete(dto.UniqueId); - - Database.Delete("WHERE UniqueId = @Id", new { Id = dto.UniqueId }); - Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); - - //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); + translation.Key = entity.Key; } } - #endregion + entity.ResetDirtyProperties(); - protected IDictionaryItem ConvertFromDto(DictionaryDto dto) + // Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + } + + protected override void PersistDeletedItem(IDictionaryItem entity) + { + RecursiveDelete(entity.Key); + + Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); + Database.Delete("WHERE id = @Id", new { Id = entity.Key }); + + // Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + + entity.DeleteDate = DateTime.Now; + } + + private void RecursiveDelete(Guid parentId) + { + List? list = + Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); + foreach (DictionaryDto? dto in list) { - var entity = DictionaryItemFactory.BuildEntity(dto); + RecursiveDelete(dto.UniqueId); - entity.Translations = dto.LanguageTextDtos.EmptyNull() - .Where(x => x.LanguageId > 0) - .Select(x => DictionaryTranslationFactory.BuildEntity(x, dto.UniqueId)) - .ToList(); + Database.Delete("WHERE UniqueId = @Id", new { Id = dto.UniqueId }); + Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); - return entity; - } - - public IDictionaryItem? Get(Guid uniqueId) - { - var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); - return uniqueIdRepo.Get(uniqueId); - } - - public IDictionaryItem? Get(string key) - { - var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, _loggerFactory.CreateLogger()); - return keyRepo.Get(key); - } - - private IEnumerable? GetRootDictionaryItems() - { - var query = Query().Where(x => x.ParentId == null); - return Get(query); - } - - public Dictionary GetDictionaryItemKeyMap() - { - var columns = new[] { "key", "id" }.Select(x => (object) SqlSyntax.GetQuotedColumnName(x)).ToArray(); - var sql = Sql().Select(columns).From(); - return Database.Fetch(sql).ToDictionary(x => x.Key, x => x.Id); - } - - private class DictionaryItemKeyIdDto - { - public string Key { get; set; } = null!; - public Guid Id { get; set; } - } - - public IEnumerable GetDictionaryItemDescendants(Guid? parentId) - { - //This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive - // lookup to get descendants. Currently this is the most efficient way to do it - - Func>> getItemsFromParents = guids => - { - return guids.InGroupsOf(Constants.Sql.MaxParameterCount) - .Select(group => - { - var sqlClause = GetBaseQuery(false) - .Where(x => x.Parent != null) - .WhereIn(x => x.Parent, group); - - var translator = new SqlTranslator(sqlClause, Query()); - var sql = translator.Translate(); - sql.OrderBy(x => x.UniqueId); - - return Database - .FetchOneToMany(x=> x.LanguageTextDtos, sql) - .Select(ConvertFromDto); - }); - }; - - var childItems = parentId.HasValue == false - ? new[] { GetRootDictionaryItems()! } - : getItemsFromParents(new[] { parentId.Value }); - - return childItems.SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())).SelectMany(items => items); - - } - - private class DictionaryByUniqueIdRepository : SimpleGetRepository - { - private readonly DictionaryRepository _dictionaryRepository; - - public DictionaryByUniqueIdRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _dictionaryRepository = dictionaryRepository; - } - - protected override IEnumerable PerformFetch(Sql sql) - { - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _dictionaryRepository.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " = @id"; - } - - protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) - { - return _dictionaryRepository.ConvertFromDto(dto); - } - - protected override object GetBaseWhereClauseArguments(Guid id) - { - return new { id = id }; - } - - protected override string GetWhereInClauseForGetAll() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; - } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); - } - } - - private class DictionaryByKeyRepository : SimpleGetRepository - { - private readonly DictionaryRepository _dictionaryRepository; - - public DictionaryByKeyRepository(DictionaryRepository dictionaryRepository, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _dictionaryRepository = dictionaryRepository; - } - - protected override IEnumerable PerformFetch(Sql sql) - { - return Database - .FetchOneToMany(x => x.LanguageTextDtos, sql); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _dictionaryRepository.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " = @id"; - } - - protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) - { - return _dictionaryRepository.ConvertFromDto(dto); - } - - protected override object GetBaseWhereClauseArguments(string? id) - { - return new { id = id }; - } - - protected override string GetWhereInClauseForGetAll() - { - return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; - } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - var options = new RepositoryCachePolicyOptions - { - //allow zero to be cached - GetAllCacheAllowZeroCount = true - }; - - return new SingleItemsOnlyRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, options); - } + // Clear the cache entries that exist by uniqueid/item key + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs index f97aec0917..5bd2844405 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentBlueprintRepository.cs @@ -1,5 +1,5 @@ -using System; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Repositories; @@ -8,43 +8,56 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Override the base content repository so we can change the node object type +/// +/// +/// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, +/// however to do that +/// requires quite a lot of work since we'd need to re-organize the inheritance quite a lot or create a helper class to +/// perform a lot of the underlying logic. +/// TODO: Create a helper method to contain most of the underlying logic for the ContentRepository +/// +internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository { - /// - /// Override the base content repository so we can change the node object type - /// - /// - /// It would be nicer if we could separate most of this down into a smaller version of the ContentRepository class, however to do that - /// requires quite a lot of work since we'd need to re-organize the inheritance quite a lot or create a helper class to perform a lot of the underlying logic. - /// - /// TODO: Create a helper method to contain most of the underlying logic for the ContentRepository - /// - internal class DocumentBlueprintRepository : DocumentRepository, IDocumentBlueprintRepository + public DocumentBlueprintRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + ILoggerFactory loggerFactory, + IContentTypeRepository contentTypeRepository, + ITemplateRepository templateRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditorCollection, + IDataTypeService dataTypeService, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base( + scopeAccessor, + appCaches, + logger, + loggerFactory, + contentTypeRepository, + templateRepository, + tagRepository, + languageRepository, + relationRepository, + relationTypeRepository, + propertyEditorCollection, + dataValueReferenceFactories, + dataTypeService, + serializer, + eventAggregator) { - public DocumentBlueprintRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - ILoggerFactory loggerFactory, - IContentTypeRepository contentTypeRepository, - ITemplateRepository templateRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditorCollection, - IDataTypeService dataTypeService, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, appCaches, logger, loggerFactory, contentTypeRepository, templateRepository, - tagRepository, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, - dataValueReferenceFactories, dataTypeService, serializer, eventAggregator) - { - } - - protected override bool EnsureUniqueNaming => false; // duplicates are allowed - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.DocumentBlueprint; } + + protected override bool EnsureUniqueNaming => false; // duplicates are allowed + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.DocumentBlueprint; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 39084aa5d9..bada35623b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,7 +11,6 @@ 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.Scoping; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -24,361 +20,1100 @@ using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for . +/// +public class DocumentRepository : ContentRepositoryBase, IDocumentRepository { + private readonly AppCaches _appCaches; + private readonly ContentByGuidReadRepository _contentByGuidReadRepository; + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ILoggerFactory _loggerFactory; + private readonly IScopeAccessor _scopeAccessor; + private readonly IJsonSerializer _serializer; + private readonly ITagRepository _tagRepository; + private readonly ITemplateRepository _templateRepository; + private PermissionRepository? _permissionRepository; + /// - /// Represents a repository for doing CRUD operations for . + /// Constructor /// - public class DocumentRepository : ContentRepositoryBase, IDocumentRepository + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 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 + /// + public DocumentRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + ILoggerFactory loggerFactory, + IContentTypeRepository contentTypeRepository, + ITemplateRepository templateRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly IContentTypeRepository _contentTypeRepository; - private readonly ITemplateRepository _templateRepository; - private readonly ITagRepository _tagRepository; - private readonly IJsonSerializer _serializer; - private readonly AppCaches _appCaches; - private readonly ILoggerFactory _loggerFactory; - private PermissionRepository? _permissionRepository; - private readonly ContentByGuidReadRepository _contentByGuidReadRepository; - private readonly IScopeAccessor _scopeAccessor; + _contentTypeRepository = + contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); + _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _serializer = serializer; + _appCaches = appCaches; + _loggerFactory = loggerFactory; + _scopeAccessor = scopeAccessor; + _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, appCaches, + loggerFactory.CreateLogger()); + } - /// - /// Constructor - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// 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 - /// - public DocumentRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - ILoggerFactory loggerFactory, - IContentTypeRepository contentTypeRepository, - ITemplateRepository templateRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditors, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, appCaches, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) + protected override DocumentRepository This => this; + + /// + /// Default is to always ensure all documents have unique names + /// + protected virtual bool EnsureUniqueNaming { get; } = true; + + // note: is ok to 'new' the repo here as it's a sub-repo really + private PermissionRepository PermissionRepository => _permissionRepository + ?? (_permissionRepository = + new PermissionRepository( + _scopeAccessor, _appCaches, + _loggerFactory + .CreateLogger< + PermissionRepository>())); + + /// + public ContentScheduleCollection GetContentSchedule(int contentId) + { + var result = new ContentScheduleCollection(); + + List? scheduleDtos = Database.Fetch(Sql() + .Select() + .From() + .Where(x => x.NodeId == contentId)); + + foreach (ContentScheduleDto? scheduleDto in scheduleDtos) { - _contentTypeRepository = contentTypeRepository ?? throw new ArgumentNullException(nameof(contentTypeRepository)); - _templateRepository = templateRepository ?? throw new ArgumentNullException(nameof(templateRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _serializer = serializer; - _appCaches = appCaches; - _loggerFactory = loggerFactory; - _scopeAccessor = scopeAccessor; - _contentByGuidReadRepository = new ContentByGuidReadRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); + result.Add(new ContentSchedule(scheduleDto.Id, + LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, + scheduleDto.Date, + scheduleDto.Action == ContentScheduleAction.Release.ToString() + ? ContentScheduleAction.Release + : ContentScheduleAction.Expire)); } - protected override DocumentRepository This => this; + return result; + } - /// - /// Default is to always ensure all documents have unique names - /// - protected virtual bool EnsureUniqueNaming { get; } = true; - - // note: is ok to 'new' the repo here as it's a sub-repo really - private PermissionRepository PermissionRepository => _permissionRepository - ?? (_permissionRepository = new PermissionRepository(_scopeAccessor, _appCaches, _loggerFactory.CreateLogger>())); - - #region Repository Base - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Document; - - protected override IContent? PerformGet(int id) + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + // note: 'updater' is the user who created the latest draft version, + // we don't have an 'updater' per culture (should we?) + if (ordering.OrderBy.InvariantEquals("updater")) { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); + Sql joins = Sql() + .InnerJoin("updaterUser") + .On((version, user) => version.UserId == user.Id, + aliasRight: "updaterUser"); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); + // see notes in ApplyOrdering: the field MUST be selected + aliased + sql = Sql( + InsertBefore(sql, "FROM", + ", " + SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering "), + sql.Arguments); + + sql = InsertJoins(sql, joins); + + return "ordering"; } - protected override IEnumerable PerformGetAll(params int[]? ids) + if (ordering.OrderBy.InvariantEquals("published")) { - var sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); - - return MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(QueryType.Many); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - AddGetByQueryOrderBy(sql); - - return MapDtosToContent(Database.Fetch(sql)); - } - - private void AddGetByQueryOrderBy(Sql sql) - { - sql - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); - } - - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType, true); - } - - // gets the COALESCE expression for variant/invariant name - private string VariantNameSqlExpression - => SqlContext.VisitDto((ccv, node) => ccv.Name ?? node.Text, "ccv").Sql; - - protected Sql GetBaseQuery(QueryType queryType, bool current) - { - var sql = SqlContext.Sql(); - - switch (queryType) + // no culture = can only work on the global 'published' flag + if (ordering.Culture.IsNullOrWhiteSpace()) { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - // R# may flag this ambiguous and red-squiggle it, but it is not - sql = sql.Select(r => - r.Select(documentDto => documentDto.ContentDto, r1 => - r1.Select(contentDto => contentDto.NodeDto)) - .Select(documentDto => documentDto.DocumentVersionDto, r1 => - r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) - .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => - r1.Select(documentVersionDto => documentVersionDto!.ContentVersionDto, "pcv"))) - - // select the variant name, coalesce to the invariant name, as "variantName" - .AndSelect(VariantNameSqlExpression + " AS variantName"); - break; + // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have + // the whole CASE fragment in ORDER BY due to it not being detected by NPoco + sql = Sql(InsertBefore(sql, "FROM", ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering "), + sql.Arguments); + return "ordering"; } - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) + // invariant: left join will yield NULL and we must use pcv to determine published + // variant: left join may yield NULL or something, and that determines published - // inner join on mandatory edited version - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.Id == right.Id) - // left join on optional published version - .LeftJoin(nested => - nested.InnerJoin("pdv") - .On((left, right) => left.Id == right.Id && right.Published, "pcv", "pdv"), "pcv") - .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") - - // TODO: should we be joining this when the query type is not single/many? + Sql joins = Sql() + .InnerJoin("ctype").On( + (content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype") // left join on optional culture variation //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code .LeftJoin(nested => - nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") - .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv"); + nested.InnerJoin("langp").On( + (ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccvp", + "langp"), + "ccvp") + .On((version, ccv) => version.Id == ccv.VersionId, + "pcv", "ccvp"); - sql - .Where(x => x.NodeObjectType == NodeObjectTypeId); + sql = InsertJoins(sql, joins); - // this would ensure we don't get the published version - keep for reference - //sql - // .WhereAny( - // x => x.Where((x1, x2) => x1.Id != x2.Id, alias2: "pcv"), - // x => x.WhereNull(x1 => x1.Id, "pcv") - // ); + // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have + // the whole CASE fragment in ORDER BY due to it not being detected by NPoco + var sqlText = InsertBefore(sql.SQL, "FROM", - if (current) - sql.Where(x => x.Current); // always get the current version + // when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id, + // otherwise check if there's a version culture variation for the lang, via ccv.id + ", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccvp.id IS NULL THEN 0 ELSE 1 END) END) AS ordering "); // trailing space is important! - return sql; + sql = Sql(sqlText, sql.Arguments); + + return "ordering"; } - protected override Sql GetBaseQuery(bool isCount) - { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - } + return base.ApplySystemOrdering(ref sql, ordering); + } - // ah maybe not, that what's used for eg Exists in base repo - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; - } + private IEnumerable MapDtosToContent(List dtos, + bool withCache = false, + bool loadProperties = true, + bool loadTemplates = true, + bool loadVariants = true) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var templateIds = new List(); - protected override IEnumerable GetDeleteClauses() + var content = new Content[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) { - var list = new List + DocumentDto dto = dtos[i]; + + if (withCache) { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.RedirectUrl + " WHERE contentKey IN (SELECT uniqueId FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + // if the cache contains the (proper version of the) item, use it + IContent? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) + { + content[i] = (Content)cached; + continue; + } + } + + // else, need to build it + + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IContentType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _contentTypeRepository.Get(contentTypeId); + } + + Content c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + if (loadTemplates) + { + // need templates + var templateId = dto.DocumentVersionDto.TemplateId; + if (templateId.HasValue) + { + templateIds.Add(templateId.Value); + } + + if (dto.Published) + { + templateId = dto.PublishedVersionDto!.TemplateId; + if (templateId.HasValue) + { + templateIds.Add(templateId.Value); + } + } + } + + // need temps, for properties, templates and variations + var versionId = dto.DocumentVersionDto.Id; + var publishedVersionId = dto.Published ? dto.PublishedVersionDto!.Id : 0; + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType, c) + { + Template1Id = dto.DocumentVersionDto.TemplateId }; - return list; + if (dto.Published) + { + temp.Template2Id = dto.PublishedVersionDto!.TemplateId; + } + + temps.Add(temp); } - #endregion - - #region Versions - - public override IEnumerable GetAllVersions(int nodeId) + Dictionary? templates = null; + if (loadTemplates) { - var sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - return MapDtosToContent(Database.Fetch(sql), true); + // load all required templates in 1 query, and index + templates = _templateRepository.GetMany(templateIds.ToArray())? + .ToDictionary(x => x.Id, x => x); } - // TODO: This method needs to return a readonly version of IContent! The content returned - // from this method does not contain all of the data required to re-persist it and if that - // is attempted some odd things will occur. - // Either we create an IContentReadOnly (which ultimately we should for vNext so we can - // differentiate between methods that return entities that can be re-persisted or not), or - // in the meantime to not break API compatibility, we can add a property to IContentBase - // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw - // an exception if that entity is passed to a Save method. - // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService. - // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly - // which can return IContentReadOnly. - // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a - // content item. Ideally for paged data that populates list views, these would be ultra slim - // content items, there's no reason to populate those with really anything apart from property data, - // but until we do something like the above, we can't do that since it would be breaking and unclear. - public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + IDictionary? properties = null; + if (loadProperties) { - var sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - var pageIndex = skip / take; - - return MapDtosToContent(Database.Page(pageIndex+1, take, sql).Items, true, - // load bare minimum, need variants though since this is used to rollback with variants - false, false, true); + // load all properties for all documents from database in 1 query - indexed by version id + properties = GetPropertyCollections(temps); } - public override IContent? GetVersion(int versionId) + // assign templates and properties + foreach (TempContent temp in temps) { - var sql = GetBaseQuery(QueryType.Single, false) - .Where(x => x.Id == versionId); + if (loadTemplates) + { + // set the template ID if it matches an existing template + if (temp.Template1Id.HasValue && (templates?.ContainsKey(temp.Template1Id.Value) ?? false)) + { + temp.Content!.TemplateId = temp.Template1Id; + } - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); + if (temp.Template2Id.HasValue && (templates?.ContainsKey(temp.Template2Id.Value) ?? false)) + { + temp.Content!.PublishTemplateId = temp.Template2Id; + } + } + + + // set properties + if (loadProperties) + { + if (properties?.ContainsKey(temp.VersionId) ?? false) + { + temp.Content!.Properties = properties[temp.VersionId]; + } + else + { + throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); + } + } } - // deletes a specific version - public override void DeleteVersion(int versionId) + if (loadVariants) { - // TODO: test object node type? - - // get the version we want to delete - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql => - tsql.Select() - .AndSelect() - .From() - .InnerJoin() - .On((c, d) => c.Id == d.Id) - .Where(x => x.Id == SqlTemplate.Arg("versionId")) - ); - var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); - - // nothing to delete - if (versionDto == null) - return; - - // don't delete the current or published version - if (versionDto.ContentVersionDto.Current) - throw new InvalidOperationException("Cannot delete the current version."); - else if (versionDto.Published) - throw new InvalidOperationException("Cannot delete the published version."); - - PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId); + // set variations, if varying + temps = temps.Where(x => x.ContentType?.VariesByCulture() ?? false).ToList(); + if (temps.Count > 0) + { + // load all variations for all documents from database, in one query + IDictionary> contentVariations = GetContentVariations(temps); + IDictionary> documentVariations = GetDocumentVariations(temps); + foreach (TempContent temp in temps) + { + SetVariations(temp.Content, contentVariations, documentVariations); + } + } } - // deletes all versions of an entity, older than a date. - public override void DeleteVersions(int nodeId, DateTime versionDate) - { - // TODO: test object node type? - // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql => - tsql.Select() - .From() - .InnerJoin() - .On((c, d) => c.Id == d.Id) - .Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - .Where(x => !x.Published) - ); - var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); - foreach (var versionDto in versionDtos) - PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + foreach (Content c in content) + { + c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) } - protected override void PerformDeleteVersion(int id, int versionId) + return content; + } + + private IContent MapDtoToContent(DocumentDto dto) + { + IContentType? contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); + Content content = ContentBaseFactory.BuildEntity(dto, contentType); + + try { - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE id = @versionId", new { versionId }); - Database.Delete("WHERE id = @versionId", new { versionId }); + content.DisableChangeTracking(); + + // get template + if (dto.DocumentVersionDto.TemplateId.HasValue) + { + content.TemplateId = dto.DocumentVersionDto.TemplateId; + } + + // get properties - indexed by version id + var versionId = dto.DocumentVersionDto.Id; + + // TODO: shall we get published properties or not? + //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; + var publishedVersionId = dto.PublishedVersionDto?.Id ?? 0; + + var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); + var ltemp = new List> {temp}; + IDictionary properties = GetPropertyCollections(ltemp); + content.Properties = properties[dto.DocumentVersionDto.Id]; + + // set variations, if varying + if (contentType?.VariesByCulture() ?? false) + { + IDictionary> contentVariations = GetContentVariations(ltemp); + IDictionary> documentVariations = GetDocumentVariations(ltemp); + SetVariations(content, contentVariations, documentVariations); + } + + // reset dirty initial properties (U4-1946) + content.ResetDirtyProperties(false); + return content; + } + finally + { + content.EnableChangeTracking(); + } + } + + private void SetVariations(Content? content, IDictionary> contentVariations, + IDictionary> documentVariations) + { + if (content is null) + { + return; } - #endregion - - #region Persist - - protected override void PersistNewItem(IContent entity) + if (contentVariations.TryGetValue(content.VersionId, out List? contentVariation)) { - entity.AddingEntity(); + foreach (ContentVariation v in contentVariation) + { + content.SetCultureInfo(v.Culture, v.Name, v.Date); + } + } - var publishing = entity.PublishedState == PublishedState.Publishing; + if (content.PublishedVersionId > 0 && + contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) + { + foreach (ContentVariation v in contentVariation) + { + content.SetPublishInfo(v.Culture, v.Name, v.Date); + } + } - // ensure that the default template is assigned - if (entity.TemplateId.HasValue == false) - entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; + if (documentVariations.TryGetValue(content.Id, out List? documentVariation)) + { + content.SetCultureEdited(documentVariation.Where(x => x.Edited).Select(x => x.Culture)); + } + } + + private IDictionary> GetContentVariations(List> temps) + where T : class, IContentBase + { + var versions = new List(); + foreach (TempContent temp in temps) + { + versions.Add(temp.VersionId); + if (temp.PublishedVersionId > 0) + { + versions.Add(temp.PublishedVersionId); + } + } + + if (versions.Count == 0) + { + return new Dictionary>(); + } + + IEnumerable dtos = + Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, + batch + => Sql() + .Select() + .From() + .WhereIn(x => x.VersionId, batch)); + + var variations = new Dictionary>(); + + foreach (ContentVersionCultureVariationDto dto in dtos) + { + if (!variations.TryGetValue(dto.VersionId, out List? variation)) + { + variations[dto.VersionId] = variation = new List(); + } + + variation.Add(new ContentVariation + { + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Name = dto.Name, Date = dto.UpdateDate + }); + } + + return variations; + } + + private IDictionary> GetDocumentVariations(List> temps) + where T : class, IContentBase + { + IEnumerable ids = temps.Select(x => x.Id); + + IEnumerable dtos = Database.FetchByGroups(ids, + Constants.Sql.MaxParameterCount, batch => + Sql() + .Select() + .From() + .WhereIn(x => x.NodeId, batch)); + + var variations = new Dictionary>(); + + foreach (DocumentCultureVariationDto dto in dtos) + { + if (!variations.TryGetValue(dto.NodeId, out List? variation)) + { + variations[dto.NodeId] = variation = new List(); + } + + variation.Add(new DocumentVariation + { + Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), Edited = dto.Edited + }); + } + + return variations; + } + + private IEnumerable GetContentVariationDtos(IContent content, bool publishing) + { + if (content.CultureInfos is not null) + { + // create dtos for the 'current' (non-published) version, all cultures + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in content.CultureInfos) + { + yield return new ContentVersionCultureVariationDto + { + VersionId = content.VersionId, + LanguageId = + LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = + content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value + }; + } + } + + // if not publishing, we're just updating the 'current' (non-published) version, + // so there are no DTOs to create for the 'published' version which remains unchanged + if (!publishing) + { + yield break; + } + + if (content.PublishCultureInfos is not null) + { + // create dtos for the 'published' version, for published cultures (those having a name) + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in content.PublishCultureInfos) + { + yield return new ContentVersionCultureVariationDto + { + VersionId = content.PublishedVersionId, + LanguageId = + LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = cultureInfo.Culture, + Name = cultureInfo.Name, + UpdateDate = + content.GetPublishDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value + }; + } + } + } + + private IEnumerable GetDocumentVariationDtos(IContent content, + HashSet editedCultures) + { + IEnumerable + allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct + foreach (var culture in allCultures) + { + var dto = new DocumentCultureVariationDto + { + NodeId = content.Id, + LanguageId = + LanguageRepository.GetIdByIsoCode(culture) ?? + throw new InvalidOperationException("Not a valid culture."), + Culture = culture, + Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), + Available = content.IsCultureAvailable(culture), + Published = content.IsCulturePublished(culture), + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem + Edited = content.IsCultureAvailable(culture) && + (!content.IsCulturePublished(culture) || + (editedCultures != null && editedCultures.Contains(culture))) + }; + + yield return dto; + } + } + + private class ContentVariation + { + public string? Culture { get; set; } + public string? Name { get; set; } + public DateTime Date { get; set; } + } + + private class DocumentVariation + { + public string? Culture { get; set; } + public bool Edited { get; set; } + } + + #region Repository Base + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Document; + + protected override IContent? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + DocumentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + private void AddGetByQueryOrderBy(Sql sql) => + sql + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); + + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); + + // gets the COALESCE expression for variant/invariant name + private string VariantNameSqlExpression + => SqlContext.VisitDto((ccv, node) => ccv.Name ?? node.Text, "ccv") + .Sql; + + protected Sql GetBaseQuery(QueryType queryType, bool current) + { + Sql sql = SqlContext.Sql(); + + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + // R# may flag this ambiguous and red-squiggle it, but it is not + sql = sql.Select(r => + r.Select(documentDto => documentDto.ContentDto, r1 => + r1.Select(contentDto => contentDto.NodeDto)) + .Select(documentDto => documentDto.DocumentVersionDto, r1 => + r1.Select(documentVersionDto => documentVersionDto.ContentVersionDto)) + .Select(documentDto => documentDto.PublishedVersionDto, "pdv", r1 => + r1.Select(documentVersionDto => documentVersionDto!.ContentVersionDto, "pcv"))) + + // select the variant name, coalesce to the invariant name, as "variantName" + .AndSelect(VariantNameSqlExpression + " AS variantName"); + break; + } + + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + + // inner join on mandatory edited version + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.Id == right.Id) + + // left join on optional published version + .LeftJoin(nested => + nested.InnerJoin("pdv") + .On((left, right) => left.Id == right.Id && right.Published, + "pcv", "pdv"), "pcv") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcv") + + // TODO: should we be joining this when the query type is not single/many? + // left join on optional culture variation + //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code + .LeftJoin(nested => + nested.InnerJoin("lang").On( + (ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccv", "lang"), "ccv") + .On((version, ccv) => version.Id == ccv.VersionId, + aliasRight: "ccv"); + + sql + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + // this would ensure we don't get the published version - keep for reference + //sql + // .WhereAny( + // x => x.Where((x1, x2) => x1.Id != x2.Id, alias2: "pcv"), + // x => x.WhereNull(x1 => x1.Id, "pcv") + // ); + + if (current) + { + sql.Where(x => x.Current); // always get the current version + } + + return sql; + } + + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + + // ah maybe not, that what's used for eg Exists in base repo + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentSchedule + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.RedirectUrl + + " WHERE contentKey IN (SELECT uniqueId FROM " + Constants.DatabaseSchema.Tables.Node + + " WHERE id = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + + " SET startContentId = NULL WHERE startContentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Domain + " WHERE domainRootStructureID = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentVersion + " WHERE id IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.AccessRule + " WHERE accessId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.Access + + " WHERE nodeId = @id OR loginNodeId = @id OR noAccessNodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE loginNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Access + " WHERE noAccessNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + }; + return list; + } + + #endregion + + #region Versions + + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + return MapDtosToContent(Database.Fetch(sql), true); + } + + // TODO: This method needs to return a readonly version of IContent! The content returned + // from this method does not contain all of the data required to re-persist it and if that + // is attempted some odd things will occur. + // Either we create an IContentReadOnly (which ultimately we should for vNext so we can + // differentiate between methods that return entities that can be re-persisted or not), or + // in the meantime to not break API compatibility, we can add a property to IContentBase + // (or go further and have it on IUmbracoEntity): "IsReadOnly" and if that is true we throw + // an exception if that entity is passed to a Save method. + // Ideally we return "Slim" versions of content for all sorts of methods here and in ContentService. + // Perhaps another non-breaking alternative is to have new services like IContentServiceReadOnly + // which can return IContentReadOnly. + // We have the ability with `MapDtosToContent` to reduce the amount of data looked up for a + // content item. Ideally for paged data that populates list views, these would be ultra slim + // content items, there's no reason to populate those with really anything apart from property data, + // but until we do something like the above, we can't do that since it would be breaking and unclear. + public override IEnumerable GetAllVersionsSlim(int nodeId, int skip, int take) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + var pageIndex = skip / take; + + return MapDtosToContent(Database.Page(pageIndex + 1, take, sql).Items, true, + // load bare minimum, need variants though since this is used to rollback with variants + false, false); + } + + public override IContent? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single, false) + .Where(x => x.Id == versionId); + + DocumentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } + + // deletes a specific version + public override void DeleteVersion(int versionId) + { + // TODO: test object node type? + + // get the version we want to delete + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql => + tsql.Select() + .AndSelect() + .From() + .InnerJoin() + .On((c, d) => c.Id == d.Id) + .Where(x => x.Id == SqlTemplate.Arg("versionId")) + ); + DocumentVersionDto? versionDto = + Database.Fetch(template.Sql(new {versionId})).FirstOrDefault(); + + // nothing to delete + if (versionDto == null) + { + return; + } + + // don't delete the current or published version + if (versionDto.ContentVersionDto.Current) + { + throw new InvalidOperationException("Cannot delete the current version."); + } + + if (versionDto.Published) + { + throw new InvalidOperationException("Cannot delete the published version."); + } + + PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId); + } + + // deletes all versions of an entity, older than a date. + public override void DeleteVersions(int nodeId, DateTime versionDate) + { + // TODO: test object node type? + + // get the versions we want to delete, excluding the current one + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql => + tsql.Select() + .From() + .InnerJoin() + .On((c, d) => c.Id == d.Id) + .Where(x => + x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && + x.VersionDate < SqlTemplate.Arg("versionDate")) + .Where(x => !x.Published) + ); + List? versionDtos = + Database.Fetch(template.Sql(new {nodeId, versionDate})); + foreach (ContentVersionDto? versionDto in versionDtos) + { + PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + } + } + + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @versionId", new {versionId}); + Database.Delete("WHERE versionId = @versionId", new {versionId}); + Database.Delete("WHERE id = @versionId", new {versionId}); + Database.Delete("WHERE id = @versionId", new {versionId}); + } + + #endregion + + #region Persist + + protected override void PersistNewItem(IContent entity) + { + entity.AddingEntity(); + + var publishing = entity.PublishedState == PublishedState.Publishing; + + // ensure that the default template is assigned + if (entity.TemplateId.HasValue == false) + { + entity.TemplateId = entity.ContentType.DefaultTemplate?.Id; + } + + // sanitize names + SanitizeNames(entity, publishing); + + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // create the dto + DocumentDto dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; + + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + + // persist the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; + + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) + { + nodeDto.NodeId = id; + } + else + { + Database.Insert(nodeDto); + } + + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // persist the content dto + ContentDto contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + // persist the content version dto + ContentVersionDto contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = !publishing; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the document version dto + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; + documentVersionDto.Id = entity.VersionId; + if (publishing) + { + documentVersionDto.Published = true; + } + + Database.Insert(documentVersionDto); + + // and again in case we're publishing immediately + if (publishing) + { + entity.PublishedVersionId = entity.VersionId; + contentVersionDto.Id = 0; + contentVersionDto.Current = true; + contentVersionDto.Text = entity.Name; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + documentVersionDto.Id = entity.VersionId; + documentVersionDto.Published = false; + Database.Insert(documentVersionDto); + } + + // persist the property data + IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, + entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, + out HashSet? editedCultures); + foreach (PropertyDataDto propertyDataDto in propertyDataDtos) + { + Database.Insert(propertyDataDto); + } + + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && entity.PublishName != entity.Name) + { + edited = true; + } + + // persist the document dto + // at that point, when publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + { + dto.Published = true; + } + + dto.NodeId = nodeDto.NodeId; + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Insert(dto); + + // persist the variations + if (entity.ContentType.VariesByCulture()) + { + // names also impact 'edited' + // ReSharper disable once UseDeconstruction + foreach (ContentCultureInfos cultureInfo in entity.CultureInfos!) + { + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) + { + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); + } + } + + // refresh content + entity.SetCultureEdited(editedCultures!); + + // bump dates to align cultures to version + entity.AdjustDates(contentVersionDto.VersionDate, publishing); + + // insert content variations + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + + // insert document variations + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); + } + + // trigger here, before we reset Published etc + OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + + // flip the entity's published property + // this also flips its published state + // note: what depends on variations (eg PublishNames) is managed directly by the content + if (entity.PublishedState == PublishedState.Publishing) + { + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; + + SetEntityTags(entity, _tagRepository, _serializer); + } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; + + ClearEntityTags(entity, _tagRepository); + } + + PersistRelations(entity); + + entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + } + + protected override void PersistUpdatedItem(IContent entity) + { + var isEntityDirty = entity.IsDirty(); + var editedSnapshot = entity.Edited; + + // check if we need to make any database changes at all + if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) + && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) + { + return; // no change to save, do nothing, don't even update dates + } + + // whatever we do, we must check that we are saving the current version + ContentVersionDto? version = Database.Fetch(SqlContext.Sql().Select() + .From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); + if (version == null || !version.Current) + { + throw new InvalidOperationException("Cannot save a non-current version."); + } + + // update + entity.UpdatingEntity(); + + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. + // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost + // copy performance by 95% just like we did for Move + + + var publishing = entity.PublishedState == PublishedState.Publishing; + + if (!isMoving) + { + // check if we need to create a new version + if (publishing && entity.PublishedVersionId > 0) + { + // published version is not published anymore + Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)) + .Where(x => x.Id == entity.PublishedVersionId)); + } // sanitize names SanitizeNames(entity, publishing); @@ -387,80 +1122,67 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // TODO: do we really want to keep doing this here? entity.SanitizeEntityPropertiesForXmlStorage(); - // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } + } - // derive path and level from parent - var parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; + // create the dto + DocumentDto dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // update the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); - // persist the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - nodeDto.NodeId = id; - else - Database.Insert(nodeDto); - - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = !publishing; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - - // persist the document version dto - var documentVersionDto = dto.DocumentVersionDto; - documentVersionDto.Id = entity.VersionId; + // update the content & document version dtos + ContentVersionDto contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + DocumentVersionDto documentVersionDto = dto.DocumentVersionDto; if (publishing) - documentVersionDto.Published = true; - Database.Insert(documentVersionDto); + { + documentVersionDto.Published = true; // now published + contentVersionDto.Current = false; // no more current + } - // and again in case we're publishing immediately + // Ensure existing version retains current preventCleanup flag (both saving and publishing). + contentVersionDto.PreventCleanup = version.PreventCleanup; + + Database.Update(contentVersionDto); + Database.Update(documentVersionDto); + + // and, if publishing, insert new content & document version dtos if (publishing) { entity.PublishedVersionId = entity.VersionId; - contentVersionDto.Id = 0; - contentVersionDto.Current = true; - contentVersionDto.Text = entity.Name; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - documentVersionDto.Id = entity.VersionId; - documentVersionDto.Published = false; + contentVersionDto.Id = 0; // want a new id + contentVersionDto.Current = true; // current version + contentVersionDto.Text = entity.Name; + contentVersionDto.PreventCleanup = false; // new draft version disregards prevent cleanup flag + Database.Insert(contentVersionDto); + entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id + + documentVersionDto.Published = false; // non-published version Database.Insert(documentVersionDto); } - // persist the property data - IEnumerable propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, entity.PublishedVersionId, entity.Properties, LanguageRepository, out var edited, out HashSet? editedCultures); - foreach (PropertyDataDto propertyDataDto in propertyDataDtos) - { - Database.Insert(propertyDataDto); - } + // replace the property data (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + + // insert property data + ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, + out HashSet? editedCultures); // if !publishing, we may have a new name != current publish name, // also impacts 'edited' @@ -469,19 +1191,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement edited = true; } - // persist the document dto - // at that point, when publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) + // To establish the new value of "edited" we compare all properties publishedValue to editedValue and look + // for differences. + // + // If we SaveAndPublish but the publish fails (e.g. already scheduled for release) + // we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison. + // + // This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save + // would change edited back to false. + if (!publishing && editedSnapshot) { - dto.Published = true; + edited = true; } - dto.NodeId = nodeDto.NodeId; - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Insert(dto); - - // persist the variations if (entity.ContentType.VariesByCulture()) { // names also impact 'edited' @@ -490,7 +1212,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) { - (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); + edited = true; + (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo + .Culture); + + // TODO: change tracking + // at the moment, we don't do any dirty tracking on property values, so we don't know whether the + // culture has just been edited or not, so we don't update its update date - that date only changes + // when the name is set, and it all works because the controller does it - but, if someone uses a + // service to change a property value and save (without setting name), the update date does not change. } } @@ -500,6 +1230,23 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement // bump dates to align cultures to version entity.AdjustDates(contentVersionDto.VersionDate, publishing); + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + Sql deleteContentVariations = Sql().Delete() + .Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); + + // replace the document version variations (rather than updating) + Sql deleteDocumentVariations = Sql().Delete() + .Where(x => x.NodeId == entity.Id); + Database.Execute(deleteDocumentVariations); + + // TODO: NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // (same in PersistNewItem above) + // insert content variations Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); @@ -507,12 +1254,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); } - // trigger here, before we reset Published etc - OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + // update the document dto + // at that point, when un/publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + { + dto.Published = true; + } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + dto.Published = false; + } + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Update(dto); + + // if entity is publishing, update tags, else leave tags there + // means that implicitly unpublished, or trashed, entities *still* have tags in db + if (entity.PublishedState == PublishedState.Publishing) + { + SetEntityTags(entity, _tagRepository, _serializer); + } + } + + // trigger here, before we reset Published etc + OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); + + if (!isMoving) + { // flip the entity's published property // this also flips its published state - // note: what depends on variations (eg PublishNames) is managed directly by the content if (entity.PublishedState == PublishedState.Publishing) { entity.Published = true; @@ -536,1091 +1307,427 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement PersistRelations(entity); - entity.ResetDirtyProperties(); - - // troubleshooting - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} + // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? } - protected override void PersistUpdatedItem(IContent entity) + entity.ResetDirtyProperties(); + + // troubleshooting + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) + //{ + // Debugger.Break(); + // throw new Exception("oops"); + //} + } + + /// + public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) + { + if (content == null) { - var isEntityDirty = entity.IsDirty(); - var editedSnapshot = entity.Edited; - - // check if we need to make any database changes at all - if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) - && !isEntityDirty && !entity.IsAnyUserPropertyDirty()) - { - return; // no change to save, do nothing, don't even update dates - } - - // whatever we do, we must check that we are saving the current version - var version = Database.Fetch(SqlContext.Sql().Select().From().Where(x => x.Id == entity.VersionId)).FirstOrDefault(); - if (version == null || !version.Current) - throw new InvalidOperationException("Cannot save a non-current version."); - - // update - entity.UpdatingEntity(); - - // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. - // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.IsMoving(); - // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. - // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost - // copy performance by 95% just like we did for Move - - - var publishing = entity.PublishedState == PublishedState.Publishing; - - if (!isMoving) - { - // check if we need to create a new version - if (publishing && entity.PublishedVersionId > 0) - { - // published version is not published anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); - } - - // sanitize names - SanitizeNames(entity, publishing); - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - var parent = GetParentNodeDto(entity.ParentId); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } - } - - // create the dto - var dto = ContentBaseFactory.BuildDto(entity, NodeObjectTypeId); - - // update the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - if (!isMoving) - { - // update the content dto - Database.Update(dto.ContentDto); - - // update the content & document version dtos - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - var documentVersionDto = dto.DocumentVersionDto; - if (publishing) - { - documentVersionDto.Published = true; // now published - contentVersionDto.Current = false; // no more current - } - - // Ensure existing version retains current preventCleanup flag (both saving and publishing). - contentVersionDto.PreventCleanup = version.PreventCleanup; - - Database.Update(contentVersionDto); - Database.Update(documentVersionDto); - - // and, if publishing, insert new content & document version dtos - if (publishing) - { - entity.PublishedVersionId = entity.VersionId; - - contentVersionDto.Id = 0; // want a new id - contentVersionDto.Current = true; // current version - contentVersionDto.Text = entity.Name; - contentVersionDto.PreventCleanup = false; // new draft version disregards prevent cleanup flag - Database.Insert(contentVersionDto); - entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - - documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); - } - - // replace the property data (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; - - // insert property data - ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, out HashSet? editedCultures); - - // if !publishing, we may have a new name != current publish name, - // also impacts 'edited' - if (!publishing && entity.PublishName != entity.Name) - { - edited = true; - } - - // To establish the new value of "edited" we compare all properties publishedValue to editedValue and look - // for differences. - // - // If we SaveAndPublish but the publish fails (e.g. already scheduled for release) - // we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison. - // - // This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save - // would change edited back to false. - if (!publishing && editedSnapshot) - { - edited = true; - } - - if (entity.ContentType.VariesByCulture()) - { - // names also impact 'edited' - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in entity.CultureInfos!) - { - if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) - { - edited = true; - (editedCultures ??= new HashSet(StringComparer.OrdinalIgnoreCase)).Add(cultureInfo.Culture); - - // TODO: change tracking - // at the moment, we don't do any dirty tracking on property values, so we don't know whether the - // culture has just been edited or not, so we don't update its update date - that date only changes - // when the name is set, and it all works because the controller does it - but, if someone uses a - // service to change a property value and save (without setting name), the update date does not change. - } - } - - // refresh content - entity.SetCultureEdited(editedCultures!); - - // bump dates to align cultures to version - entity.AdjustDates(contentVersionDto.VersionDate, publishing); - - // replace the content version variations (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deleteContentVariations); - - // replace the document version variations (rather than updating) - var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); - Database.Execute(deleteDocumentVariations); - - // TODO: NPoco InsertBulk issue? - // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) - // but by using SQL Server and updating a variants name will cause: Unable to cast object of type - // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. - // (same in PersistNewItem above) - - // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); - - // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures!)); - } - - // update the document dto - // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) - { - dto.Published = true; - } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - dto.Published = false; - } - - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Update(dto); - - // if entity is publishing, update tags, else leave tags there - // means that implicitly unpublished, or trashed, entities *still* have tags in db - if (entity.PublishedState == PublishedState.Publishing) - { - SetEntityTags(entity, _tagRepository, _serializer); - } - } - - // trigger here, before we reset Published etc - OnUowRefreshedEntity(new ContentRefreshNotification(entity, new EventMessages())); - - if (!isMoving) - { - // flip the entity's published property - // this also flips its published state - if (entity.PublishedState == PublishedState.Publishing) - { - entity.Published = true; - entity.PublishTemplateId = entity.TemplateId; - entity.PublisherId = entity.WriterId; - entity.PublishName = entity.Name; - entity.PublishDate = entity.UpdateDate; - - SetEntityTags(entity, _tagRepository, _serializer); - } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - entity.Published = false; - entity.PublishTemplateId = null; - entity.PublisherId = null; - entity.PublishName = null; - entity.PublishDate = null; - - ClearEntityTags(entity, _tagRepository); - } - - PersistRelations(entity); - - // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? - } - - entity.ResetDirtyProperties(); - - // troubleshooting - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE published=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} - //if (Database.ExecuteScalar($"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.DocumentVersion} JOIN {Constants.DatabaseSchema.Tables.ContentVersion} ON {Constants.DatabaseSchema.Tables.DocumentVersion}.id={Constants.DatabaseSchema.Tables.ContentVersion}.id WHERE [current]=1 AND nodeId=" + content.Id) > 1) - //{ - // Debugger.Break(); - // throw new Exception("oops"); - //} + throw new ArgumentNullException(nameof(content)); } - /// - public void PersistContentSchedule(IContent content, ContentScheduleCollection contentSchedule) + if (contentSchedule == null) { - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - - if (contentSchedule == null) - { - throw new ArgumentNullException(nameof(contentSchedule)); - } - - var schedules = ContentBaseFactory.BuildScheduleDto(content, contentSchedule, LanguageRepository).ToList(); - - //remove any that no longer exist - var ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); - Database.Execute(Sql() - .Delete() - .Where(x => x.NodeId == content.Id) - .WhereNotIn(x => x.Id, ids)); - - //add/update the rest - foreach (var schedule in schedules) - { - if (schedule.Model.Id == Guid.Empty) - { - schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); - Database.Insert(schedule.Dto); - } - else - { - Database.Update(schedule.Dto); - } - } + throw new ArgumentNullException(nameof(contentSchedule)); } - protected override void PersistDeletedItem(IContent entity) + var schedules = ContentBaseFactory.BuildScheduleDto(content, contentSchedule, LanguageRepository).ToList(); + + //remove any that no longer exist + IEnumerable ids = schedules.Where(x => x.Model.Id != Guid.Empty).Select(x => x.Model.Id).Distinct(); + Database.Execute(Sql() + .Delete() + .Where(x => x.NodeId == content.Id) + .WhereNotIn(x => x.Id, ids)); + + //add/update the rest + foreach ((ContentSchedule Model, ContentScheduleDto Dto) schedule in schedules) { - // Raise event first else potential FK issues - OnUowRemovingEntity(entity); - - //We need to clear out all access rules but we need to do this in a manual way since - // nothing in that table is joined to a content id - var subQuery = SqlContext.Sql() - .Select(x => x.AccessId) - .From() - .InnerJoin() - .On(left => left.AccessId, right => right.Id) - .Where(dto => dto.NodeId == entity.Id); - Database.Execute(SqlContext.SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); - - //now let the normal delete clauses take care of everything else - base.PersistDeletedItem(entity); - } - - #endregion - - #region Content Repository - - public int CountPublished(string? contentTypeAlias = null) - { - var sql = SqlContext.Sql(); - if (contentTypeAlias.IsNullOrWhiteSpace()) + if (schedule.Model.Id == Guid.Empty) { - sql.SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) - .Where(x => x.Published); + schedule.Model.Id = schedule.Dto.Id = Guid.NewGuid(); + Database.Insert(schedule.Dto); } else { - sql.SelectCount() - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) - .Where(x => x.Alias == contentTypeAlias) - .Where(x => x.Published); + Database.Update(schedule.Dto); } - - return Database.ExecuteScalar(sql); } + } - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + protected override void PersistDeletedItem(IContent entity) + { + // Raise event first else potential FK issues + OnUowRemovingEntity(entity); + + //We need to clear out all access rules but we need to do this in a manual way since + // nothing in that table is joined to a content id + Sql subQuery = SqlContext.Sql() + .Select(x => x.AccessId) + .From() + .InnerJoin() + .On(left => left.AccessId, right => right.Id) + .Where(dto => dto.NodeId == entity.Id); + Database.Execute(SqlContext.SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); + + //now let the normal delete clauses take care of everything else + base.PersistDeletedItem(entity); + } + + #endregion + + #region Content Repository + + public int CountPublished(string? contentTypeAlias = null) + { + Sql sql = SqlContext.Sql(); + if (contentTypeAlias.IsNullOrWhiteSpace()) { - PermissionRepository.ReplaceEntityPermissions(permissionSet); - } - - /// - /// Assigns a single permission to the current content item for the specified group ids - /// - /// - /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) - { - PermissionRepository.AssignEntityPermission(entity, permission, groupIds); - } - - public EntityPermissionCollection GetPermissionsForEntity(int entityId) - { - return PermissionRepository.GetPermissionsForEntity(entityId); - } - - /// - /// Used to add/update a permission for a content item - /// - /// - public void AddOrUpdatePermissions(ContentPermissionSet permission) - { - PermissionRepository.Save(permission); - } - - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - Sql? filterSql = null; - - // if we have a filter, map its clauses to an Sql statement - if (filter != null) - { - // if the clause works on "name", we need to swap the field and use the variantName instead, - // so that querying also works on variant content (for instance when searching a listview). - - // figure out how the "name" field is going to look like - so we can look for it - var nameField = SqlContext.VisitModelField(x => x.Name); - - filterSql = Sql(); - foreach (var filterClause in filter.GetWhereClauses()) - { - var clauseSql = filterClause.Item1; - var clauseArgs = filterClause.Item2; - - // replace the name field - // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here - clauseSql = clauseSql.Replace(nameField, VariantNameSqlExpression); - - // append the clause - filterSql.Append($"AND ({clauseSql})", clauseArgs); - } - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - public bool IsPathPublished(IContent? content) - { - // fail fast - if (content?.Path.StartsWith("-1,-20,") ?? false) - return false; - - // succeed fast - if (content?.ParentId == -1) - return content.Published; - - var ids = content?.Path.Split(Constants.CharArrays.Comma).Skip(1).Select(s => int.Parse(s, CultureInfo.InvariantCulture)); - - var sql = SqlContext.Sql() - .SelectCount(x => x.NodeId) + sql.SelectCount() .From() - .InnerJoin().On((n, d) => n.NodeId == d.NodeId && d.Published) - .WhereIn(x => x.NodeId, ids); - - var count = Database.ExecuteScalar(sql); - return count == content?.Level; + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Published); + } + else + { + sql.SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Trashed == false) + .Where(x => x.Alias == contentTypeAlias) + .Where(x => x.Published); } - #endregion + return Database.ExecuteScalar(sql); + } - #region Recycle Bin + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) => + PermissionRepository.ReplaceEntityPermissions(permissionSet); - public override int RecycleBinId => Cms.Core.Constants.System.RecycleBinContent; + /// + /// Assigns a single permission to the current content item for the specified group ids + /// + /// + /// + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) => + PermissionRepository.AssignEntityPermission(entity, permission, groupIds); - public bool RecycleBinSmells() + public EntityPermissionCollection GetPermissionsForEntity(int entityId) => + PermissionRepository.GetPermissionsForEntity(entityId); + + /// + /// Used to add/update a permission for a content item + /// + /// + public void AddOrUpdatePermissions(ContentPermissionSet permission) => PermissionRepository.Save(permission); + + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) + { + Sql? filterSql = null; + + // if we have a filter, map its clauses to an Sql statement + if (filter != null) { - var cache = _appCaches.RuntimeCache; - var cacheKey = CacheKeys.ContentRecycleBinCacheKey; + // if the clause works on "name", we need to swap the field and use the variantName instead, + // so that querying also works on variant content (for instance when searching a listview). - // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); - } + // figure out how the "name" field is going to look like - so we can look for it + var nameField = SqlContext.VisitModelField(x => x.Name); - #endregion - - #region Read Repository implementation for Guid keys - - public IContent? Get(Guid id) - { - return _contentByGuidReadRepository.Get(id); - } - - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return _contentByGuidReadRepository.GetMany(ids); - } - - public bool Exists(Guid id) - { - return _contentByGuidReadRepository.Exists(id); - } - - // reading repository purely for looking up by GUID - // TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this - private class ContentByGuidReadRepository : EntityRepositoryBase - { - private readonly DocumentRepository _outerRepo; - - public ContentByGuidReadRepository(DocumentRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) + filterSql = Sql(); + foreach (Tuple filterClause in filter.GetWhereClauses()) { - _outerRepo = outerRepo; - } + var clauseSql = filterClause.Item1; + var clauseArgs = filterClause.Item2; - protected override IContent? PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Single) - .Where(x => x.UniqueId == id); + // replace the name field + // we cannot reference an aliased field in a WHERE clause, so have to repeat the expression here + clauseSql = clauseSql.Replace(nameField, VariantNameSqlExpression); - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = _outerRepo.MapDtoToContent(dto); - - return content; - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Many); - if (ids?.Length > 0) - sql.WhereIn(x => x.UniqueId, ids); - - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistNewItem(IContent entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistUpdatedItem(IContent entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); + // append the clause + filterSql.Append($"AND ({clauseSql})", clauseArgs); } } - #endregion + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } - #region Schedule - - /// - public void ClearSchedule(DateTime date) + public bool IsPathPublished(IContent? content) + { + // fail fast + if (content?.Path.StartsWith("-1,-20,") ?? false) { - var sql = Sql().Delete().Where(x => x.Date <= date); - Database.Execute(sql); + return false; } - /// - public void ClearSchedule(DateTime date, ContentScheduleAction action) + // succeed fast + if (content?.ParentId == -1) { - var a = action.ToString(); - var sql = Sql().Delete().Where(x => x.Date <= date && x.Action == a); - Database.Execute(sql); + return content.Published; } - private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + IEnumerable? ids = content?.Path.Split(Constants.CharArrays.Comma).Skip(1) + .Select(s => int.Parse(s, CultureInfo.InvariantCulture)); + + Sql sql = SqlContext.Sql() + .SelectCount(x => x.NodeId) + .From() + .InnerJoin().On((n, d) => n.NodeId == d.NodeId && d.Published) + .WhereIn(x => x.NodeId, ids); + + var count = Database.ExecuteScalar(sql); + return count == content?.Level; + } + + #endregion + + #region Recycle Bin + + public override int RecycleBinId => Constants.System.RecycleBinContent; + + public bool RecycleBinSmells() + { + IAppPolicyCache cache = _appCaches.RuntimeCache; + var cacheKey = CacheKeys.ContentRecycleBinCacheKey; + + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } + + #endregion + + #region Read Repository implementation for Guid keys + + public IContent? Get(Guid id) => _contentByGuidReadRepository.Get(id); + + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + _contentByGuidReadRepository.GetMany(ids); + + public bool Exists(Guid id) => _contentByGuidReadRepository.Exists(id); + + // reading repository purely for looking up by GUID + // TODO: ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this + private class ContentByGuidReadRepository : EntityRepositoryBase + { + private readonly DocumentRepository _outerRepo; + + public ContentByGuidReadRepository(DocumentRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) => + _outerRepo = outerRepo; + + protected override IContent? PerformGet(Guid id) { - var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql - .SelectCount() - .From() - .Where(x => x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); + Sql sql = _outerRepo.GetBaseQuery(QueryType.Single) + .Where(x => x.UniqueId == id); - var sql = template.Sql(action.ToString(), date); - return sql; - } + DocumentDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - public bool HasContentForExpiration(DateTime date) - { - var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); - return Database.ExecuteScalar(sql) > 0; - } - - public bool HasContentForRelease(DateTime date) - { - var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); - return Database.ExecuteScalar(sql) > 0; - } - - /// - public IEnumerable GetContentForRelease(DateTime date) - { - var action = ContentScheduleAction.Release.ToString(); - - var sql = GetBaseQuery(QueryType.Many) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date <= date)); - - AddGetByQueryOrderBy(sql); - - return MapDtosToContent(Database.Fetch(sql)); - } - - /// - public IEnumerable GetContentForExpiration(DateTime date) - { - var action = ContentScheduleAction.Expire.ToString(); - - var sql = GetBaseQuery(QueryType.Many) - .WhereIn(x => x.NodeId, Sql() - .Select(x => x.NodeId) - .From() - .Where(x => x.Action == action && x.Date <= date)); - - AddGetByQueryOrderBy(sql); - - return MapDtosToContent(Database.Fetch(sql)); - } - - #endregion - - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) - { - // note: 'updater' is the user who created the latest draft version, - // we don't have an 'updater' per culture (should we?) - if (ordering.OrderBy.InvariantEquals("updater")) + if (dto == null) { - var joins = Sql() - .InnerJoin("updaterUser").On((version, user) => version.UserId == user.Id, aliasRight: "updaterUser"); - - // see notes in ApplyOrdering: the field MUST be selected + aliased - sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering "), sql.Arguments); - - sql = InsertJoins(sql, joins); - - return "ordering"; + return null; } - if (ordering.OrderBy.InvariantEquals("published")) - { - // no culture = can only work on the global 'published' flag - if (ordering.Culture.IsNullOrWhiteSpace()) - { - // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have - // the whole CASE fragment in ORDER BY due to it not being detected by NPoco - sql = Sql(InsertBefore(sql, "FROM", ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering "), sql.Arguments); - return "ordering"; - } - - // invariant: left join will yield NULL and we must use pcv to determine published - // variant: left join may yield NULL or something, and that determines published - - - var joins = Sql() - .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype") - // left join on optional culture variation - //the magic "[[[ISOCODE]]]" parameter value will be replaced in ContentRepositoryBase.GetPage() by the actual ISO code - .LeftJoin(nested => - nested.InnerJoin("langp").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == "[[[ISOCODE]]]", "ccvp", "langp"), "ccvp") - .On((version, ccv) => version.Id == ccv.VersionId, aliasLeft: "pcv", aliasRight: "ccvp"); - - sql = InsertJoins(sql, joins); - - // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have - // the whole CASE fragment in ORDER BY due to it not being detected by NPoco - var sqlText = InsertBefore(sql.SQL, "FROM", - - // when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id, - // otherwise check if there's a version culture variation for the lang, via ccv.id - ", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccvp.id IS NULL THEN 0 ELSE 1 END) END) AS ordering "); // trailing space is important! - - sql = Sql(sqlText, sql.Arguments); - - return "ordering"; - } - - return base.ApplySystemOrdering(ref sql, ordering); - } - - private IEnumerable MapDtosToContent(List dtos, - bool withCache = false, - bool loadProperties = true, - bool loadTemplates = true, - bool loadVariants = true) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var templateIds = new List(); - - var content = new Content[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) - { - content[i] = (Content)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out var contentType) == false) - contentTypes[contentTypeId] = contentType = _contentTypeRepository.Get(contentTypeId); - - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - if (loadTemplates) - { - // need templates - var templateId = dto.DocumentVersionDto.TemplateId; - if (templateId.HasValue) - templateIds.Add(templateId.Value); - if (dto.Published) - { - templateId = dto.PublishedVersionDto.TemplateId; - if (templateId.HasValue) - templateIds.Add(templateId.Value); - } - } - - // need temps, for properties, templates and variations - var versionId = dto.DocumentVersionDto.Id; - var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType, c) - { - Template1Id = dto.DocumentVersionDto.TemplateId - }; - if (dto.Published) - temp.Template2Id = dto.PublishedVersionDto.TemplateId; - temps.Add(temp); - } - - Dictionary? templates = null; - if (loadTemplates) - { - // load all required templates in 1 query, and index - templates = _templateRepository.GetMany(templateIds.ToArray())? - .ToDictionary(x => x.Id, x => x); - } - - IDictionary? properties = null; - if (loadProperties) - { - // load all properties for all documents from database in 1 query - indexed by version id - properties = GetPropertyCollections(temps); - } - - // assign templates and properties - foreach (var temp in temps) - { - if (loadTemplates) - { - // set the template ID if it matches an existing template - if (temp.Template1Id.HasValue && (templates?.ContainsKey(temp.Template1Id.Value) ?? false)) - temp.Content!.TemplateId = temp.Template1Id; - if (temp.Template2Id.HasValue && (templates?.ContainsKey(temp.Template2Id.Value) ?? false)) - temp.Content!.PublishTemplateId = temp.Template2Id; - } - - - // set properties - if (loadProperties) - { - if (properties?.ContainsKey(temp.VersionId) ?? false) - temp.Content!.Properties = properties[temp.VersionId]; - else - throw new InvalidOperationException($"No property data found for version: '{temp.VersionId}'."); - } - } - - if (loadVariants) - { - // set variations, if varying - temps = temps.Where(x => x.ContentType?.VariesByCulture() ?? false).ToList(); - if (temps.Count > 0) - { - // load all variations for all documents from database, in one query - var contentVariations = GetContentVariations(temps); - var documentVariations = GetDocumentVariations(temps); - foreach (var temp in temps) - SetVariations(temp.Content, contentVariations, documentVariations); - } - } - - - - foreach (var c in content) - c.ResetDirtyProperties(false); // reset dirty initial properties (U4-1946) + IContent content = _outerRepo.MapDtoToContent(dto); return content; } - private IContent MapDtoToContent(DocumentDto dto) + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - var contentType = _contentTypeRepository.Get(dto.ContentDto.ContentTypeId); - var content = ContentBaseFactory.BuildEntity(dto, contentType); - - try + Sql sql = _outerRepo.GetBaseQuery(QueryType.Many); + if (ids?.Length > 0) { - content.DisableChangeTracking(); - - // get template - if (dto.DocumentVersionDto.TemplateId.HasValue) - content.TemplateId = dto.DocumentVersionDto.TemplateId; - - // get properties - indexed by version id - var versionId = dto.DocumentVersionDto.Id; - - // TODO: shall we get published properties or not? - //var publishedVersionId = dto.Published ? dto.PublishedVersionDto.Id : 0; - var publishedVersionId = dto.PublishedVersionDto?.Id ?? 0; - - var temp = new TempContent(dto.NodeId, versionId, publishedVersionId, contentType); - var ltemp = new List> { temp }; - var properties = GetPropertyCollections(ltemp); - content.Properties = properties[dto.DocumentVersionDto.Id]; - - // set variations, if varying - if (contentType?.VariesByCulture() ?? false) - { - var contentVariations = GetContentVariations(ltemp); - var documentVariations = GetDocumentVariations(ltemp); - SetVariations(content, contentVariations, documentVariations); - } - - // reset dirty initial properties (U4-1946) - content.ResetDirtyProperties(false); - return content; - } - finally - { - content.EnableChangeTracking(); + sql.WhereIn(x => x.UniqueId, ids); } + + return _outerRepo.MapDtosToContent(Database.Fetch(sql)); } - /// - public ContentScheduleCollection GetContentSchedule(int contentId) - { - var result = new ContentScheduleCollection(); + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); - var scheduleDtos = Database.Fetch(Sql() - .Select() + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistNewItem(IContent entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistUpdatedItem(IContent entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + } + + #endregion + + #region Schedule + + /// + public void ClearSchedule(DateTime date) + { + Sql sql = Sql().Delete().Where(x => x.Date <= date); + Database.Execute(sql); + } + + /// + public void ClearSchedule(DateTime date, ContentScheduleAction action) + { + var a = action.ToString(); + Sql sql = Sql().Delete() + .Where(x => x.Date <= date && x.Action == a); + Database.Execute(sql); + } + + private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + { + SqlTemplate template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", + tsql => tsql + .SelectCount() .From() - .Where(x => x.NodeId == contentId )); + .Where(x => + x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); - foreach (var scheduleDto in scheduleDtos) + Sql sql = template.Sql(action.ToString(), date); + return sql; + } + + public bool HasContentForExpiration(DateTime date) + { + Sql sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); + return Database.ExecuteScalar(sql) > 0; + } + + public bool HasContentForRelease(DateTime date) + { + Sql sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); + return Database.ExecuteScalar(sql) > 0; + } + + /// + public IEnumerable GetContentForRelease(DateTime date) + { + var action = ContentScheduleAction.Release.ToString(); + + Sql sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + /// + public IEnumerable GetContentForExpiration(DateTime date) + { + var action = ContentScheduleAction.Expire.ToString(); + + Sql sql = GetBaseQuery(QueryType.Many) + .WhereIn(x => x.NodeId, Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.Action == action && x.Date <= date)); + + AddGetByQueryOrderBy(sql); + + return MapDtosToContent(Database.Fetch(sql)); + } + + #endregion + + #region Utilities + + private void SanitizeNames(IContent content, bool publishing) + { + // a content item *must* have an invariant name, and invariant published name + // else we just cannot write the invariant rows (node, content version...) to the database + + // ensure that we have an invariant name + // invariant content = must be there already, else throw + // variant content = update with default culture or anything really + EnsureInvariantNameExists(content); + + // ensure that invariant name is unique + EnsureInvariantNameIsUnique(content); + + // and finally, + // ensure that each culture has a unique node name + // no published name = not published + // else, it needs to be unique + EnsureVariantNamesAreUnique(content, publishing); + } + + private void EnsureInvariantNameExists(IContent content) + { + if (content.ContentType.VariesByCulture()) + { + // content varies by culture + // then it must have at least a variant name, else it makes no sense + if (content.CultureInfos?.Count == 0) { - result.Add(new ContentSchedule(scheduleDto.Id, - LanguageRepository.GetIsoCodeById(scheduleDto.LanguageId) ?? string.Empty, - scheduleDto.Date, - scheduleDto.Action == ContentScheduleAction.Release.ToString() - ? ContentScheduleAction.Release - : ContentScheduleAction.Expire)); + throw new InvalidOperationException("Cannot save content with an empty name."); } - return result; + // and then, we need to set the invariant name implicitly, + // using the default culture if it has a name, otherwise anything we can + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + content.Name = defaultCulture != null && + (content.CultureInfos?.TryGetValue(defaultCulture, out ContentCultureInfos cultureName) ?? + false) + ? cultureName.Name! + : content.CultureInfos![0].Name!; } - - private void SetVariations(Content? content, IDictionary> contentVariations, IDictionary> documentVariations) + else { - if (content is null) + // content is invariant, and invariant content must have an explicit invariant name + if (string.IsNullOrWhiteSpace(content.Name)) { - return; - } - if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) - foreach (var v in contentVariation) - content.SetCultureInfo(v.Culture, v.Name, v.Date); - - if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) - { - foreach (var v in contentVariation) - content.SetPublishInfo(v.Culture, v.Name, v.Date); - } - - if (documentVariations.TryGetValue(content.Id, out var documentVariation)) - content.SetCultureEdited(documentVariation.Where(x => x.Edited).Select(x => x.Culture)); - } - - private IDictionary> GetContentVariations(List> temps) - where T : class, IContentBase - { - var versions = new List(); - foreach (var temp in temps) - { - versions.Add(temp.VersionId); - if (temp.PublishedVersionId > 0) - versions.Add(temp.PublishedVersionId); - } - if (versions.Count == 0) - return new Dictionary>(); - - var dtos = Database.FetchByGroups(versions, Constants.Sql.MaxParameterCount, batch - => Sql() - .Select() - .From() - .WhereIn(x => x.VersionId, batch)); - - var variations = new Dictionary>(); - - foreach (var dto in dtos) - { - if (!variations.TryGetValue(dto.VersionId, out var variation)) - variations[dto.VersionId] = variation = new List(); - - variation.Add(new ContentVariation - { - Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), - Name = dto.Name, - Date = dto.UpdateDate - }); - } - - return variations; - } - - private IDictionary> GetDocumentVariations(List> temps) - where T : class, IContentBase - { - var ids = temps.Select(x => x.Id); - - var dtos = Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => - Sql() - .Select() - .From() - .WhereIn(x => x.NodeId, batch)); - - var variations = new Dictionary>(); - - foreach (var dto in dtos) - { - if (!variations.TryGetValue(dto.NodeId, out var variation)) - variations[dto.NodeId] = variation = new List(); - - variation.Add(new DocumentVariation - { - Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId), - Edited = dto.Edited - }); - } - - return variations; - } - - private IEnumerable GetContentVariationDtos(IContent content, bool publishing) - { - if (content.CultureInfos is not null) - { - // create dtos for the 'current' (non-published) version, all cultures - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.CultureInfos) - yield return new ContentVersionCultureVariationDto - { - VersionId = content.VersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = cultureInfo.Culture, - Name = cultureInfo.Name, - UpdateDate = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value - }; - } - - // if not publishing, we're just updating the 'current' (non-published) version, - // so there are no DTOs to create for the 'published' version which remains unchanged - if (!publishing) - yield break; - - if (content.PublishCultureInfos is not null) - { - // create dtos for the 'published' version, for published cultures (those having a name) - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in content.PublishCultureInfos) - yield return new ContentVersionCultureVariationDto - { - VersionId = content.PublishedVersionId, - LanguageId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = cultureInfo.Culture, - Name = cultureInfo.Name, - UpdateDate = content.GetPublishDate(cultureInfo.Culture) ?? DateTime.MinValue // we *know* there is a value - }; + throw new InvalidOperationException("Cannot save content with an empty name."); } } + } - private IEnumerable GetDocumentVariationDtos(IContent content, HashSet editedCultures) - { - var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct - foreach (var culture in allCultures) - { - var dto = new DocumentCultureVariationDto - { - NodeId = content.Id, - LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), - Culture = culture, + private void EnsureInvariantNameIsUnique(IContent content) => + content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); - Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), - Available = content.IsCultureAvailable(culture), - Published = content.IsCulturePublished(culture), - // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem - Edited = content.IsCultureAvailable(culture) && - (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) - }; + protected override string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) => + EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); - yield return dto; - } - - } - - private class ContentVariation - { - public string? Culture { get; set; } - public string? Name { get; set; } - public DateTime Date { get; set; } - } - - private class DocumentVariation - { - public string? Culture { get; set; } - public bool Edited { get; set; } - } - - #region Utilities - - private void SanitizeNames(IContent content, bool publishing) - { - // a content item *must* have an invariant name, and invariant published name - // else we just cannot write the invariant rows (node, content version...) to the database - - // ensure that we have an invariant name - // invariant content = must be there already, else throw - // variant content = update with default culture or anything really - EnsureInvariantNameExists(content); - - // ensure that invariant name is unique - EnsureInvariantNameIsUnique(content); - - // and finally, - // ensure that each culture has a unique node name - // no published name = not published - // else, it needs to be unique - EnsureVariantNamesAreUnique(content, publishing); - } - - private void EnsureInvariantNameExists(IContent content) - { - if (content.ContentType.VariesByCulture()) - { - // content varies by culture - // then it must have at least a variant name, else it makes no sense - if (content.CultureInfos?.Count == 0) - throw new InvalidOperationException("Cannot save content with an empty name."); - - // and then, we need to set the invariant name implicitly, - // using the default culture if it has a name, otherwise anything we can - var defaultCulture = LanguageRepository.GetDefaultIsoCode(); - content.Name = defaultCulture != null && (content.CultureInfos?.TryGetValue(defaultCulture, out var cultureName) ?? false) - ? cultureName.Name! - : content.CultureInfos![0].Name!; - } - else - { - // content is invariant, and invariant content must have an explicit invariant name - if (string.IsNullOrWhiteSpace(content.Name)) - throw new InvalidOperationException("Cannot save content with an empty name."); - } - } - - private void EnsureInvariantNameIsUnique(IContent content) - { - content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); - } - - protected override string? EnsureUniqueNodeName(int parentId, string? nodeName, int id = 0) - { - return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); - } - - private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql + private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get( + "Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql .Select(x => x.Id, x => x.Name, x => x.LanguageId) .From() - .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin() + .On(x => x.Id, x => x.VersionId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.Current == SqlTemplate.Arg("current")) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && @@ -1628,58 +1735,73 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureVariantNamesAreUnique(IContent content, bool publishing) + private void EnsureVariantNamesAreUnique(IContent content, bool publishing) + { + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos?.Count == 0) { - if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureInfos?.Count == 0) - return; - - // get names per culture, at same level (ie all siblings) - var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); - var names = Database.Fetch(sql) - .GroupBy(x => x.LanguageId) - .ToDictionary(x => x.Key, x => x); - - if (names.Count == 0) - return; - - // note: the code below means we are going to unique-ify every culture names, regardless - // of whether the name has changed (ie the culture has been updated) - some saving culture - // fr-FR could cause culture en-UK name to change - not sure that is clean - - if (content.CultureInfos is null) - { - return; - } - foreach (var cultureInfo in content.CultureInfos) - { - var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); - if (!langId.HasValue) - continue; - if (!names.TryGetValue(langId.Value, out var cultureNames)) - continue; - - // get a unique name - var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); - - if (uniqueName == content.GetCultureName(cultureInfo.Culture)) - continue; - - // update the name, and the publish name if published - content.SetCultureName(uniqueName, cultureInfo.Culture); - if (publishing && (content.PublishCultureInfos?.ContainsKey(cultureInfo.Culture) ?? false)) - content.SetPublishInfo(cultureInfo.Culture, uniqueName, DateTime.Now); //TODO: This is weird, this call will have already been made in the SetCultureName - } + return; } - // ReSharper disable once ClassNeverInstantiated.Local - private class CultureNodeName + // get names per culture, at same level (ie all siblings) + Sql sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); + var names = Database.Fetch(sql) + .GroupBy(x => x.LanguageId) + .ToDictionary(x => x.Key, x => x); + + if (names.Count == 0) { - public int Id { get; set; } - public string? Name { get; set; } - public int LanguageId { get; set; } + return; } - #endregion + // note: the code below means we are going to unique-ify every culture names, regardless + // of whether the name has changed (ie the culture has been updated) - some saving culture + // fr-FR could cause culture en-UK name to change - not sure that is clean + + if (content.CultureInfos is null) + { + return; + } + + foreach (ContentCultureInfos cultureInfo in content.CultureInfos) + { + var langId = LanguageRepository.GetIdByIsoCode(cultureInfo.Culture); + if (!langId.HasValue) + { + continue; + } + + if (!names.TryGetValue(langId.Value, out IGrouping? cultureNames)) + { + continue; + } + + // get a unique name + IEnumerable otherNames = + cultureNames.Select(x => new SimilarNodeName {Id = x.Id, Name = x.Name}); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, cultureInfo.Name); + + if (uniqueName == content.GetCultureName(cultureInfo.Culture)) + { + continue; + } + + // update the name, and the publish name if published + content.SetCultureName(uniqueName, cultureInfo.Culture); + if (publishing && (content.PublishCultureInfos?.ContainsKey(cultureInfo.Culture) ?? false)) + { + content.SetPublishInfo(cultureInfo.Culture, uniqueName, + DateTime.Now); //TODO: This is weird, this call will have already been made in the SetCultureName + } + } } + + // ReSharper disable once ClassNeverInstantiated.Local + private class CultureNodeName + { + public int Id { get; set; } + public string? Name { get; set; } + public int LanguageId { get; set; } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs index f37886fee2..c97199e76b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentTypeContainerRepository.cs @@ -1,14 +1,15 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository { - internal class DocumentTypeContainerRepository : EntityContainerRepository, IDocumentTypeContainerRepository + public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.DocumentTypeContainer) { - public DocumentTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.DocumentTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs index aff71feb63..e922ed3cdb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -1,35 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class DocumentVersionRepository : IDocumentVersionRepository { - internal class DocumentVersionRepository : IDocumentVersionRepository + private readonly IScopeAccessor _scopeAccessor; + + public DocumentVersionRepository(IScopeAccessor scopeAccessor) => + _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + + /// + /// + /// Never includes current draft version.
+ /// Never includes current published version.
+ /// Never includes versions marked as "preventCleanup".
+ ///
+ public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup() { - private readonly IScopeAccessor _scopeAccessor; + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - public DocumentVersionRepository(IScopeAccessor scopeAccessor) => - _scopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - - /// - /// - /// Never includes current draft version.
- /// Never includes current published version.
- /// Never includes versions marked as "preventCleanup".
- ///
- public IReadOnlyCollection? GetDocumentVersionsEligibleForCleanup() - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - - query?.Select(@"umbracoDocument.nodeId as contentId, + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -38,39 +34,39 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .Where(x => !x.Current) // Never delete current draft version - .Where(x => !x.PreventCleanup) // Never delete "pinned" versions - .Where(x => !x.Published); // Never delete published version + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .Where(x => !x.Current) // Never delete current draft version + .Where(x => !x.PreventCleanup) // Never delete "pinned" versions + .Where(x => !x.Published); // Never delete published version - return _scopeAccessor.AmbientScope?.Database.Fetch(query); - } + return _scopeAccessor.AmbientScope?.Database.Fetch(query); + } - /// - public IReadOnlyCollection? GetCleanupPolicies() - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public IReadOnlyCollection? GetCleanupPolicies() + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - query?.Select() - .From(); + query?.Select() + .From(); - return _scopeAccessor.AmbientScope?.Database.Fetch(query); - } + return _scopeAccessor.AmbientScope?.Database.Fetch(query); + } - /// - public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null) - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public IEnumerable? GetPagedItemsByContentId(int contentId, long pageIndex, int pageSize, out long totalRecords, int? languageId = null) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); - query?.Select(@"umbracoDocument.nodeId as contentId, + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -79,86 +75,87 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .LeftJoin() - .On(left => left.VersionId, right => right.Id) - .Where(x => x.NodeId == contentId); + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .LeftJoin() + .On(left => left.VersionId, right => right.Id) + .Where(x => x.NodeId == contentId); - // TODO: If there's not a better way to write this then we need a better way to write this. - query = languageId.HasValue - ? query?.Where(x => x.LanguageId == languageId.Value) - : query?.Where("umbracoContentVersionCultureVariation.languageId is null"); + // TODO: If there's not a better way to write this then we need a better way to write this. + query = languageId.HasValue + ? query?.Where(x => x.LanguageId == languageId.Value) + : query?.Where("umbracoContentVersionCultureVariation.languageId is null"); - query = query?.OrderByDescending(x => x.Id); + query = query?.OrderByDescending(x => x.Id); - Page? page = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); + Page? page = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); - totalRecords = page?.TotalItems ?? 0; + totalRecords = page?.TotalItems ?? 0; - return page?.Items; - } + return page?.Items; + } - /// - /// - /// Deletes in batches of - /// - public void DeleteVersions(IEnumerable versionIds) + /// + /// + /// Deletes in batches of + /// + public void DeleteVersions(IEnumerable versionIds) + { + foreach (IEnumerable group in versionIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - foreach (IEnumerable group in versionIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - var groupedVersionIds = group.ToList(); + var groupedVersionIds = group.ToList(); - /* Note: We had discussed doing this in a single SQL Command. - * If you can work out how to make that work with SQL CE, let me know! - * Can use test PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive to try things out. - */ + /* Note: We had discussed doing this in a single SQL Command. + * If you can work out how to make that work with SQL CE, let me know! + * Can use test PerformContentVersionCleanup_WithNoKeepPeriods_DeletesEverythingExceptActive to try things out. + */ - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.VersionId, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.VersionId, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.Id, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - - query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Delete() - .WhereIn(x => x.Id, groupedVersionIds); - _scopeAccessor.AmbientScope?.Database.Execute(query); - } - } - - /// - public void SetPreventCleanup(int versionId, bool preventCleanup) - { Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() - .Update(x => x.Set(y => y.PreventCleanup, preventCleanup)) - .Where(x => x.Id == versionId); + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.VersionId, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); + _scopeAccessor.AmbientScope?.Database.Execute(query); + + query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Delete() + .WhereIn(x => x.Id, groupedVersionIds); _scopeAccessor.AmbientScope?.Database.Execute(query); } + } - /// - public ContentVersionMeta? Get(int versionId) - { - Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + /// + public void SetPreventCleanup(int versionId, bool preventCleanup) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql() + .Update(x => x.Set(y => y.PreventCleanup, preventCleanup)) + .Where(x => x.Id == versionId); - query?.Select(@"umbracoDocument.nodeId as contentId, + _scopeAccessor.AmbientScope?.Database.Execute(query); + } + + /// + public ContentVersionMeta? Get(int versionId) + { + Sql? query = _scopeAccessor.AmbientScope?.SqlContext.Sql(); + + query?.Select(@"umbracoDocument.nodeId as contentId, umbracoContent.contentTypeId as contentTypeId, umbracoContentVersion.id as versionId, umbracoContentVersion.userId as userId, @@ -167,18 +164,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement umbracoContentVersion.[current] as currentDraftVersion, umbracoContentVersion.preventCleanup as preventCleanup, umbracoUser.userName as username") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.Id, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserId) - .Where(x => x.Id == versionId); + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.Id, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserId) + .Where(x => x.Id == versionId); - return _scopeAccessor.AmbientScope?.Database.Single(query); - } + return _scopeAccessor.AmbientScope?.Database.Single(query); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs index 0cc0bc44ad..9304d27b84 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DomainRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -9,199 +6,218 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +// TODO: We need to get a readonly ISO code for the domain assigned +internal class DomainRepository : EntityRepositoryBase, IDomainRepository { - // TODO: We need to get a readonly ISO code for the domain assigned - - internal class DomainRepository : EntityRepositoryBase, IDomainRepository + public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public DomainRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() + public IDomain? GetByName(string domainName) => + GetMany().FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); + + public bool Exists(string domainName) => GetMany().Any(x => x.DomainName.InvariantEquals(domainName)); + + public IEnumerable GetAll(bool includeWildcards) => + GetMany().Where(x => includeWildcards || x.IsWildcard == false); + + public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) => + GetMany() + .Where(x => x.RootContentId == contentId) + .Where(x => includeWildcards || x.IsWildcard == false); + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ + false); + + protected override IDomain? PerformGet(int id) => + + // use the underlying GetAll which will force cache all domains + GetMany().FirstOrDefault(x => x.Id == id); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id > 0); + if (ids?.Any() ?? false) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + sql.WhereIn(x => x.Id, ids); } - protected override IDomain? PerformGet(int id) + return Database.Fetch(sql).Select(ConvertFromDto); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method"); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - //use the underlying GetAll which will force cache all domains - return GetMany()?.FirstOrDefault(x => x.Id == id); + sql.SelectCount().From(); + } + else + { + sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode") + .From() + .LeftJoin() + .On(dto => dto.DefaultLanguage, dto => dto.Id); } - protected override IEnumerable PerformGetAll(params int[]? ids) + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoDomain WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(IDomain entity) + { + var exists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", + new { domainName = entity.DomainName }); + if (exists > 0) { - var sql = GetBaseQuery(false).Where(x => x.Id > 0); - if (ids?.Any() ?? false) + throw new DuplicateNameException( + string.Format("The domain name {0} is already assigned", entity.DomainName)); + } + + if (entity.RootContentId.HasValue) + { + var contentExists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", + new { id = entity.RootContentId.Value }); + if (contentExists == 0) { - sql.WhereIn(x => x.Id, ids); + throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); } - - return Database.Fetch(sql).Select(ConvertFromDto); } - protected override IEnumerable PerformGetByQuery(IQuery query) + if (entity.LanguageId.HasValue) { - throw new NotSupportedException("This repository does not support this method"); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - if (isCount) + var languageExists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + new { id = entity.LanguageId.Value }); + if (languageExists == 0) { - sql.SelectCount().From(); + throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); } - else + } + + entity.AddingEntity(); + + var factory = new DomainModelFactory(); + DomainDto dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + // if the language changed, we need to resolve the ISO code! + if (entity.LanguageId.HasValue) + { + ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( + "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IDomain entity) + { + entity.UpdatingEntity(); + + var exists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", + new { domainName = entity.DomainName, id = entity.Id }); + + // ensure there is no other domain with the same name on another entity + if (exists > 0) + { + throw new DuplicateNameException( + string.Format("The domain name {0} is already assigned", entity.DomainName)); + } + + if (entity.RootContentId.HasValue) + { + var contentExists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", + new { id = entity.RootContentId.Value }); + if (contentExists == 0) { - sql.Select("umbracoDomain.*, umbracoLanguage.languageISOCode") - .From() - .LeftJoin() - .On(dto => dto.DefaultLanguage, dto => dto.Id); + throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); } - - return sql; } - protected override string GetBaseWhereClause() + if (entity.LanguageId.HasValue) { - return $"{Constants.DatabaseSchema.Tables.Domain}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoDomain WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IDomain entity) - { - var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName", new { domainName = entity.DomainName }); - if (exists > 0) throw new DuplicateNameException(string.Format("The domain name {0} is already assigned", entity.DomainName)); - - if (entity.RootContentId.HasValue) + var languageExists = Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", + new { id = entity.LanguageId.Value }); + if (languageExists == 0) { - var contentExists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", new { id = entity.RootContentId.Value }); - if (contentExists == 0) throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); + throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); } - - if (entity.LanguageId.HasValue) - { - var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", new { id = entity.LanguageId.Value }); - if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); - } - - entity.AddingEntity(); - - var factory = new DomainModelFactory(); - var dto = factory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - //if the language changed, we need to resolve the ISO code! - if (entity.LanguageId.HasValue) - { - ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar("SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); - } - - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(IDomain entity) + var factory = new DomainModelFactory(); + DomainDto dto = factory.BuildDto(entity); + + Database.Update(dto); + + // if the language changed, we need to resolve the ISO code! + if (entity.WasPropertyDirty("LanguageId")) { - entity.UpdatingEntity(); + ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar( + "SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new { langId = entity.LanguageId }); + } - var exists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoDomain WHERE domainName = @domainName AND umbracoDomain.id <> @id", - new { domainName = entity.DomainName, id = entity.Id }); - //ensure there is no other domain with the same name on another entity - if (exists > 0) throw new DuplicateNameException(string.Format("The domain name {0} is already assigned", entity.DomainName)); + entity.ResetDirtyProperties(); + } - if (entity.RootContentId.HasValue) + private IDomain ConvertFromDto(DomainDto dto) + { + var factory = new DomainModelFactory(); + IDomain entity = factory.BuildEntity(dto); + return entity; + } + + internal class DomainModelFactory + { + public IDomain BuildEntity(DomainDto dto) + { + var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) { - var contentExists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Cms.Core.Constants.DatabaseSchema.Tables.Content} WHERE nodeId = @id", new { id = entity.RootContentId.Value }); - if (contentExists == 0) throw new NullReferenceException("No content exists with id " + entity.RootContentId.Value); - } + Id = dto.Id, + LanguageId = dto.DefaultLanguage, + RootContentId = dto.RootStructureId, + }; - if (entity.LanguageId.HasValue) + // reset dirty initial properties (U4-1946) + domain.ResetDirtyProperties(false); + return domain; + } + + public DomainDto BuildDto(IDomain entity) + { + var dto = new DomainDto { - var languageExists = Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoLanguage WHERE id = @id", new { id = entity.LanguageId.Value }); - if (languageExists == 0) throw new NullReferenceException("No language exists with id " + entity.LanguageId.Value); - } - - var factory = new DomainModelFactory(); - var dto = factory.BuildDto(entity); - - Database.Update(dto); - - //if the language changed, we need to resolve the ISO code! - if (entity.WasPropertyDirty("LanguageId")) - { - ((UmbracoDomain)entity).LanguageIsoCode = Database.ExecuteScalar("SELECT languageISOCode FROM umbracoLanguage WHERE id=@langId", new {langId = entity.LanguageId}); - } - - entity.ResetDirtyProperties(); - } - - public IDomain? GetByName(string domainName) - { - return GetMany()?.FirstOrDefault(x => x.DomainName.InvariantEquals(domainName)); - } - - public bool Exists(string domainName) - { - return GetMany()?.Any(x => x.DomainName.InvariantEquals(domainName)) ?? false; - } - - public IEnumerable GetAll(bool includeWildcards) - { - return GetMany().Where(x => includeWildcards || x.IsWildcard == false); - } - - public IEnumerable GetAssignedDomains(int contentId, bool includeWildcards) - { - return GetMany() - .Where(x => x.RootContentId == contentId) - .Where(x => includeWildcards || x.IsWildcard == false); - } - - private IDomain ConvertFromDto(DomainDto dto) - { - var factory = new DomainModelFactory(); - var entity = factory.BuildEntity(dto); - return entity; - } - - internal class DomainModelFactory - { - - public IDomain BuildEntity(DomainDto dto) - { - var domain = new UmbracoDomain(dto.DomainName, dto.IsoCode) - { - Id = dto.Id, - LanguageId = dto.DefaultLanguage, - RootContentId = dto.RootStructureId - }; - // reset dirty initial properties (U4-1946) - domain.ResetDirtyProperties(false); - return domain; - } - - public DomainDto BuildDto(IDomain entity) - { - var dto = new DomainDto { DefaultLanguage = entity.LanguageId, DomainName = entity.DomainName, Id = entity.Id, RootStructureId = entity.RootContentId }; - return dto; - } + DefaultLanguage = entity.LanguageId, + DomainName = entity.DomainName, + Id = entity.Id, + RootStructureId = entity.RootContentId, + }; + return dto; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs index 468b83062c..7d3a9ab4fa 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityContainerRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -12,336 +9,333 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// An internal repository for managing entity containers such as doc type, media type, data type containers. +/// +internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository { - /// - /// An internal repository for managing entity containers such as doc type, media type, data type containers. - /// - internal class EntityContainerRepository : EntityRepositoryBase, IEntityContainerRepository + public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger, Guid containerObjectType) + : base(scopeAccessor, cache, logger) { - public EntityContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger, Guid containerObjectType) - : base(scopeAccessor, cache, logger) + Guid[] allowedContainers = { - Guid[] allowedContainers = new[] - { - Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, - Constants.ObjectTypes.DataTypeContainer - }; - NodeObjectTypeId = containerObjectType; - if (allowedContainers.Contains(NodeObjectTypeId) == false) - { - throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); - } + Constants.ObjectTypes.DocumentTypeContainer, Constants.ObjectTypes.MediaTypeContainer, + Constants.ObjectTypes.DataTypeContainer, + }; + NodeObjectTypeId = containerObjectType; + if (allowedContainers.Contains(NodeObjectTypeId) == false) + { + throw new InvalidOperationException("No container type exists with ID: " + NodeObjectTypeId); + } + } + + protected Guid NodeObjectTypeId { get; } + + // temp - so we don't have to implement GetByQuery + public EntityContainer? Get(Guid id) + { + Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + + NodeDto? nodeDto = Database.Fetch(sql).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } + + public IEnumerable Get(string name, int level) + { + Sql sql = GetBaseQuery(false) + .Where( + "text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", + new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); + return Database.Fetch(sql).Select(CreateEntity); + } + + // never cache + protected override IRepositoryCachePolicy CreateCachePolicy() => + NoCacheRepositoryCachePolicy.Instance; + + protected override EntityContainer? PerformGet(int id) + { + Sql sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { id, NodeObjectType = NodeObjectTypeId }); + + NodeDto? nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Any() ?? false) + { + return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => + GetBaseQuery(false) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .WhereIn(x => x.NodeId, batch)) + .Select(CreateEntity); } - protected Guid NodeObjectTypeId { get; } + // else + Sql sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) + .OrderBy(x => x.Level); - // never cache - protected override IRepositoryCachePolicy CreateCachePolicy() => - NoCacheRepositoryCachePolicy.Instance; + return Database.Fetch(sql).Select(CreateEntity); + } - protected override EntityContainer? PerformGet(int id) + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotImplementedException(); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - Sql sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { id, NodeObjectType = NodeObjectTypeId }); - - NodeDto? nodeDto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - return nodeDto == null ? null : CreateEntity(nodeDto); + sql.SelectCount(); + } + else + { + sql.SelectAll(); } - // temp - so we don't have to implement GetByQuery - public EntityContainer? Get(Guid id) - { - Sql sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + sql.From(); + return sql; + } - NodeDto? nodeDto = Database.Fetch(sql).FirstOrDefault(); - return nodeDto == null ? null : CreateEntity(nodeDto); + private static EntityContainer CreateEntity(NodeDto nodeDto) + { + if (nodeDto.NodeObjectType.HasValue == false) + { + throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); } - public IEnumerable Get(string name, int level) + // throws if node is not a container + Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); + + var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, + nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, + containedObjectType, + nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + + return entity; + } + + protected override string GetBaseWhereClause() => "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; + + protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); + + protected override void PersistDeletedItem(EntityContainer entity) + { + if (entity == null) { - Sql sql = GetBaseQuery(false) - .Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", - new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); - return Database.Fetch(sql).Select(CreateEntity); + throw new ArgumentNullException(nameof(entity)); } - protected override IEnumerable PerformGetAll(params int[]? ids) + EnsureContainerType(entity); + + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + + if (nodeDto == null) { - if (ids?.Any() ?? false) - { - return Database.FetchByGroups(ids, Constants.Sql.MaxParameterCount, batch => - GetBaseQuery(false) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.NodeId, batch)) - .Select(CreateEntity); - } - - // else - - Sql sql = GetBaseQuery(false) - .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) - .OrderBy(x => x.Level); - - return Database.Fetch(sql).Select(CreateEntity); + return; } - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new NotImplementedException(); + // move children to the parent so they are not orphans + List childDtos = Database.Fetch(Sql().SelectAll() + .From() + .Where( + "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", + new + { + parentID = entity.Id, + containedObjectType = entity.ContainedObjectType, + containerObjectType = entity.ContainerObjectType, + })); - private static EntityContainer CreateEntity(NodeDto nodeDto) + foreach (NodeDto childDto in childDtos) { - if (nodeDto.NodeObjectType.HasValue == false) - { - throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); - } - - // throws if node is not a container - Guid containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); - - var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, - nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, - containedObjectType, - nodeDto.Text, nodeDto.UserId ?? Constants.Security.UnknownUserId); - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - - return entity; + childDto.ParentId = nodeDto.ParentId; + Database.Update(childDto); } - protected override Sql GetBaseQuery(bool isCount) - { - Sql sql = Sql(); - if (isCount) - { - sql.SelectCount(); - } - else - { - sql.SelectAll(); - } + // delete + Database.Delete(nodeDto); - sql.From(); - return sql; + entity.DeleteDate = DateTime.Now; + } + + protected override void PersistNewItem(EntityContainer entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); } - protected override string GetBaseWhereClause() => "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; + EnsureContainerType(entity); - protected override IEnumerable GetDeleteClauses() => throw new NotImplementedException(); - - protected override void PersistDeletedItem(EntityContainer entity) + if (entity.Name == null) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } - - EnsureContainerType(entity); - - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - - if (nodeDto == null) - { - return; - } - - // move children to the parent so they are not orphans - List childDtos = Database.Fetch(Sql().SelectAll() - .From() - .Where( - "parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", - new - { - parentID = entity.Id, - containedObjectType = entity.ContainedObjectType, - containerObjectType = entity.ContainerObjectType - })); - - foreach (NodeDto childDto in childDtos) - { - childDto.ParentId = nodeDto.ParentId; - Database.Update(childDto); - } - - // delete - Database.Delete(nodeDto); - - entity.DeleteDate = DateTime.Now; + throw new InvalidOperationException("Entity name can't be null."); } - protected override void PersistNewItem(EntityContainer entity) + if (string.IsNullOrWhiteSpace(entity.Name)) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } - EnsureContainerType(entity); + entity.Name = entity.Name.Trim(); - if (entity.Name == null) - { - throw new InvalidOperationException("Entity name can't be null."); - } + // guard against duplicates + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto != null) + { + throw new InvalidOperationException("A container with the same name already exists."); + } - if (string.IsNullOrWhiteSpace(entity.Name)) - { - throw new InvalidOperationException( - "Entity name can't be empty or consist only of white-space characters."); - } - - entity.Name = entity.Name.Trim(); - - // guard against duplicates - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + // create + var level = 0; + var path = "-1"; + if (entity.ParentId > -1) + { + NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => - dto.ParentId == entity.ParentId && dto.Text == entity.Name && - dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto != null) + dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + + if (parentDto == null) { - throw new InvalidOperationException("A container with the same name already exists."); + throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); } - // create - var level = 0; - var path = "-1"; + level = parentDto.Level; + path = parentDto.Path; + } + + // note: sortOrder is NOT managed and always zero for containers + nodeDto = new NodeDto + { + CreateDate = DateTime.Now, + Level = Convert.ToInt16(level + 1), + NodeObjectType = entity.ContainerObjectType, + ParentId = entity.ParentId, + Path = path, + SortOrder = 0, + Text = entity.Name, + UserId = entity.CreatorId, + UniqueId = entity.Key, + }; + + // insert, get the id, update the path with the id + var id = Convert.ToInt32(Database.Insert(nodeDto)); + nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; + Database.Save(nodeDto); + + // refresh the entity + entity.Id = id; + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.CreateDate = nodeDto.CreateDate; + entity.ResetDirtyProperties(); + } + + // beware! does NOT manage descendants in case of a new parent + protected override void PersistUpdatedItem(EntityContainer entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + EnsureContainerType(entity); + + if (entity.Name == null) + { + throw new InvalidOperationException("Entity name can't be null."); + } + + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new InvalidOperationException( + "Entity name can't be empty or consist only of white-space characters."); + } + + entity.Name = entity.Name.Trim(); + + // find container to update + NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto == null) + { + throw new InvalidOperationException("Could not find container with id " + entity.Id); + } + + // guard against duplicates + NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() + .From() + .Where(dto => + dto.ParentId == entity.ParentId && dto.Text == entity.Name && + dto.NodeObjectType == entity.ContainerObjectType)); + if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) + { + throw new InvalidOperationException("A container with the same name already exists."); + } + + // update + nodeDto.Text = entity.Name; + if (nodeDto.ParentId != entity.ParentId) + { + nodeDto.Level = 0; + nodeDto.Path = "-1"; if (entity.ParentId > -1) { - NodeDto parentDto = Database.FirstOrDefault(Sql().SelectAll() + NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() .From() .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); - if (parentDto == null) + if (parent == null) { - throw new InvalidOperationException("Could not find parent container with id " + entity.ParentId); + throw new InvalidOperationException( + "Could not find parent container with id " + entity.ParentId); } - level = parentDto.Level; - path = parentDto.Path; + nodeDto.Level = Convert.ToInt16(parent.Level + 1); + nodeDto.Path = parent.Path + "," + nodeDto.NodeId; } - // note: sortOrder is NOT managed and always zero for containers - - nodeDto = new NodeDto - { - CreateDate = DateTime.Now, - Level = Convert.ToInt16(level + 1), - NodeObjectType = entity.ContainerObjectType, - ParentId = entity.ParentId, - Path = path, - SortOrder = 0, - Text = entity.Name, - UserId = entity.CreatorId, - UniqueId = entity.Key - }; - - // insert, get the id, update the path with the id - var id = Convert.ToInt32(Database.Insert(nodeDto)); - nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; - Database.Save(nodeDto); - - // refresh the entity - entity.Id = id; - entity.Path = nodeDto.Path; - entity.Level = nodeDto.Level; - entity.SortOrder = 0; - entity.CreateDate = nodeDto.CreateDate; - entity.ResetDirtyProperties(); + nodeDto.ParentId = entity.ParentId; } - // beware! does NOT manage descendants in case of a new parent - // - protected override void PersistUpdatedItem(EntityContainer entity) + // note: sortOrder is NOT managed and always zero for containers + + // update + Database.Update(nodeDto); + + // refresh the entity + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.ResetDirtyProperties(); + } + + private void EnsureContainerType(EntityContainer entity) + { + if (entity.ContainerObjectType != NodeObjectTypeId) { - if (entity == null) - { - throw new ArgumentNullException(nameof(entity)); - } - - EnsureContainerType(entity); - - if (entity.Name == null) - { - throw new InvalidOperationException("Entity name can't be null."); - } - - if (string.IsNullOrWhiteSpace(entity.Name)) - { - throw new InvalidOperationException( - "Entity name can't be empty or consist only of white-space characters."); - } - - entity.Name = entity.Name.Trim(); - - // find container to update - NodeDto nodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); - if (nodeDto == null) - { - throw new InvalidOperationException("Could not find container with id " + entity.Id); - } - - // guard against duplicates - NodeDto dupNodeDto = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => - dto.ParentId == entity.ParentId && dto.Text == entity.Name && - dto.NodeObjectType == entity.ContainerObjectType)); - if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) - { - throw new InvalidOperationException("A container with the same name already exists."); - } - - // update - nodeDto.Text = entity.Name; - if (nodeDto.ParentId != entity.ParentId) - { - nodeDto.Level = 0; - nodeDto.Path = "-1"; - if (entity.ParentId > -1) - { - NodeDto parent = Database.FirstOrDefault(Sql().SelectAll() - .From() - .Where(dto => - dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); - - if (parent == null) - { - throw new InvalidOperationException( - "Could not find parent container with id " + entity.ParentId); - } - - nodeDto.Level = Convert.ToInt16(parent.Level + 1); - nodeDto.Path = parent.Path + "," + nodeDto.NodeId; - } - - nodeDto.ParentId = entity.ParentId; - } - - // note: sortOrder is NOT managed and always zero for containers - - // update - Database.Update(nodeDto); - - // refresh the entity - entity.Path = nodeDto.Path; - entity.Level = nodeDto.Level; - entity.SortOrder = 0; - entity.ResetDirtyProperties(); - } - - private void EnsureContainerType(EntityContainer entity) - { - if (entity.ContainerObjectType != NodeObjectTypeId) - { - throw new InvalidOperationException("The container type does not match the repository object type"); - } + throw new InvalidOperationException("The container type does not match the repository object type"); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index ef0f02540e..c904b5b440 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence.Querying; -using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Querying; @@ -16,720 +12,769 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the EntityRepository used to query entity objects. +/// +/// +/// Limited to objects that have a corresponding node (in umbracoNode table). +/// Returns objects, i.e. lightweight representation of entities. +/// +internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended { - /// - /// Represents the EntityRepository used to query entity objects. - /// - /// - /// Limited to objects that have a corresponding node (in umbracoNode table). - /// Returns objects, i.e. lightweight representation of entities. - /// - internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended + public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) + : base(scopeAccessor, appCaches) { - public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) - : base(scopeAccessor, appCaches) + } + + #region Repository + + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) => + GetPagedResultsByQuery(query, new[] {objectType}, pageIndex, pageSize, out totalRecords, filter, ordering); + + // get a page of entities + public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null) + { + var isContent = objectTypes.Any(objectType => + objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint); + var isMedia = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Media); + var isMember = objectTypes.Any(objectType => objectType == Constants.ObjectTypes.Member); + + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, s => { - } + sqlCustomization?.Invoke(s); - #region Repository - - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - return GetPagedResultsByQuery(query, new[] { objectType }, pageIndex, pageSize, out totalRecords, filter, ordering); - } - - // get a page of entities - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering, Action>? sqlCustomization = null) - { - var isContent = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint); - var isMedia = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Media); - var isMember = objectTypes.Any(objectType => objectType == Cms.Core.Constants.ObjectTypes.Member); - - Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, s => + if (filter != null) { - sqlCustomization?.Invoke(s); - - if (filter != null) + foreach (Tuple filterClause in filter.GetWhereClauses()) { - foreach (Tuple filterClause in filter.GetWhereClauses()) - { - s.Where(filterClause.Item1, filterClause.Item2); - } + s.Where(filterClause.Item1, filterClause.Item2); } - }, objectTypes); - - ordering = ordering ?? Ordering.ByDefault(); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - - if (!ordering.IsEmpty) - { - // apply ordering - ApplyOrdering(ref sql, ordering); } + }, objectTypes); - // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently - // no matter what we always must have node id ordered at the end - sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); + ordering = ordering ?? Ordering.ByDefault(); - // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names - var pageIndexToFetch = pageIndex + 1; - IEnumerable dtos; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - dtos = page.Items; - totalRecords = page.TotalItems; + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); - var entities = dtos.Select(BuildEntity).ToArray(); - - BuildVariants(entities.OfType()); - - return entities; + if (!ordering.IsEmpty) + { + // apply ordering + ApplyOrdering(ref sql, ordering); } - public IEntitySlim? Get(Guid key) + // TODO: we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently + // no matter what we always must have node id ordered at the end + sql = ordering.Direction == Direction.Ascending ? sql.OrderBy("NodeId") : sql.OrderByDescending("NodeId"); + + // for content we must query for ContentEntityDto entities to produce the correct culture variant entity names + var pageIndexToFetch = pageIndex + 1; + IEnumerable dtos; + Page? page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + + EntitySlim[] entities = dtos.Select(BuildEntity).ToArray(); + + BuildVariants(entities.OfType()); + + return entities; + } + + public IEntitySlim? Get(Guid key) + { + Sql sql = GetBaseWhere(false, false, false, false, key); + BaseDto? dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(dto); + } + + + private IEntitySlim? GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) + { + // isContent is going to return a 1:M result now with the variants so we need to do different things + if (isContent) { - var sql = GetBaseWhere(false, false, false, false, key); - var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); + List? cdtos = Database.Fetch(sql); + + return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } + BaseDto? dto = isMedia + ? Database.FirstOrDefault(sql) + : Database.FirstOrDefault(sql); - private IEntitySlim? GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) + if (dto == null) { - // isContent is going to return a 1:M result now with the variants so we need to do different things - if (isContent) + return null; + } + + EntitySlim entity = BuildEntity(dto); + + return entity; + } + + public IEntitySlim? Get(Guid key, Guid objectTypeId) + { + var isContent = objectTypeId == Constants.ObjectTypes.Document || + objectTypeId == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; + + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key); + return GetEntity(sql, isContent, isMedia, isMember); + } + + public IEntitySlim? Get(int id) + { + Sql sql = GetBaseWhere(false, false, false, false, id); + BaseDto? dto = Database.FirstOrDefault(sql); + return dto == null ? null : BuildEntity(dto); + } + + public IEntitySlim? Get(int id, Guid objectTypeId) + { + var isContent = objectTypeId == Constants.ObjectTypes.Document || + objectTypeId == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; + + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id); + return GetEntity(sql, isContent, isMedia, isMember); + } + + public IEnumerable GetAll(Guid objectType, params int[] ids) => + ids.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAll(objectType); + + public IEnumerable GetAll(Guid objectType, params Guid[] keys) => + keys.Length > 0 + ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAll(objectType); + + private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) + { + // isContent is going to return a 1:M result now with the variants so we need to do different things + if (isContent) + { + List? cdtos = Database.Fetch(sql); + + return cdtos.Count == 0 + ? Enumerable.Empty() + : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); + } + + IEnumerable? dtos = isMedia + ? (IEnumerable)Database.Fetch(sql) + : Database.Fetch(sql); + + EntitySlim[] entities = dtos.Select(BuildEntity).ToArray(); + + return entities; + } + + private IEnumerable PerformGetAll(Guid objectType, Action>? filter = null) + { + var isContent = objectType == Constants.ObjectTypes.Document || + objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; + + Sql sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); + return GetEntities(sql, isContent, isMedia, isMember); + } + + public IEnumerable GetAllPaths(Guid objectType, params int[]? ids) => + ids?.Any() ?? false + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) + : PerformGetAllPaths(objectType); + + public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) => + keys.Any() + ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) + : PerformGetAllPaths(objectType); + + private IEnumerable PerformGetAllPaths(Guid objectType, Action>? filter = null) + { + // NodeId is named Id on TreeEntityPath = use an alias + Sql sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path) + .From().Where(x => x.NodeObjectType == objectType); + filter?.Invoke(sql); + return Database.Fetch(sql); + } + + public IEnumerable GetByQuery(IQuery query) + { + Sql sqlClause = GetBase(false, false, false, null); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql = AddGroupBy(false, false, false, sql, true); + List? dtos = Database.Fetch(sql); + return dtos.Select(BuildEntity).ToList(); + } + + public IEnumerable GetByQuery(IQuery query, Guid objectType) + { + var isContent = objectType == Constants.ObjectTypes.Document || + objectType == Constants.ObjectTypes.DocumentBlueprint; + var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; + + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] {objectType}); + + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + sql = AddGroupBy(isContent, isMedia, isMember, sql, true); + + return GetEntities(sql, isContent, isMedia, isMember); + } + + public UmbracoObjectTypes GetObjectType(int id) + { + Sql sql = Sql().Select(x => x.NodeObjectType).From() + .Where(x => x.NodeId == id); + return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); + } + + public UmbracoObjectTypes GetObjectType(Guid key) + { + Sql sql = Sql().Select(x => x.NodeObjectType).From() + .Where(x => x.UniqueId == key); + return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); + } + + public int ReserveId(Guid key) + { + NodeDto node; + + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.UniqueId == key && x.NodeObjectType == Constants.ObjectTypes.IdReservation); + + node = Database.SingleOrDefault(sql); + if (node != null) + { + throw new InvalidOperationException("An identifier has already been reserved for this Udi."); + } + + node = new NodeDto + { + UniqueId = key, + Text = "RESERVED.ID", + NodeObjectType = Constants.ObjectTypes.IdReservation, + CreateDate = DateTime.Now, + UserId = null, + ParentId = -1, + Level = 1, + Path = "-1", + SortOrder = 0, + Trashed = false + }; + Database.Insert(node); + + return node.NodeId; + } + + public bool Exists(Guid key) + { + Sql sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); + return Database.ExecuteScalar(sql) > 0; + } + + public bool Exists(int id) + { + Sql sql = Sql().SelectCount().From().Where(x => x.NodeId == id); + return Database.ExecuteScalar(sql) > 0; + } + + private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) + => BuildVariants(new[] {entity}).First(); + + private IEnumerable BuildVariants(IEnumerable entities) + { + List? v = null; + var entitiesList = entities.ToList(); + foreach (DocumentEntitySlim e in entitiesList) + { + if (e.Variations.VariesByCulture()) { - var cdtos = Database.Fetch(sql); - - return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); + (v ?? (v = new List())).Add(e); } - - var dto = isMedia - ? Database.FirstOrDefault(sql) - : Database.FirstOrDefault(sql); - - if (dto == null) return null; - - var entity = BuildEntity(dto); - - return entity; } - public IEntitySlim? Get(Guid key, Guid objectTypeId) + if (v == null) { - var isContent = objectTypeId == Cms.Core.Constants.ObjectTypes.Document || objectTypeId == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectTypeId == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key); - return GetEntity(sql, isContent, isMedia, isMember); - } - - public IEntitySlim? Get(int id) - { - var sql = GetBaseWhere(false, false, false, false, id); - var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(dto); - } - - public IEntitySlim? Get(int id, Guid objectTypeId) - { - var isContent = objectTypeId == Cms.Core.Constants.ObjectTypes.Document || objectTypeId == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectTypeId == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectTypeId == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id); - return GetEntity(sql, isContent, isMedia, isMember); - } - - public IEnumerable GetAll(Guid objectType, params int[] ids) - { - return ids.Length > 0 - ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) - : PerformGetAll(objectType); - } - - public IEnumerable GetAll(Guid objectType, params Guid[] keys) - { - return keys.Length > 0 - ? PerformGetAll(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) - : PerformGetAll(objectType); - } - - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) - { - // isContent is going to return a 1:M result now with the variants so we need to do different things - if (isContent) - { - var cdtos = Database.Fetch(sql); - - return cdtos.Count == 0 - ? Enumerable.Empty() - : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); - } - - var dtos = isMedia - ? (IEnumerable)Database.Fetch(sql) - : Database.Fetch(sql); - - var entities = dtos.Select(BuildEntity).ToArray(); - - return entities; - } - - private IEnumerable PerformGetAll(Guid objectType, Action>? filter = null) - { - var isContent = objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectType == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); - return GetEntities(sql, isContent, isMedia, isMember); - } - - public IEnumerable GetAllPaths(Guid objectType, params int[]? ids) - { - return ids?.Any() ?? false - ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.NodeId, ids.Distinct())) - : PerformGetAllPaths(objectType); - } - - public IEnumerable GetAllPaths(Guid objectType, params Guid[] keys) - { - return keys.Any() - ? PerformGetAllPaths(objectType, sql => sql.WhereIn(x => x.UniqueId, keys.Distinct())) - : PerformGetAllPaths(objectType); - } - - private IEnumerable PerformGetAllPaths(Guid objectType, Action>? filter = null) - { - // NodeId is named Id on TreeEntityPath = use an alias - var sql = Sql().Select(x => Alias(x.NodeId, nameof(TreeEntityPath.Id)), x => x.Path).From().Where(x => x.NodeObjectType == objectType); - filter?.Invoke(sql); - return Database.Fetch(sql); - } - - public IEnumerable GetByQuery(IQuery query) - { - var sqlClause = GetBase(false, false, false, null); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql = AddGroupBy(false, false, false, sql, true); - var dtos = Database.Fetch(sql); - return dtos.Select(BuildEntity).ToList(); - } - - public IEnumerable GetByQuery(IQuery query, Guid objectType) - { - var isContent = objectType == Cms.Core.Constants.ObjectTypes.Document || objectType == Cms.Core.Constants.ObjectTypes.DocumentBlueprint; - var isMedia = objectType == Cms.Core.Constants.ObjectTypes.Media; - var isMember = objectType == Cms.Core.Constants.ObjectTypes.Member; - - var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, new[] { objectType }); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, isMember, sql, true); - - return GetEntities(sql, isContent, isMedia, isMember); - } - - public UmbracoObjectTypes GetObjectType(int id) - { - var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.NodeId == id); - return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); - } - - public UmbracoObjectTypes GetObjectType(Guid key) - { - var sql = Sql().Select(x => x.NodeObjectType).From().Where(x => x.UniqueId == key); - return ObjectTypes.GetUmbracoObjectType(Database.ExecuteScalar(sql)); - } - - public int ReserveId(Guid key) - { - NodeDto node; - - Sql sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.UniqueId == key && x.NodeObjectType == Cms.Core.Constants.ObjectTypes.IdReservation); - - node = Database.SingleOrDefault(sql); - if (node != null) - throw new InvalidOperationException("An identifier has already been reserved for this Udi."); - - node = new NodeDto - { - UniqueId = key, - Text = "RESERVED.ID", - NodeObjectType = Cms.Core.Constants.ObjectTypes.IdReservation, - - CreateDate = DateTime.Now, - UserId = null, - ParentId = -1, - Level = 1, - Path = "-1", - SortOrder = 0, - Trashed = false - }; - Database.Insert(node); - - return node.NodeId; - } - - public bool Exists(Guid key) - { - var sql = Sql().SelectCount().From().Where(x => x.UniqueId == key); - return Database.ExecuteScalar(sql) > 0; - } - - public bool Exists(int id) - { - var sql = Sql().SelectCount().From().Where(x => x.NodeId == id); - return Database.ExecuteScalar(sql) > 0; - } - - private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) - => BuildVariants(new[] { entity }).First(); - - private IEnumerable BuildVariants(IEnumerable entities) - { - List? v = null; - var entitiesList = entities.ToList(); - foreach (var e in entitiesList) - { - if (e.Variations.VariesByCulture()) - (v ?? (v = new List())).Add(e); - } - - if (v == null) return entitiesList; - - // fetch all variant info dtos - var dtos = Database.FetchByGroups(v.Select(x => x.Id), Constants.Sql.MaxParameterCount, GetVariantInfos); - - // group by node id (each group contains all languages) - var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); - - foreach (var e in v) - { - // since we're only iterating on entities that vary, we must have something - var edtos = xdtos[e.Id]; - - e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); - e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); - e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); - } - return entitiesList; } - #endregion + // fetch all variant info dtos + IEnumerable dtos = Database.FetchByGroups(v.Select(x => x.Id), + Constants.Sql.MaxParameterCount, GetVariantInfos); - #region Sql + // group by node id (each group contains all languages) + var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); - protected Sql GetVariantInfos(IEnumerable ids) + foreach (DocumentEntitySlim e in v) { - return Sql() - .Select(x => x.NodeId) - .AndSelect(x => x.IsoCode) - .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited")) - .AndSelect("dcv", - x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), x => Alias(x.Edited, "CultureEdited"), - x => Alias(x.Name, "Name")) + // since we're only iterating on entities that vary, we must have something + IGrouping edtos = xdtos[e.Id]; - // from node x language - .From() - .CrossJoin() - - // join to document - always exists - indicates global document published/edited status - .InnerJoin("doc") - .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") - - // left-join do document variation - matches cultures that are *available* + indicates when *edited* - .LeftJoin("dcv") - .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") - - // for selected nodes - .WhereIn(x => x.NodeId, ids) - .OrderBy(x => x.Id); + e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); + e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); + e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); } - // gets the full sql for a given object type and a given unique id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Guid uniqueId) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the full sql for a given object type and a given node id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, int nodeId) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the full sql for a given object type, with a given filter - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action>? filter) - { - var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] { objectType }); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the base SELECT + FROM [+ filter] sql - // always from the 'current' content version - protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action>? filter, bool isCount = false) - { - var sql = Sql(); - - if (isCount) - { - sql.SelectCount(); - } - else - { - sql - .Select(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) - .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate) - .Append(", COUNT(child.id) AS children"); - - if (isContent || isMedia || isMember) - sql - .AndSelect(x => Alias(x.Id, "versionId"), x=>x.VersionDate) - .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); - - if (isContent) - { - sql - .AndSelect(x => x.Published, x => x.Edited); - } - - if (isMedia) - { - sql - .AndSelect(x => Alias(x.Path, "MediaPath")); - } - } - - sql - .From(); - - if (isContent || isMedia || isMember) - { - sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin().On((left, right) => left.ContentTypeId == right.NodeId); - } - - if (isContent) - { - sql - .LeftJoin().On((left, right) => left.NodeId == right.NodeId); - } - - if (isMedia) - { - sql - .LeftJoin().On((left, right) => left.Id == right.Id); - } - - //Any LeftJoin statements need to come last - if (isCount == false) - { - sql - .LeftJoin("child").On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); - } - - - filter?.Invoke(sql); - - return sql; - } - - // gets the base SELECT + FROM [+ filter] + WHERE sql - // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action>? filter, Guid[] objectTypes) - { - var sql = GetBase(isContent, isMedia, isMember, filter, isCount); - if (objectTypes.Length > 0) - { - sql.WhereIn(x => x.NodeObjectType, objectTypes); - } - return sql; - } - - // gets the base SELECT + FROM + WHERE sql - // for a given node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) - { - var sql = GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.NodeId == id); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the base SELECT + FROM + WHERE sql - // for a given unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) - { - var sql = GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.UniqueId == uniqueId); - return AddGroupBy(isContent, isMedia, isMember, sql, true); - } - - // gets the base SELECT + FROM + WHERE sql - // for a given object type and node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, int nodeId) - { - return GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); - } - - // gets the base SELECT + FROM + WHERE sql - // for a given object type and unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, Guid uniqueId) - { - return GetBase(isContent, isMedia, isMember, null, isCount) - .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); - } - - // gets the GROUP BY / ORDER BY sql - // required in order to count children - protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, bool defaultSort) - { - sql - .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) - .AndBy(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate); - - if (isContent) - { - sql - .AndBy(x => x.Published, x => x.Edited); - } - - if (isMedia) - { - sql - .AndBy(x => Alias(x.Path, "MediaPath")); - } - - - if (isContent || isMedia || isMember) - sql - .AndBy(x => x.Id, x => x.VersionDate) - .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); - - if (defaultSort) - sql.OrderBy(x => x.SortOrder); - - return sql; - } - - private void ApplyOrdering(ref Sql sql, Ordering ordering) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - - // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort - // As more things are attempted to be sorted we'll prob have to add more expressions here - string orderBy; - switch (ordering.OrderBy?.ToUpperInvariant()) - { - case "PATH": - orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); - break; - - default: - orderBy = ordering.OrderBy ?? string.Empty; - break; - } - - if (ordering.Direction == Direction.Ascending) - sql.OrderBy(orderBy); - else - sql.OrderByDescending(orderBy); - } - - #endregion - - #region Classes - - /// - /// The DTO used to fetch results for a generic content item which could be either a document, media or a member - /// - private class GenericContentEntityDto : DocumentEntityDto - { - public string? MediaPath { get; set; } - } - - /// - /// The DTO used to fetch results for a document item with its variation info - /// - private class DocumentEntityDto : BaseDto - { - public ContentVariation Variations { get; set; } - - public bool Published { get; set; } - public bool Edited { get; set; } - } - - /// - /// The DTO used to fetch results for a media item with its media path info - /// - private class MediaEntityDto : BaseDto - { - public string? MediaPath { get; set; } - } - - /// - /// The DTO used to fetch results for a member item - /// - private class MemberEntityDto : BaseDto - { - } - - public class VariantInfoDto - { - public int NodeId { get; set; } - public string IsoCode { get; set; } = null!; - public string Name { get; set; } = null!; - public bool DocumentPublished { get; set; } - public bool DocumentEdited { get; set; } - - public bool CultureAvailable { get; set; } - public bool CulturePublished { get; set; } - public bool CultureEdited { get; set; } - } - - // ReSharper disable once ClassNeverInstantiated.Local - /// - /// the DTO corresponding to fields selected by GetBase - /// - private class BaseDto - { - // ReSharper disable UnusedAutoPropertyAccessor.Local - // ReSharper disable UnusedMember.Local - public int NodeId { get; set; } - public bool Trashed { get; set; } - public int ParentId { get; set; } - public int? UserId { get; set; } - public int Level { get; set; } - public string Path { get; set; } = null!; - public int SortOrder { get; set; } - public Guid UniqueId { get; set; } - public string? Text { get; set; } - public Guid NodeObjectType { get; set; } - public DateTime CreateDate { get; set; } - public DateTime VersionDate { get; set; } - public int Children { get; set; } - public int VersionId { get; set; } - public string Alias { get; set; } = null!; - public string? Icon { get; set; } - public string? Thumbnail { get; set; } - public bool IsContainer { get; set; } - - // ReSharper restore UnusedAutoPropertyAccessor.Local - // ReSharper restore UnusedMember.Local - } - #endregion - - #region Factory - - private EntitySlim BuildEntity(BaseDto dto) - { - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Document) - return BuildDocumentEntity(dto); - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Media) - return BuildMediaEntity(dto); - if (dto.NodeObjectType == Cms.Core.Constants.ObjectTypes.Member) - return BuildMemberEntity(dto); - - // EntitySlim does not track changes - var entity = new EntitySlim(); - BuildEntity(entity, dto); - return entity; - } - - private static void BuildEntity(EntitySlim entity, BaseDto dto) - { - entity.Trashed = dto.Trashed; - entity.CreateDate = dto.CreateDate; - entity.UpdateDate = dto.VersionDate; - entity.CreatorId = dto.UserId ?? Cms.Core.Constants.Security.UnknownUserId; - entity.Id = dto.NodeId; - entity.Key = dto.UniqueId; - entity.Level = dto.Level; - entity.Name = dto.Text; - entity.NodeObjectType = dto.NodeObjectType; - entity.ParentId = dto.ParentId; - entity.Path = dto.Path; - entity.SortOrder = dto.SortOrder; - entity.HasChildren = dto.Children > 0; - entity.IsContainer = dto.IsContainer; - } - - private static void BuildContentEntity(ContentEntitySlim entity, BaseDto dto) - { - BuildEntity(entity, dto); - entity.ContentTypeAlias = dto.Alias; - entity.ContentTypeIcon = dto.Icon; - entity.ContentTypeThumbnail = dto.Thumbnail; - } - - private MediaEntitySlim BuildMediaEntity(BaseDto dto) - { - // EntitySlim does not track changes - var entity = new MediaEntitySlim(); - BuildContentEntity(entity, dto); - - // fill in the media info - if (dto is MediaEntityDto mediaEntityDto) - { - entity.MediaPath = mediaEntityDto.MediaPath; - } - else if (dto is GenericContentEntityDto genericContentEntityDto) - { - entity.MediaPath = genericContentEntityDto.MediaPath; - } - - return entity; - } - - private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) - { - // EntitySlim does not track changes - var entity = new DocumentEntitySlim(); - BuildContentEntity(entity, dto); - - if (dto is DocumentEntityDto contentDto) - { - // fill in the invariant info - entity.Edited = contentDto.Edited; - entity.Published = contentDto.Published; - entity.Variations = contentDto.Variations; - } - - return entity; - } - - private MemberEntitySlim BuildMemberEntity(BaseDto dto) - { - // EntitySlim does not track changes - var entity = new MemberEntitySlim(); - BuildEntity(entity, dto); - - entity.ContentTypeAlias = dto.Alias; - entity.ContentTypeIcon = dto.Icon; - entity.ContentTypeThumbnail = dto.Thumbnail; - - return entity; - } - - #endregion + return entitiesList; } + + #endregion + + #region Sql + + protected Sql GetVariantInfos(IEnumerable ids) => + Sql() + .Select(x => x.NodeId) + .AndSelect(x => x.IsoCode) + .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), + x => Alias(x.Edited, "DocumentEdited")) + .AndSelect("dcv", + x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), + x => Alias(x.Edited, "CultureEdited"), + x => Alias(x.Name, "Name")) + + // from node x language + .From() + .CrossJoin() + + // join to document - always exists - indicates global document published/edited status + .InnerJoin("doc") + .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") + + // left-join do document variation - matches cultures that are *available* + indicates when *edited* + .LeftJoin("dcv") + .On( + (node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") + + // for selected nodes + .WhereIn(x => x.NodeId, ids) + .OrderBy(x => x.Id); + + // gets the full sql for a given object type and a given unique id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + Guid uniqueId) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the full sql for a given object type and a given node id + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + int nodeId) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the full sql for a given object type, with a given filter + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, + Action>? filter) + { + Sql sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, new[] {objectType}); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the base SELECT + FROM [+ filter] sql + // always from the 'current' content version + protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action>? filter, + bool isCount = false) + { + Sql sql = Sql(); + + if (isCount) + { + sql.SelectCount(); + } + else + { + sql + .Select(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, + x => x.Path) + .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, + x => x.CreateDate) + .Append(", COUNT(child.id) AS children"); + + if (isContent || isMedia || isMember) + { + sql + .AndSelect(x => Alias(x.Id, "versionId"), x => x.VersionDate) + .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, + x => x.Variations); + } + + if (isContent) + { + sql + .AndSelect(x => x.Published, x => x.Edited); + } + + if (isMedia) + { + sql + .AndSelect(x => Alias(x.Path, "MediaPath")); + } + } + + sql + .From(); + + if (isContent || isMedia || isMember) + { + sql + .LeftJoin() + .On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin() + .On((left, right) => left.ContentTypeId == right.NodeId); + } + + if (isContent) + { + sql + .LeftJoin().On((left, right) => left.NodeId == right.NodeId); + } + + if (isMedia) + { + sql + .LeftJoin() + .On((left, right) => left.Id == right.Id); + } + + //Any LeftJoin statements need to come last + if (isCount == false) + { + sql + .LeftJoin("child") + .On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); + } + + + filter?.Invoke(sql); + + return sql; + } + + // gets the base SELECT + FROM [+ filter] + WHERE sql + // for a given object type, with a given filter + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, + Action>? filter, Guid[] objectTypes) + { + Sql sql = GetBase(isContent, isMedia, isMember, filter, isCount); + if (objectTypes.Length > 0) + { + sql.WhereIn(x => x.NodeObjectType, objectTypes); + } + + return sql; + } + + // gets the base SELECT + FROM + WHERE sql + // for a given node id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) + { + Sql sql = GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.NodeId == id); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the base SELECT + FROM + WHERE sql + // for a given unique id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) + { + Sql sql = GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.UniqueId == uniqueId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); + } + + // gets the base SELECT + FROM + WHERE sql + // for a given object type and node id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, + int nodeId) => + GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); + + // gets the base SELECT + FROM + WHERE sql + // for a given object type and unique id + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, + Guid uniqueId) => + GetBase(isContent, isMedia, isMember, null, isCount) + .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); + + // gets the GROUP BY / ORDER BY sql + // required in order to count children + protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, + bool defaultSort) + { + sql + .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) + .AndBy(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate); + + if (isContent) + { + sql + .AndBy(x => x.Published, x => x.Edited); + } + + if (isMedia) + { + sql + .AndBy(x => Alias(x.Path, "MediaPath")); + } + + + if (isContent || isMedia || isMember) + { + sql + .AndBy(x => x.Id, x => x.VersionDate) + .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, + x => x.Variations); + } + + if (defaultSort) + { + sql.OrderBy(x => x.SortOrder); + } + + return sql; + } + + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } + + // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort + // As more things are attempted to be sorted we'll prob have to add more expressions here + string orderBy; + switch (ordering.OrderBy?.ToUpperInvariant()) + { + case "PATH": + orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); + break; + + default: + orderBy = ordering.OrderBy ?? string.Empty; + break; + } + + if (ordering.Direction == Direction.Ascending) + { + sql.OrderBy(orderBy); + } + else + { + sql.OrderByDescending(orderBy); + } + } + + #endregion + + #region Classes + + /// + /// The DTO used to fetch results for a generic content item which could be either a document, media or a member + /// + private class GenericContentEntityDto : DocumentEntityDto + { + public string? MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a document item with its variation info + /// + private class DocumentEntityDto : BaseDto + { + public ContentVariation Variations { get; set; } + + public bool Published { get; set; } + public bool Edited { get; set; } + } + + /// + /// The DTO used to fetch results for a media item with its media path info + /// + private class MediaEntityDto : BaseDto + { + public string? MediaPath { get; set; } + } + + /// + /// The DTO used to fetch results for a member item + /// + private class MemberEntityDto : BaseDto + { + } + + public class VariantInfoDto + { + public int NodeId { get; set; } + public string IsoCode { get; set; } = null!; + public string Name { get; set; } = null!; + public bool DocumentPublished { get; set; } + public bool DocumentEdited { get; set; } + + public bool CultureAvailable { get; set; } + public bool CulturePublished { get; set; } + public bool CultureEdited { get; set; } + } + + // ReSharper disable once ClassNeverInstantiated.Local + /// + /// the DTO corresponding to fields selected by GetBase + /// + private class BaseDto + { + // ReSharper disable UnusedAutoPropertyAccessor.Local + // ReSharper disable UnusedMember.Local + public int NodeId { get; set; } + public bool Trashed { get; set; } + public int ParentId { get; set; } + public int? UserId { get; set; } + public int Level { get; set; } + public string Path { get; } = null!; + public int SortOrder { get; set; } + public Guid UniqueId { get; set; } + public string? Text { get; set; } + public Guid NodeObjectType { get; set; } + public DateTime CreateDate { get; set; } + public DateTime VersionDate { get; set; } + public int Children { get; set; } + public int VersionId { get; set; } + public string Alias { get; } = null!; + public string? Icon { get; set; } + public string? Thumbnail { get; set; } + public bool IsContainer { get; set; } + + // ReSharper restore UnusedAutoPropertyAccessor.Local + // ReSharper restore UnusedMember.Local + } + + #endregion + + #region Factory + + private EntitySlim BuildEntity(BaseDto dto) + { + if (dto.NodeObjectType == Constants.ObjectTypes.Document) + { + return BuildDocumentEntity(dto); + } + + if (dto.NodeObjectType == Constants.ObjectTypes.Media) + { + return BuildMediaEntity(dto); + } + + if (dto.NodeObjectType == Constants.ObjectTypes.Member) + { + return BuildMemberEntity(dto); + } + + // EntitySlim does not track changes + var entity = new EntitySlim(); + BuildEntity(entity, dto); + return entity; + } + + private static void BuildEntity(EntitySlim entity, BaseDto dto) + { + entity.Trashed = dto.Trashed; + entity.CreateDate = dto.CreateDate; + entity.UpdateDate = dto.VersionDate; + entity.CreatorId = dto.UserId ?? Constants.Security.UnknownUserId; + entity.Id = dto.NodeId; + entity.Key = dto.UniqueId; + entity.Level = dto.Level; + entity.Name = dto.Text; + entity.NodeObjectType = dto.NodeObjectType; + entity.ParentId = dto.ParentId; + entity.Path = dto.Path; + entity.SortOrder = dto.SortOrder; + entity.HasChildren = dto.Children > 0; + entity.IsContainer = dto.IsContainer; + } + + private static void BuildContentEntity(ContentEntitySlim entity, BaseDto dto) + { + BuildEntity(entity, dto); + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + } + + private MediaEntitySlim BuildMediaEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MediaEntitySlim(); + BuildContentEntity(entity, dto); + + // fill in the media info + if (dto is MediaEntityDto mediaEntityDto) + { + entity.MediaPath = mediaEntityDto.MediaPath; + } + else if (dto is GenericContentEntityDto genericContentEntityDto) + { + entity.MediaPath = genericContentEntityDto.MediaPath; + } + + return entity; + } + + private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new DocumentEntitySlim(); + BuildContentEntity(entity, dto); + + if (dto is DocumentEntityDto contentDto) + { + // fill in the invariant info + entity.Edited = contentDto.Edited; + entity.Published = contentDto.Published; + entity.Variations = contentDto.Variations; + } + + return entity; + } + + private MemberEntitySlim BuildMemberEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MemberEntitySlim(); + BuildEntity(entity, dto); + + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + + return entity; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs index 4ac8adbd91..611d89b6cf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepositoryBase.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,243 +10,234 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Provides a base class to all based repositories. +/// +/// The type of the entity's unique identifier. +/// The type of the entity managed by this repository. +public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository + where TEntity : class, IEntity { + private static RepositoryCachePolicyOptions? _defaultOptions; + private IRepositoryCachePolicy? _cachePolicy; + private IQuery? _hasIdQuery; + /// - /// Provides a base class to all based repositories. + /// Initializes a new instance of the class. /// - /// The type of the entity's unique identifier. - /// The type of the entity managed by this repository. - public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository - where TEntity : class, IEntity + protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger) + : base(scopeAccessor, appCaches) => + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + /// + /// Gets the logger + /// + protected ILogger> Logger { get; } + + /// + /// Gets the isolated cache for the + /// + protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); + + /// + /// Gets the isolated cache. + /// + /// Depends on the ambient scope cache mode. + protected IAppPolicyCache IsolatedCache { - private static RepositoryCachePolicyOptions? s_defaultOptions; - private IRepositoryCachePolicy? _cachePolicy; - private IQuery? _hasIdQuery; - - /// - /// Initializes a new instance of the class. - /// - protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, - ILogger> logger) - : base(scopeAccessor, appCaches) => - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - /// - /// Gets the logger - /// - protected ILogger> Logger { get; } - - /// - /// Gets the isolated cache for the - /// - protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate(); - - /// - /// Gets the isolated cache. - /// - /// Depends on the ambient scope cache mode. - protected IAppPolicyCache IsolatedCache + get { - get + switch (AmbientScope.RepositoryCacheMode) { - switch (AmbientScope.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - return AppCaches.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.Scoped: - return AmbientScope.IsolatedCaches.GetOrCreate(); - case RepositoryCacheMode.None: - return NoAppCache.Instance; - default: - throw new Exception("oops: cache mode."); - } + case RepositoryCacheMode.Default: + return AppCaches.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.Scoped: + return AmbientScope.IsolatedCaches.GetOrCreate(); + case RepositoryCacheMode.None: + return NoAppCache.Instance; + default: + throw new Exception("oops: cache mode."); } } - - /// - /// Gets the default - /// - protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions - = new RepositoryCachePolicyOptions(() => - { - // get count of all entities of current type (TEntity) to ensure cached result is correct - // create query once if it is needed (no need for locking here) - query is static! - IQuery query = _hasIdQuery ?? - (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0)); - return PerformCount(query); - })); - - /// - /// Gets the repository cache policy - /// - protected IRepositoryCachePolicy CachePolicy - { - get - { - if (AppCaches == AppCaches.NoCache) - { - return NoCacheRepositoryCachePolicy.Instance; - } - - // create the cache policy using IsolatedCache which is either global - // or scoped depending on the repository cache mode for the current scope - - switch (AmbientScope.RepositoryCacheMode) - { - case RepositoryCacheMode.Default: - case RepositoryCacheMode.Scoped: - // return the same cache policy in both cases - the cache policy is - // supposed to pick either the global or scope cache depending on the - // scope cache mode - return _cachePolicy ?? (_cachePolicy = CreateCachePolicy()); - case RepositoryCacheMode.None: - return NoCacheRepositoryCachePolicy.Instance; - default: - throw new Exception("oops: cache mode."); - } - } - } - - /// - /// Adds or Updates an entity of type TEntity - /// - /// This method is backed by an cache - public virtual void Save(TEntity entity) - { - if (entity.HasIdentity == false) - { - CachePolicy.Create(entity, PersistNewItem); - } - else - { - CachePolicy.Update(entity, PersistUpdatedItem); - } - } - - /// - /// Deletes the passed in entity - /// - public virtual void Delete(TEntity entity) - => CachePolicy.Delete(entity, PersistDeletedItem); - - /// - /// Gets an entity by the passed in Id utilizing the repository's cache policy - /// - public TEntity? Get(TId? id) - => CachePolicy.Get(id, PerformGet, PerformGetAll); - - /// - /// Gets all entities of type TEntity or a list according to the passed in Ids - /// - public IEnumerable GetMany(params TId[]? ids) - { - // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct() - - // don't query by anything that is a default of T (like a zero) - // TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids - // .Where(x => Equals(x, default(TId)) == false) - .ToArray(); - - // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, - // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group - if (ids?.Length <= Constants.Sql.MaxParameterCount) - { - return CachePolicy.GetAll(ids, PerformGetAll) ?? Enumerable.Empty(); - } - - var entities = new List(); - foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - var groups = CachePolicy.GetAll(group.ToArray(), PerformGetAll); - if (groups is not null) - { - entities.AddRange(groups); - } - } - - return entities; - } - - /// - /// Gets a list of entities by the passed in query - /// - public IEnumerable Get(IQuery query) - { - - // ensure we don't include any null refs in the returned collection! - return PerformGetByQuery(query) - .WhereNotNull(); - } - - /// - /// Returns a boolean indicating whether an entity with the passed Id exists - /// - public bool Exists(TId id) - => CachePolicy.Exists(id, PerformExists, PerformGetAll); - - /// - /// Returns an integer with the count of entities found with the passed in query - /// - public int Count(IQuery query) - => PerformCount(query); - - /// - /// Get the entity id for the - /// - protected virtual TId GetEntityId(TEntity entity) - => (TId)(object)entity.Id; - - /// - /// Create the repository cache policy - /// - protected virtual IRepositoryCachePolicy CreateCachePolicy() - => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - - protected abstract TEntity? PerformGet(TId? id); - - protected abstract IEnumerable PerformGetAll(params TId[]? ids); - - protected abstract IEnumerable PerformGetByQuery(IQuery query); - - protected abstract void PersistNewItem(TEntity item); - - protected abstract void PersistUpdatedItem(TEntity item); - - // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); - protected abstract Sql GetBaseQuery(bool isCount); - - protected abstract string GetBaseWhereClause(); - - protected abstract IEnumerable GetDeleteClauses(); - - protected virtual bool PerformExists(TId id) - { - Sql sql = GetBaseQuery(true); - sql.Where(GetBaseWhereClause(), new { id }); - var count = Database.ExecuteScalar(sql); - return count == 1; - } - - protected virtual int PerformCount(IQuery query) - { - Sql sqlClause = GetBaseQuery(true); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); - - return Database.ExecuteScalar(sql); - } - - protected virtual void PersistDeletedItem(TEntity entity) - { - IEnumerable deletes = GetDeleteClauses(); - foreach (var delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } - - entity.DeleteDate = DateTime.Now; - } + } + + /// + /// Gets the default + /// + protected virtual RepositoryCachePolicyOptions DefaultOptions => _defaultOptions + ??= new RepositoryCachePolicyOptions(() => + { + // get count of all entities of current type (TEntity) to ensure cached result is correct + // create query once if it is needed (no need for locking here) - query is static! + IQuery query = _hasIdQuery ??= AmbientScope.SqlContext.Query().Where(x => x.Id != 0); + return PerformCount(query); + }); + + /// + /// Gets the repository cache policy + /// + protected IRepositoryCachePolicy CachePolicy + { + get + { + if (AppCaches == AppCaches.NoCache) + { + return NoCacheRepositoryCachePolicy.Instance; + } + + // create the cache policy using IsolatedCache which is either global + // or scoped depending on the repository cache mode for the current scope + switch (AmbientScope.RepositoryCacheMode) + { + case RepositoryCacheMode.Default: + case RepositoryCacheMode.Scoped: + // return the same cache policy in both cases - the cache policy is + // supposed to pick either the global or scope cache depending on the + // scope cache mode + return _cachePolicy ??= CreateCachePolicy(); + case RepositoryCacheMode.None: + return NoCacheRepositoryCachePolicy.Instance; + default: + throw new Exception("oops: cache mode."); + } + } + } + + /// + /// Adds or Updates an entity of type TEntity + /// + /// This method is backed by an cache + public virtual void Save(TEntity entity) + { + if (entity.HasIdentity == false) + { + CachePolicy.Create(entity, PersistNewItem); + } + else + { + CachePolicy.Update(entity, PersistUpdatedItem); + } + } + + /// + /// Deletes the passed in entity + /// + public virtual void Delete(TEntity entity) + => CachePolicy.Delete(entity, PersistDeletedItem); + + /// + /// Gets an entity by the passed in Id utilizing the repository's cache policy + /// + public TEntity? Get(TId? id) + => CachePolicy.Get(id, PerformGet, PerformGetAll); + + /// + /// Gets all entities of type TEntity or a list according to the passed in Ids + /// + public IEnumerable GetMany(params TId[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct() + + // don't query by anything that is a default of T (like a zero) + // TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids + // .Where(x => Equals(x, default(TId)) == false) + .ToArray(); + + // can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities, + // the additional overhead of fetching them in groups is minimal compared to the lookup time of each group + if (ids?.Length <= Constants.Sql.MaxParameterCount) + { + return CachePolicy.GetAll(ids, PerformGetAll); + } + + var entities = new List(); + foreach (IEnumerable group in ids.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + TEntity[] groups = CachePolicy.GetAll(group.ToArray(), PerformGetAll); + entities.AddRange(groups); + } + + return entities; + } + + /// + /// Gets a list of entities by the passed in query + /// + public IEnumerable Get(IQuery query) => + + // ensure we don't include any null refs in the returned collection! + PerformGetByQuery(query) + .WhereNotNull(); + + /// + /// Returns a boolean indicating whether an entity with the passed Id exists + /// + public bool Exists(TId id) + => CachePolicy.Exists(id, PerformExists, PerformGetAll); + + /// + /// Returns an integer with the count of entities found with the passed in query + /// + public int Count(IQuery query) + => PerformCount(query); + + /// + /// Get the entity id for the + /// + protected virtual TId GetEntityId(TEntity entity) + => (TId)(object)entity.Id; + + /// + /// Create the repository cache policy + /// + protected virtual IRepositoryCachePolicy CreateCachePolicy() + => new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + + protected abstract TEntity? PerformGet(TId? id); + + protected abstract IEnumerable PerformGetAll(params TId[]? ids); + + protected abstract IEnumerable PerformGetByQuery(IQuery query); + + protected abstract void PersistNewItem(TEntity item); + + protected abstract void PersistUpdatedItem(TEntity item); + + // TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType); + protected abstract Sql GetBaseQuery(bool isCount); + + protected abstract string GetBaseWhereClause(); + + protected abstract IEnumerable GetDeleteClauses(); + + protected virtual bool PerformExists(TId id) + { + Sql sql = GetBaseQuery(true); + sql.Where(GetBaseWhereClause(), new { id }); + var count = Database.ExecuteScalar(sql); + return count == 1; + } + + protected virtual int PerformCount(IQuery query) + { + Sql sqlClause = GetBaseQuery(true); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database.ExecuteScalar(sql); + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + IEnumerable deletes = GetDeleteClauses(); + foreach (var delete in deletes) + { + Database.Execute(delete, new { id = GetEntityId(entity) }); + } + + entity.DeleteDate = DateTime.Now; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 9739c9a295..2207bdb16e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,289 +12,308 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginRepository, IExternalLoginWithKeyRepository { - internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginRepository, IExternalLoginWithKeyRepository + public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) { - public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - /// - [Obsolete("Use method that takes guid as param")] - public void DeleteUserLogins(int memberId) => DeleteUserLogins(memberId.ToGuid()); + /// + [Obsolete("Use method that takes guid as param")] + public void DeleteUserLogins(int memberId) => DeleteUserLogins(memberId.ToGuid()); - /// - [Obsolete("Use method that takes guid as param")] - public void Save(int userId, IEnumerable logins) => Save(userId.ToGuid(), logins); + /// + [Obsolete("Use method that takes guid as param")] + public void Save(int userId, IEnumerable logins) => Save(userId.ToGuid(), logins); - /// - [Obsolete("Use method that takes guid as param")] - public void Save(int userId, IEnumerable tokens) => Save(userId.ToGuid(), tokens); + /// + [Obsolete("Use method that takes guid as param")] + public void Save(int userId, IEnumerable tokens) => Save(userId.ToGuid(), tokens); - /// - public void DeleteUserLogins(Guid userOrMemberKey) => Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey }); + /// + /// Query for user tokens + /// + /// + /// + public IEnumerable Get(IQuery? query) + { + Sql sqlClause = GetBaseTokenQuery(false); - /// - public void Save(Guid userOrMemberKey, IEnumerable logins) + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List dtos = Database.Fetch(sql); + + foreach (ExternalLoginTokenDto dto in dtos) { - var sql = Sql() - .Select() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey) - .ForUpdate(); - - // deduplicate the logins - logins = logins.DistinctBy(x => x.ProviderKey + x.LoginProvider).ToList(); - - var toUpdate = new Dictionary(); - var toDelete = new List(); - var toInsert = new List(logins); - - var existingLogins = Database.Fetch(sql); - - foreach (var existing in existingLogins) - { - var found = logins.FirstOrDefault(x => - x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase) - && x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase)); - - if (found != null) - { - toUpdate.Add(existing.Id, found); - // if it's an update then it's not an insert - toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider); - } - else - { - toDelete.Add(existing.Id); - } - } - - // do the deletes, updates and inserts - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } - - foreach (var u in toUpdate) - { - Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key)); - } - - Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i))); + yield return ExternalLoginFactory.BuildEntity(dto); } + } - protected override IIdentityUserLogin? PerformGet(int id) + /// + /// Count for user tokens + /// + /// + /// + public int Count(IQuery query) + { + Sql sql = Sql().SelectCount().From(); + return Database.ExecuteScalar(sql); + } + + /// + public void DeleteUserLogins(Guid userOrMemberKey) => + Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey }); + + /// + public void Save(Guid userOrMemberKey, IEnumerable logins) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey) + .ForUpdate(); + + // deduplicate the logins + logins = logins.DistinctBy(x => x.ProviderKey + x.LoginProvider).ToList(); + + var toUpdate = new Dictionary(); + var toDelete = new List(); + var toInsert = new List(logins); + + List? existingLogins = Database.Fetch(sql); + + foreach (ExternalLoginDto? existing in existingLogins) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id = id }); + IExternalLogin? found = logins.FirstOrDefault(x => + x.LoginProvider.Equals(existing.LoginProvider, StringComparison.InvariantCultureIgnoreCase) + && x.ProviderKey.Equals(existing.ProviderKey, StringComparison.InvariantCultureIgnoreCase)); - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - if (dto == null) - return null; - - var entity = ExternalLoginFactory.BuildEntity(dto); - - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); - - return entity; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - if (ids?.Any() ?? false) + if (found != null) { - return PerformGetAllOnIds(ids); + toUpdate.Add(existing.Id, found); + + // if it's an update then it's not an insert + toInsert.RemoveAll(x => x.ProviderKey == found.ProviderKey && x.LoginProvider == found.LoginProvider); } - - var sql = GetBaseQuery(false).OrderByDescending(x => x.CreateDate); - - return ConvertFromDtos(Database.Fetch(sql)) - .ToArray();// we don't want to re-iterate again! - } - - private IEnumerable PerformGetAllOnIds(params int[] ids) - { - if (ids.Any() == false) yield break; - foreach (var id in ids) - { - IIdentityUserLogin? identityUserLogin = Get(id); - if (identityUserLogin is not null) - { - yield return identityUserLogin; - } - } - } - - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - foreach (var entity in dtos.Select(ExternalLoginFactory.BuildEntity)) - { - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); - - yield return entity; - } - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - foreach (var dto in dtos) - { - yield return ExternalLoginFactory.BuildEntity(dto); - } - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - if (isCount) - sql.SelectCount(); else - sql.SelectAll(); - sql.From(); - return sql; - } - - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.ExternalLogin}.id = @id"; - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoExternalLogin WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IIdentityUserLogin entity) - { - entity.AddingEntity(); - - var dto = ExternalLoginFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IIdentityUserLogin entity) - { - entity.UpdatingEntity(); - - var dto = ExternalLoginFactory.BuildDto(entity); - - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - /// - /// Query for user tokens - /// - /// - /// - public IEnumerable Get(IQuery? query) - { - Sql sqlClause = GetBaseTokenQuery(false); - - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); - - List dtos = Database.Fetch(sql); - - foreach (ExternalLoginTokenDto dto in dtos) { - yield return ExternalLoginFactory.BuildEntity(dto); + toDelete.Add(existing.Id); } } - /// - /// Count for user tokens - /// - /// - /// - public int Count(IQuery query) + // do the deletes, updates and inserts + if (toDelete.Count > 0) { - Sql sql = Sql().SelectCount().From(); - return Database.ExecuteScalar(sql); + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); } - /// - public void Save(Guid userOrMemberKey, IEnumerable tokens) + foreach (KeyValuePair u in toUpdate) { - // get the existing logins (provider + id) - var existingUserLogins = Database - .Fetch(GetBaseQuery(false).Where(x => x.UserOrMemberKey == userOrMemberKey)) - .ToDictionary(x => x.LoginProvider, x => x.Id); - - // deduplicate the tokens - tokens = tokens.DistinctBy(x => x.LoginProvider + x.Name).ToList(); - - var providers = tokens.Select(x => x.LoginProvider).Distinct().ToList(); - - Sql sql = GetBaseTokenQuery(true) - .WhereIn(x => x.LoginProvider, providers) - .Where(x => x.UserOrMemberKey == userOrMemberKey); - - var toUpdate = new Dictionary(); - var toDelete = new List(); - var toInsert = new List(tokens); - - var existingTokens = Database.Fetch(sql); - - foreach (ExternalLoginTokenDto existing in existingTokens) - { - IExternalLoginToken? found = tokens.FirstOrDefault(x => - x.LoginProvider.InvariantEquals(existing.ExternalLoginDto.LoginProvider) - && x.Name.InvariantEquals(existing.Name)); - - if (found != null) - { - toUpdate.Add(existing.Id, (found, existing.ExternalLoginId)); - // if it's an update then it's not an insert - toInsert.RemoveAll(x => x.LoginProvider.InvariantEquals(found.LoginProvider) && x.Name.InvariantEquals(found.Name)); - } - else - { - toDelete.Add(existing.Id); - } - } - - // do the deletes, updates and inserts - if (toDelete.Count > 0) - { - Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); - } - - foreach (KeyValuePair u in toUpdate) - { - Database.Update(ExternalLoginFactory.BuildDto(u.Value.externalLoginId, u.Value.externalLoginToken, u.Key)); - } - - var insertDtos = new List(); - foreach(IExternalLoginToken t in toInsert) - { - if (!existingUserLogins.TryGetValue(t.LoginProvider, out int externalLoginId)) - { - throw new InvalidOperationException($"A token was attempted to be saved for login provider {t.LoginProvider} which is not assigned to this user"); - } - insertDtos.Add(ExternalLoginFactory.BuildDto(externalLoginId, t)); - } - Database.InsertBulk(insertDtos); + Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key)); } - private Sql GetBaseTokenQuery(bool forUpdate) - => forUpdate ? Sql() + Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i))); + } + + /// + public void Save(Guid userOrMemberKey, IEnumerable tokens) + { + // get the existing logins (provider + id) + var existingUserLogins = Database + .Fetch(GetBaseQuery(false) + .Where(x => x.UserOrMemberKey == userOrMemberKey)) + .ToDictionary(x => x.LoginProvider, x => x.Id); + + // deduplicate the tokens + tokens = tokens.DistinctBy(x => x.LoginProvider + x.Name).ToList(); + + var providers = tokens.Select(x => x.LoginProvider).Distinct().ToList(); + + Sql sql = GetBaseTokenQuery(true) + .WhereIn(x => x.LoginProvider, providers) + .Where(x => x.UserOrMemberKey == userOrMemberKey); + + var toUpdate = new Dictionary(); + var toDelete = new List(); + var toInsert = new List(tokens); + + List? existingTokens = Database.Fetch(sql); + + foreach (ExternalLoginTokenDto existing in existingTokens) + { + IExternalLoginToken? found = tokens.FirstOrDefault(x => + x.LoginProvider.InvariantEquals(existing.ExternalLoginDto.LoginProvider) + && x.Name.InvariantEquals(existing.Name)); + + if (found != null) + { + toUpdate.Add(existing.Id, (found, existing.ExternalLoginId)); + + // if it's an update then it's not an insert + toInsert.RemoveAll(x => + x.LoginProvider.InvariantEquals(found.LoginProvider) && x.Name.InvariantEquals(found.Name)); + } + else + { + toDelete.Add(existing.Id); + } + } + + // do the deletes, updates and inserts + if (toDelete.Count > 0) + { + Database.DeleteMany().Where(x => toDelete.Contains(x.Id)).Execute(); + } + + foreach (KeyValuePair u in toUpdate) + { + Database.Update(ExternalLoginFactory.BuildDto(u.Value.externalLoginId, u.Value.externalLoginToken, u.Key)); + } + + var insertDtos = new List(); + foreach (IExternalLoginToken t in toInsert) + { + if (!existingUserLogins.TryGetValue(t.LoginProvider, out var externalLoginId)) + { + throw new InvalidOperationException( + $"A token was attempted to be saved for login provider {t.LoginProvider} which is not assigned to this user"); + } + + insertDtos.Add(ExternalLoginFactory.BuildDto(externalLoginId, t)); + } + + Database.InsertBulk(insertDtos); + } + + protected override IIdentityUserLogin? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + + ExternalLoginDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + if (dto == null) + { + return null; + } + + IIdentityUserLogin entity = ExternalLoginFactory.BuildEntity(dto); + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + + return entity; + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + if (ids?.Any() ?? false) + { + return PerformGetAllOnIds(ids); + } + + Sql sql = GetBaseQuery(false).OrderByDescending(x => x.CreateDate); + + return ConvertFromDtos(Database.Fetch(sql)) + .ToArray(); // we don't want to re-iterate again! + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + foreach (ExternalLoginDto? dto in dtos) + { + yield return ExternalLoginFactory.BuildEntity(dto); + } + } + + private IEnumerable PerformGetAllOnIds(params int[] ids) + { + if (ids.Any() == false) + { + yield break; + } + + foreach (var id in ids) + { + IIdentityUserLogin? identityUserLogin = Get(id); + if (identityUserLogin is not null) + { + yield return identityUserLogin; + } + } + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + foreach (IIdentityUserLogin entity in dtos.Select(ExternalLoginFactory.BuildEntity)) + { + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); + + yield return entity; + } + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) + { + sql.SelectCount(); + } + else + { + sql.SelectAll(); + } + + sql.From(); + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.ExternalLogin}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoExternalLogin WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(IIdentityUserLogin entity) + { + entity.AddingEntity(); + + ExternalLoginDto dto = ExternalLoginFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IIdentityUserLogin entity) + { + entity.UpdatingEntity(); + + ExternalLoginDto dto = ExternalLoginFactory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + private Sql GetBaseTokenQuery(bool forUpdate) + => forUpdate + ? Sql() .Select(r => r.Select(x => x.ExternalLoginDto)) .From() .AppendForUpdateHint() // ensure these table values are locked for updates, the ForUpdate ext method does not work here @@ -309,5 +325,4 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .From() .InnerJoin() .On(x => x.ExternalLoginId, x => x.Id); - } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs index 54d0796680..a6e9d517c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/FileRepository.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.IO; using System.Text; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -7,235 +5,238 @@ using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Persistence; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal abstract class FileRepository : IReadRepository, IWriteRepository + where TEntity : IFile { - internal abstract class FileRepository : IReadRepository, IWriteRepository - where TEntity : IFile + protected FileRepository(IFileSystem? fileSystem) => FileSystem = fileSystem; + + protected IFileSystem? FileSystem { get; } + + public virtual void AddFolder(string folderPath) => PersistNewItem(new Folder(folderPath)); + + public virtual void DeleteFolder(string folderPath) => PersistDeletedItem(new Folder(folderPath)); + + public Stream GetFileContentStream(string filepath) { - protected FileRepository(IFileSystem? fileSystem) => FileSystem = fileSystem; - - protected IFileSystem? FileSystem { get; } - - public virtual void AddFolder(string folderPath) => PersistNewItem(new Folder(folderPath)); - - public virtual void DeleteFolder(string folderPath) => PersistDeletedItem(new Folder(folderPath)); - - #region Implementation of IRepository - - public virtual void Save(TEntity entity) + if (FileSystem?.FileExists(filepath) == false) { - if (FileSystem?.FileExists(entity.OriginalPath) == false) + return Stream.Null; + } + + try + { + return FileSystem?.OpenFile(filepath) ?? Stream.Null; + } + catch + { + return Stream.Null; // deal with race conds + } + } + + internal virtual void PersistNewFolder(Folder entity) => FileSystem?.CreateFolder(entity.Path); + + internal virtual void PersistDeletedFolder(Folder entity) => FileSystem?.DeleteDirectory(entity.Path); + + /// + /// Gets a stream that is used to write to the file + /// + /// + /// + protected virtual Stream GetContentStream(string content) => new MemoryStream(Encoding.UTF8.GetBytes(content)); + + /// + /// Returns all files in the file system + /// + /// + /// + /// + /// Returns a list of all files with their paths. For example: + /// \hello.txt + /// \folder1\test.txt + /// \folder1\blah.csv + /// \folder1\folder2\blahhhhh.svg + /// + protected IEnumerable FindAllFiles(string path, string filter) + { + var list = new List(); + IEnumerable? collection = FileSystem?.GetFiles(path, filter); + if (collection is not null) + { + list.AddRange(collection); + } + + IEnumerable? directories = FileSystem?.GetDirectories(path); + if (directories is not null) + { + foreach (var directory in directories) { - PersistNewItem(entity); - } - else - { - PersistUpdatedItem(entity); + list.AddRange(FindAllFiles(directory, filter)); } } - public virtual void Delete(TEntity entity) => PersistDeletedItem(entity); + return list; + } - public abstract TEntity? Get(TId? id); - - public abstract IEnumerable GetMany(params TId[]? ids); - - public virtual bool Exists(TId id) => FileSystem?.FileExists(id!.ToString()!) ?? false; - - #endregion - - #region Implementation of IUnitOfWorkRepository - - public void PersistNewItem(IEntity entity) + protected string? GetFileContent(string? filename) + { + if (filename is null || FileSystem?.FileExists(filename) == false) { - //special case for folder - if (entity is Folder folder) - { - PersistNewFolder(folder); - } - else - { - PersistNewItem((TEntity)entity); - } - } - - public void PersistUpdatedItem(IEntity entity) => PersistUpdatedItem((TEntity)entity); - - public void PersistDeletedItem(IEntity entity) - { - //special case for folder - if (entity is Folder folder) - { - PersistDeletedFolder(folder); - } - else - { - PersistDeletedItem((TEntity)entity); - } - } - - #endregion - - internal virtual void PersistNewFolder(Folder entity) => FileSystem?.CreateFolder(entity.Path); - - internal virtual void PersistDeletedFolder(Folder entity) => FileSystem?.DeleteDirectory(entity.Path); - - #region Abstract IUnitOfWorkRepository Methods - - protected virtual void PersistNewItem(TEntity entity) - { - if (entity.Content is null || FileSystem is null) - { - return; - } - using (Stream stream = GetContentStream(entity.Content)) - { - FileSystem.AddFile(entity.Path, stream, true); - entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; - entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; - //the id can be the hash - entity.Id = entity.Path.GetHashCode(); - entity.Key = entity.Path.EncodeAsGuid(); - entity.VirtualPath = FileSystem?.GetUrl(entity.Path); - } - } - - protected virtual void PersistUpdatedItem(TEntity entity) - { - if (entity.Content is null || FileSystem is null) - { - return; - } - using (Stream stream = GetContentStream(entity.Content)) - { - FileSystem.AddFile(entity.Path, stream, true); - entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; - entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; - //the id can be the hash - entity.Id = entity.Path.GetHashCode(); - entity.Key = entity.Path.EncodeAsGuid(); - entity.VirtualPath = FileSystem.GetUrl(entity.Path); - } - - //now that the file has been written, we need to check if the path had been changed - if (entity.Path.InvariantEquals(entity.OriginalPath) == false) - { - //delete the original file - FileSystem?.DeleteFile(entity.OriginalPath); - //reset the original path on the file - entity.ResetOriginalPath(); - } - } - - protected virtual void PersistDeletedItem(TEntity entity) - { - if (FileSystem?.FileExists(entity.Path) ?? false) - { - FileSystem.DeleteFile(entity.Path); - } - } - - #endregion - - /// - /// Gets a stream that is used to write to the file - /// - /// - /// - protected virtual Stream GetContentStream(string content) => new MemoryStream(Encoding.UTF8.GetBytes(content)); - - /// - /// Returns all files in the file system - /// - /// - /// - /// - /// Returns a list of all files with their paths. For example: - /// - /// \hello.txt - /// \folder1\test.txt - /// \folder1\blah.csv - /// \folder1\folder2\blahhhhh.svg - /// - protected IEnumerable FindAllFiles(string path, string filter) - { - var list = new List(); - var collection = FileSystem?.GetFiles(path, filter); - if (collection is not null) - { - list.AddRange(collection); - } - - IEnumerable? directories = FileSystem?.GetDirectories(path); - if (directories is not null) - { - foreach (var directory in directories) - { - list.AddRange(FindAllFiles(directory, filter)); - } - } - - return list; - } - - protected string? GetFileContent(string? filename) - { - if (filename is null || FileSystem?.FileExists(filename) == false) - { - return null; - } - - try - { - using Stream? stream = FileSystem?.OpenFile(filename!); - if (stream is not null) - { - using var reader = new StreamReader(stream, Encoding.UTF8, true); - return reader.ReadToEnd(); - } - } - catch - { - return null; // deal with race conds - } - return null; } - public Stream GetFileContentStream(string filepath) + try { - if (FileSystem?.FileExists(filepath) == false) + using Stream? stream = FileSystem?.OpenFile(filename); + if (stream is not null) { - return Stream.Null; - } - - try - { - return FileSystem?.OpenFile(filepath) ?? Stream.Null; - } - catch - { - return Stream.Null; // deal with race conds + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); } } - - public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); - - public long GetFileSize(string filename) + catch { - if (FileSystem?.FileExists(filename) == false) - { - return -1; - } + return null; // deal with race conds + } - try - { - return FileSystem!.GetSize(filename); - } - catch - { - return -1; // deal with race conds - } + return null; + } + + public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); + + public long GetFileSize(string filename) + { + if (FileSystem?.FileExists(filename) == false) + { + return -1; + } + + try + { + return FileSystem!.GetSize(filename); + } + catch + { + return -1; // deal with race conds } } + + #region Implementation of IRepository + + public virtual void Save(TEntity entity) + { + if (FileSystem?.FileExists(entity.OriginalPath) == false) + { + PersistNewItem(entity); + } + else + { + PersistUpdatedItem(entity); + } + } + + public virtual void Delete(TEntity entity) => PersistDeletedItem(entity); + + public abstract TEntity? Get(TId? id); + + public abstract IEnumerable GetMany(params TId[]? ids); + + public virtual bool Exists(TId id) => FileSystem?.FileExists(id!.ToString()!) ?? false; + + #endregion + + #region Implementation of IUnitOfWorkRepository + + public void PersistNewItem(IEntity entity) + { + // special case for folder + if (entity is Folder folder) + { + PersistNewFolder(folder); + } + else + { + PersistNewItem((TEntity)entity); + } + } + + public void PersistUpdatedItem(IEntity entity) => PersistUpdatedItem((TEntity)entity); + + public void PersistDeletedItem(IEntity entity) + { + // special case for folder + if (entity is Folder folder) + { + PersistDeletedFolder(folder); + } + else + { + PersistDeletedItem((TEntity)entity); + } + } + + #endregion + + #region Abstract IUnitOfWorkRepository Methods + + protected virtual void PersistNewItem(TEntity entity) + { + if (entity.Content is null || FileSystem is null) + { + return; + } + + using (Stream stream = GetContentStream(entity.Content)) + { + FileSystem.AddFile(entity.Path, stream, true); + entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; + entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; + + // the id can be the hash + entity.Id = entity.Path.GetHashCode(); + entity.Key = entity.Path.EncodeAsGuid(); + entity.VirtualPath = FileSystem?.GetUrl(entity.Path); + } + } + + protected virtual void PersistUpdatedItem(TEntity entity) + { + if (entity.Content is null || FileSystem is null) + { + return; + } + + using (Stream stream = GetContentStream(entity.Content)) + { + FileSystem.AddFile(entity.Path, stream, true); + entity.CreateDate = FileSystem.GetCreated(entity.Path).UtcDateTime; + entity.UpdateDate = FileSystem.GetLastModified(entity.Path).UtcDateTime; + + // the id can be the hash + entity.Id = entity.Path.GetHashCode(); + entity.Key = entity.Path.EncodeAsGuid(); + entity.VirtualPath = FileSystem.GetUrl(entity.Path); + } + + // now that the file has been written, we need to check if the path had been changed + if (entity.Path.InvariantEquals(entity.OriginalPath) == false) + { + // delete the original file + FileSystem?.DeleteFile(entity.OriginalPath); + + // reset the original path on the file + entity.ResetOriginalPath(); + } + } + + protected virtual void PersistDeletedItem(TEntity entity) + { + if (FileSystem?.FileExists(entity.Path) ?? false) + { + FileSystem.DeleteFile(entity.Path); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs index 007e09c4a2..fe7929ccd5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/IdKeyMapRepository.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; @@ -10,44 +9,54 @@ public class IdKeyMapRepository : IIdKeyMapRepository { private readonly IScopeAccessor _scopeAccessor; - public IdKeyMapRepository(IScopeAccessor scopeAccessor) - { - _scopeAccessor = scopeAccessor; - } + public IdKeyMapRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; public int? GetIdForKey(Guid key, UmbracoObjectTypes umbracoObjectType) { - //if it's unknown don't include the nodeObjectType in the query + // if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key}); - } - else - { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", - new { id = key, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT id FROM umbracoNode WHERE uniqueId=@id", new { id = key }); } + + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT id FROM umbracoNode WHERE uniqueId=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + new + { + id = key, + type = GetNodeObjectTypeGuid(umbracoObjectType), + reservation = Constants.ObjectTypes.IdReservation, + }); } public Guid? GetIdForKey(int id, UmbracoObjectTypes umbracoObjectType) { - //if it's unknown don't include the nodeObjectType in the query + // if it's unknown don't include the nodeObjectType in the query if (umbracoObjectType == UmbracoObjectTypes.Unknown) { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id }); - } - else - { - return _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", - new { id, type = GetNodeObjectTypeGuid(umbracoObjectType), reservation = Cms.Core.Constants.ObjectTypes.IdReservation }); + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT uniqueId FROM umbracoNode WHERE id=@id", new { id }); } + + return _scopeAccessor.AmbientScope?.Database.ExecuteScalar( + "SELECT uniqueId FROM umbracoNode WHERE id=@id AND (nodeObjectType=@type OR nodeObjectType=@reservation)", + new + { + id, + type = GetNodeObjectTypeGuid(umbracoObjectType), + reservation = Constants.ObjectTypes.IdReservation, + }); } private Guid GetNodeObjectTypeGuid(UmbracoObjectTypes umbracoObjectType) { - var guid = umbracoObjectType.GetGuid(); + Guid guid = umbracoObjectType.GetGuid(); if (guid == Guid.Empty) + { throw new NotSupportedException("Unsupported object type (" + umbracoObjectType + ")."); + } + return guid; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 6fd3da008c..c7259df863 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -12,112 +10,112 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class KeyValueRepository : EntityRepositoryBase, IKeyValueRepository { - internal class KeyValueRepository : EntityRepositoryBase, IKeyValueRepository + public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public KeyValueRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } - - /// - public IReadOnlyDictionary? FindByKeyPrefix(string keyPrefix) - => Get(Query().Where(entity => entity.Identifier!.StartsWith(keyPrefix)))? - .ToDictionary(x => x.Identifier!, x => x.Value); - - #region Overrides of IReadWriteQueryRepository - - public override void Save(IKeyValue entity) - { - if (Get(entity.Identifier) == null) - PersistNewItem(entity); - else - PersistUpdatedItem(entity); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From(); - - return sql; - } - - protected override string GetBaseWhereClause() => Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; - - protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); - - protected override IKeyValue? PerformGet(string? id) - { - var sql = GetBaseQuery(false).Where(x => x.Key == id); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - protected override IEnumerable PerformGetAll(params string[]? ids) - { - var sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); - var dtos = Database.Fetch(sql); - return dtos?.WhereNotNull().Select(Map)!; - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(Map).WhereNotNull(); - } - - protected override void PersistNewItem(IKeyValue entity) - { - var dto = Map(entity); - Database.Insert(dto); - } - - protected override void PersistUpdatedItem(IKeyValue entity) - { - var dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } - } - - private static KeyValueDto? Map(IKeyValue keyValue) - { - if (keyValue == null) return null; - - return new KeyValueDto - { - Key = keyValue.Identifier, - Value = keyValue.Value, - UpdateDate = keyValue.UpdateDate, - }; - } - - private static IKeyValue? Map(KeyValueDto dto) - { - if (dto == null) return null; - - return new KeyValue - { - Identifier = dto.Key, - Value = dto.Value, - UpdateDate = dto.UpdateDate, - }; - } - - #endregion } + + /// + public IReadOnlyDictionary FindByKeyPrefix(string keyPrefix) + => Get(Query().Where(entity => entity.Identifier.StartsWith(keyPrefix))) + .ToDictionary(x => x.Identifier, x => x.Value); + + #region Overrides of IReadWriteQueryRepository + + public override void Save(IKeyValue entity) + { + if (Get(entity.Identifier) == null) + { + PersistNewItem(entity); + } + else + { + PersistUpdatedItem(entity); + } + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + return sql; + } + + protected override string GetBaseWhereClause() => Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; + + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); + + protected override IKeyValue? PerformGet(string? id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Key == id); + KeyValueDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + protected override IEnumerable PerformGetAll(params string[]? ids) + { + Sql sql = GetBaseQuery(false).WhereIn(x => x.Key, ids); + List? dtos = Database.Fetch(sql); + return dtos?.WhereNotNull().Select(Map)!; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(Map).WhereNotNull(); + } + + protected override void PersistNewItem(IKeyValue entity) + { + KeyValueDto? dto = Map(entity); + Database.Insert(dto); + } + + protected override void PersistUpdatedItem(IKeyValue entity) + { + KeyValueDto? dto = Map(entity); + if (dto is not null) + { + Database.Update(dto); + } + } + + private static KeyValueDto? Map(IKeyValue? keyValue) + { + if (keyValue == null) + { + return null; + } + + return new KeyValueDto { Key = keyValue.Identifier, Value = keyValue.Value, UpdateDate = keyValue.UpdateDate }; + } + + private static IKeyValue? Map(KeyValueDto? dto) + { + if (dto == null) + { + return null; + } + + return new KeyValue { Identifier = dto.Key, Value = dto.Value, UpdateDate = dto.UpdateDate }; + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs index bf1bc4f4b4..398a55ebaf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepository.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; 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,316 +11,350 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class LanguageRepository : EntityRepositoryBase, ILanguageRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class LanguageRepository : EntityRepositoryBase, ILanguageRepository + private readonly Dictionary _codeIdMap = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _idCodeMap = new(); + + public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - private readonly Dictionary _codeIdMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - private readonly Dictionary _idCodeMap = new Dictionary(); + } - public LanguageRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + private FullDataSetRepositoryCachePolicy? TypedCachePolicy => + CachePolicy as FullDataSetRepositoryCachePolicy; - protected override IRepositoryCachePolicy CreateCachePolicy() + public ILanguage? GetByIsoCode(string isoCode) + { + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + TypedCachePolicy.GetAllCached(PerformGetAll); } - private FullDataSetRepositoryCachePolicy? TypedCachePolicy => CachePolicy as FullDataSetRepositoryCachePolicy; + var id = GetIdByIsoCode(isoCode, false); + return id.HasValue ? Get(id.Value) : null; + } - #region Overrides of RepositoryBase - - protected override ILanguage? PerformGet(int id) + // fast way of getting an id for an isoCode - avoiding cloning + // _codeIdMap is rebuilt whenever PerformGetAll runs + public int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true) + { + if (isoCode == null) { - return PerformGetAll(id).FirstOrDefault(); + return null; } - protected override IEnumerable PerformGetAll(params int[]? ids) + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) { - var sql = GetBaseQuery(false).Where(x => x.Id > 0); - if (ids?.Any() ?? false) + TypedCachePolicy.GetAllCached(PerformGetAll); + } + else + { + PerformGetAll(); // We don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap + } + + lock (_codeIdMap) + { + if (_codeIdMap.TryGetValue(isoCode, out var id)) { - sql.WhereIn(x => x.Id, ids); + return id; } + } - //this needs to be sorted since that is the way legacy worked - default language is the first one!! - //even though legacy didn't sort, it should be by id - sql.OrderBy(x => x.Id); + if (throwOnNotFound) + { + throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); + } - // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + return null; + } - // initialize the code-id map - lock (_codeIdMap) + // fast way of getting an isoCode for an id - avoiding cloning + // _idCodeMap is rebuilt whenever PerformGetAll runs + public string? GetIsoCodeById(int? id, bool throwOnNotFound = true) + { + if (id == null) + { + return null; + } + + // ensure cache is populated, in a non-expensive way + if (TypedCachePolicy != null) + { + TypedCachePolicy.GetAllCached(PerformGetAll); + } + else + { + PerformGetAll(); + } + + // yes, we want to lock _codeIdMap + lock (_codeIdMap) + { + if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) { - _codeIdMap.Clear(); - _idCodeMap.Clear(); - foreach (var language in languages) - { - _codeIdMap[language.IsoCode] = language.Id; - _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); - } + return isoCode; } - - return languages; } - protected override IEnumerable PerformGetByQuery(IQuery query) + if (throwOnNotFound) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - var dtos = Database.Fetch(sql); - return dtos.Select(ConvertFromDto).ToList(); + throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); } - #endregion + return null; + } - #region Overrides of EntityRepositoryBase + public string GetDefaultIsoCode() => GetDefault().IsoCode; - protected override Sql GetBaseQuery(bool isCount) + public int? GetDefaultId() => GetDefault().Id; + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + + protected ILanguage ConvertFromDto(LanguageDto dto) + => LanguageFactory.BuildEntity(dto); + + // do NOT leak that language, it's not deep-cloned! + private ILanguage GetDefault() + { + // get all cached + var languages = + (TypedCachePolicy + ?.GetAllCached( + PerformGetAll) // Try to get all cached non-cloned if using the correct cache policy (not the case in unit tests) + ?? CachePolicy.GetAll(Array.Empty(), PerformGetAll)).ToList(); + + ILanguage? language = languages.FirstOrDefault(x => x.IsDefault); + if (language != null) { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql.From(); - - return sql; + return language; } - protected override string GetBaseWhereClause() + // this is an anomaly, the service/repo should ensure it cannot happen + Logger.LogWarning( + "There is no default language. Fix this anomaly by editing the language table in database and setting one language as the default language."); + + // still, don't kill the site, and return "something" + ILanguage? first = null; + foreach (ILanguage l in languages) { - return $"{Constants.DatabaseSchema.Tables.Language}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - //NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) - // but we still need to remove them - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Language + " WHERE id = @id" - }; - return list; - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(ILanguage entity) - { - // validate iso code and culture name - if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - - entity.AddingEntity(); - - // deal with entity becoming the new default entity - if (entity.IsDefault) + if (first == null || l.Id < first.Id) { - // set all other entities to non-default - // safe (no race cond) because the service locks languages - var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefault, false)); - Database.Execute(setAllDefaultToFalse); + first = l; } - - // fallback cycles are detected at service level - - // insert - var dto = LanguageFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(ILanguage entity) + return first!; + } + + #region Overrides of RepositoryBase + + protected override ILanguage? PerformGet(int id) => PerformGetAll(id).FirstOrDefault(); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id > 0); + if (ids?.Any() ?? false) { - // validate iso code and culture name - if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) - throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); - - entity.UpdatingEntity(); - - if (entity.IsDefault) - { - // deal with entity becoming the new default entity - - // set all other entities to non-default - // safe (no race cond) because the service locks languages - var setAllDefaultToFalse = Sql() - .Update(u => u.Set(x => x.IsDefault, false)); - Database.Execute(setAllDefaultToFalse); - } - else - { - // deal with the entity not being default anymore - // which is illegal - another entity has to become default - var selectDefaultId = Sql() - .Select(x => x.Id) - .From() - .Where(x => x.IsDefault); - - var defaultId = Database.ExecuteScalar(selectDefaultId); - if (entity.Id == defaultId) - throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); - } - - if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) - { - //if the iso code is changing, ensure there's not another lang with the same code already assigned - var sameCode = Sql() - .SelectCount() - .From() - .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); - - var countOfSameCode = Database.ExecuteScalar(sameCode); - if (countOfSameCode > 0) - throw new InvalidOperationException($"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); - } - - // fallback cycles are detected at service level - - // update - var dto = LanguageFactory.BuildDto(entity); - Database.Update(dto); - entity.ResetDirtyProperties(); + sql.WhereIn(x => x.Id, ids); } - protected override void PersistDeletedItem(ILanguage entity) + // this needs to be sorted since that is the way legacy worked - default language is the first one!! + // even though legacy didn't sort, it should be by id + sql.OrderBy(x => x.Id); + + // get languages + var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + + // initialize the code-id map + lock (_codeIdMap) { - // validate that the entity is not the default language. + _codeIdMap.Clear(); + _idCodeMap.Clear(); + foreach (ILanguage language in languages) + { + _codeIdMap[language.IsoCode] = language.Id; + _idCodeMap[language.Id] = language.IsoCode.ToLowerInvariant(); + } + } + + return languages; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + List? dtos = Database.Fetch(sql); + return dtos.Select(ConvertFromDto).ToList(); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql.From(); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Language}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + // NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) + // but we still need to remove them + "DELETE FROM " + Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Language + " WHERE id = @id", + }; + return list; + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(ILanguage entity) + { + // validate iso code and culture name + if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); + } + + entity.AddingEntity(); + + // deal with entity becoming the new default entity + if (entity.IsDefault) + { + // set all other entities to non-default // safe (no race cond) because the service locks languages + Sql setAllDefaultToFalse = Sql() + .Update(u => u.Set(x => x.IsDefault, false)); + Database.Execute(setAllDefaultToFalse); + } - var selectDefaultId = Sql() + // fallback cycles are detected at service level + + // insert + LanguageDto dto = LanguageFactory.BuildDto(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(ILanguage entity) + { + // validate iso code and culture name + if (entity.IsoCode.IsNullOrWhiteSpace() || entity.CultureName.IsNullOrWhiteSpace()) + { + throw new InvalidOperationException("Cannot save a language without an ISO code and a culture name."); + } + + entity.UpdatingEntity(); + + if (entity.IsDefault) + { + // deal with entity becoming the new default entity + + // set all other entities to non-default + // safe (no race cond) because the service locks languages + Sql setAllDefaultToFalse = Sql() + .Update(u => u.Set(x => x.IsDefault, false)); + Database.Execute(setAllDefaultToFalse); + } + else + { + // deal with the entity not being default anymore + // which is illegal - another entity has to become default + Sql selectDefaultId = Sql() .Select(x => x.Id) .From() .Where(x => x.IsDefault); var defaultId = Database.ExecuteScalar(selectDefaultId); if (entity.Id == defaultId) - throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); - - // We need to remove any references to the language if it's being used as a fall-back from other ones - var clearFallbackLanguage = Sql() - .Update(u => u - .Set(x => x.FallbackLanguageId, null)) - .Where(x => x.FallbackLanguageId == entity.Id); - - Database.Execute(clearFallbackLanguage); - - // delete - base.PersistDeletedItem(entity); - } - - #endregion - - protected ILanguage ConvertFromDto(LanguageDto dto) - => LanguageFactory.BuildEntity(dto); - - public ILanguage? GetByIsoCode(string isoCode) - { - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) { - TypedCachePolicy.GetAllCached(PerformGetAll); + throw new InvalidOperationException( + $"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); } - - var id = GetIdByIsoCode(isoCode, throwOnNotFound: false); - return id.HasValue ? Get(id.Value) : null; } - // fast way of getting an id for an isoCode - avoiding cloning - // _codeIdMap is rebuilt whenever PerformGetAll runs - public int? GetIdByIsoCode(string? isoCode, bool throwOnNotFound = true) + if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) { - if (isoCode == null) return null; + // If the iso code is changing, ensure there's not another lang with the same code already assigned + Sql sameCode = Sql() + .SelectCount() + .From() + .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) - TypedCachePolicy.GetAllCached(PerformGetAll); - else - PerformGetAll(); //we don't have a typed cache (i.e. unit tests) but need to populate the _codeIdMap - - lock (_codeIdMap) + var countOfSameCode = Database.ExecuteScalar(sameCode); + if (countOfSameCode > 0) { - if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; + throw new InvalidOperationException( + $"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); } - if (throwOnNotFound) - throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); - - return null; } - // fast way of getting an isoCode for an id - avoiding cloning - // _idCodeMap is rebuilt whenever PerformGetAll runs - public string? GetIsoCodeById(int? id, bool throwOnNotFound = true) - { - if (id == null) return null; + // fallback cycles are detected at service level - // ensure cache is populated, in a non-expensive way - if (TypedCachePolicy != null) - TypedCachePolicy.GetAllCached(PerformGetAll); - else - PerformGetAll(); - - lock (_codeIdMap) // yes, we want to lock _codeIdMap - { - if (_idCodeMap.TryGetValue(id.Value, out var isoCode)) return isoCode; - } - if (throwOnNotFound) - throw new ArgumentException($"Id {id} does not correspond to an existing language.", nameof(id)); - - return null; - } - - public string GetDefaultIsoCode() - { - return GetDefault().IsoCode; - } - - public int? GetDefaultId() - { - return GetDefault()?.Id; - } - - // do NOT leak that language, it's not deep-cloned! - private ILanguage GetDefault() - { - // get all cached - var languages = (TypedCachePolicy?.GetAllCached(PerformGetAll) //try to get all cached non-cloned if using the correct cache policy (not the case in unit tests) - ?? CachePolicy.GetAll(Array.Empty(), PerformGetAll)!).ToList(); - - var language = languages.FirstOrDefault(x => x.IsDefault); - if (language != null) return language; - - // this is an anomaly, the service/repo should ensure it cannot happen - Logger.LogWarning("There is no default language. Fix this anomaly by editing the language table in database and setting one language as the default language."); - - // still, don't kill the site, and return "something" - - ILanguage? first = null; - foreach (var l in languages) - { - if (first == null || l.Id < first.Id) - first = l; - } - - return first!; - } + // update + LanguageDto dto = LanguageFactory.BuildDto(entity); + Database.Update(dto); + entity.ResetDirtyProperties(); } + + protected override void PersistDeletedItem(ILanguage entity) + { + // validate that the entity is not the default language. + // safe (no race cond) because the service locks languages + Sql selectDefaultId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + var defaultId = Database.ExecuteScalar(selectDefaultId); + if (entity.Id == defaultId) + { + throw new InvalidOperationException($"Cannot delete the default language ({entity.IsoCode})."); + } + + // We need to remove any references to the language if it's being used as a fall-back from other ones + Sql clearFallbackLanguage = Sql() + .Update(u => u + .Set(x => x.FallbackLanguageId, null)) + .Where(x => x.FallbackLanguageId == entity.Id); + + Database.Execute(clearFallbackLanguage); + + // delete + base.PersistDeletedItem(entity); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs index 72324eb874..2caad816fb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LanguageRepositoryExtensions.cs @@ -1,14 +1,17 @@ -using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal static class LanguageRepositoryExtensions { - internal static class LanguageRepositoryExtensions + public static bool IsDefault(this ILanguageRepository repo, string? culture) { - public static bool IsDefault(this ILanguageRepository repo, string culture) + if (culture == null || culture == "*") { - if (culture == null || culture == "*") return false; - return repo.GetDefaultIsoCode().InvariantEquals(culture); + return false; } + + return repo.GetDefaultIsoCode().InvariantEquals(culture); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs index bfecc66765..a00c35de6d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/LogViewerQueryRepository.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -12,124 +10,116 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository { - internal class LogViewerQueryRepository : EntityRepositoryBase, ILogViewerQueryRepository + public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public LogViewerQueryRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() + public ILogViewerQuery? GetByName(string name) => + + // use the underlying GetAll which will force cache all log queries + GetMany().FirstOrDefault(x => x.Name == name); + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql? sql = GetBaseQuery(false).Where($"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id > 0"); + if (ids?.Any() ?? false) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + sql.Where($"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id in (@ids)", new { ids }); } - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id > 0"); - if (ids?.Any() ?? false) - { - sql.Where($"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id in (@ids)", new { ids = ids }); - } + return Database.Fetch(sql).Select(ConvertFromDto); + } - return Database.Fetch(sql).Select(ConvertFromDto); + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method"); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + sql = isCount ? sql.SelectCount() : sql.Select(); + sql = sql.From(); + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.LogViewerQuery}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { $"DELETE FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(ILogViewerQuery entity) + { + var exists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name", + new { name = entity.Name }); + if (exists > 0) + { + throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); } - protected override IEnumerable PerformGetByQuery(IQuery query) + entity.AddingEntity(); + + var factory = new LogViewerQueryModelFactory(); + LogViewerQueryDto dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + } + + protected override void PersistUpdatedItem(ILogViewerQuery entity) + { + entity.UpdatingEntity(); + + var exists = Database.ExecuteScalar( + $"SELECT COUNT(*) FROM {Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name AND id <> @id", + new { name = entity.Name, id = entity.Id }); + + // ensure there is no other log query with the same name on another entity + if (exists > 0) { - throw new NotSupportedException("This repository does not support this method"); + throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); } - protected override Sql GetBaseQuery(bool isCount) + var factory = new LogViewerQueryModelFactory(); + LogViewerQueryDto dto = factory.BuildDto(entity); + + Database.Update(dto); + } + + protected override ILogViewerQuery? PerformGet(int id) => + + // use the underlying GetAll which will force cache all log queries + GetMany().FirstOrDefault(x => x.Id == id); + + private ILogViewerQuery ConvertFromDto(LogViewerQueryDto dto) + { + var factory = new LogViewerQueryModelFactory(); + ILogViewerQuery entity = factory.BuildEntity(dto); + return entity; + } + + internal class LogViewerQueryModelFactory + { + public ILogViewerQuery BuildEntity(LogViewerQueryDto dto) { - var sql = Sql(); - sql = isCount ? sql.SelectCount() : sql.Select(); - sql = sql.From(); - return sql; + var logViewerQuery = new LogViewerQuery(dto.Name, dto.Query) { Id = dto.Id }; + return logViewerQuery; } - protected override string GetBaseWhereClause() + public LogViewerQueryDto BuildDto(ILogViewerQuery entity) { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - $"DELETE FROM {Cms.Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(ILogViewerQuery entity) - { - var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name", - new { name = entity.Name }); - if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); - - entity.AddingEntity(); - - var factory = new LogViewerQueryModelFactory(); - var dto = factory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - } - - protected override void PersistUpdatedItem(ILogViewerQuery entity) - { - entity.UpdatingEntity(); - - var exists = Database.ExecuteScalar($"SELECT COUNT(*) FROM {Core.Constants.DatabaseSchema.Tables.LogViewerQuery} WHERE name = @name AND id <> @id", - new { name = entity.Name, id = entity.Id }); - //ensure there is no other log query with the same name on another entity - if (exists > 0) throw new DuplicateNameException($"The log query name '{entity.Name}' is already used"); - - - var factory = new LogViewerQueryModelFactory(); - var dto = factory.BuildDto(entity); - - Database.Update(dto); - } - - private ILogViewerQuery ConvertFromDto(LogViewerQueryDto dto) - { - var factory = new LogViewerQueryModelFactory(); - var entity = factory.BuildEntity(dto); - return entity; - } - - internal class LogViewerQueryModelFactory - { - - public ILogViewerQuery BuildEntity(LogViewerQueryDto dto) - { - var logViewerQuery = new LogViewerQuery(dto.Name, dto.Query) - { - Id = dto.Id, - }; - return logViewerQuery; - } - - public LogViewerQueryDto BuildDto(ILogViewerQuery entity) - { - var dto = new LogViewerQueryDto { Name = entity.Name, Query = entity.Query, Id = entity.Id }; - return dto; - } - } - - protected override ILogViewerQuery? PerformGet(int id) - { - //use the underlying GetAll which will force cache all log queries - return GetMany()?.FirstOrDefault(x => x.Id == id); - } - - public ILogViewerQuery? GetByName(string name) - { - //use the underlying GetAll which will force cache all log queries - return GetMany()?.FirstOrDefault(x => x.Name == name); + var dto = new LogViewerQueryDto { Name = entity.Name, Query = entity.Query, Id = entity.Id }; + return dto; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs index a918590a0c..323238084f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MacroRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -16,250 +13,254 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MacroRepository : EntityRepositoryBase, IMacroWithAliasRepository { - internal class MacroRepository : EntityRepositoryBase, IMacroWithAliasRepository + private readonly IRepositoryCachePolicy _macroByAliasCachePolicy; + private readonly IShortStringHelper _shortStringHelper; + + public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger) { - private readonly IShortStringHelper _shortStringHelper; - private readonly IRepositoryCachePolicy _macroByAliasCachePolicy; + _shortStringHelper = shortStringHelper; + _macroByAliasCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + } - public MacroRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger) + public IMacro? Get(Guid id) + { + Sql sql = GetBaseQuery().Where(x => x.UniqueId == id); + return GetBySql(sql); + } + + public IEnumerable GetMany(params Guid[]? ids) => + ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); + + public bool Exists(Guid id) => Get(id) != null; + + public IMacro? GetByAlias(string alias) => + _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias); + + public IEnumerable GetAllByAlias(string[] aliases) + { + if (aliases.Any() is false) { - _shortStringHelper = shortStringHelper; - _macroByAliasCachePolicy = new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + return base.GetMany(); } - protected override IMacro? PerformGet(int id) + return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias); + } + + protected override IMacro? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + return GetBySql(sql); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) => + ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .Select(x => Get(x.Id)!); + } + + private IMacro? GetBySql(Sql sql) + { + MacroDto? macroDto = Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .FirstOrDefault(); + + if (macroDto == null) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id }); - return GetBySql(sql); + return null; } - public IMacro? Get(Guid id) + IMacro entity = MacroFactory.BuildEntity(_shortStringHelper, macroDto); + + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); + + return entity; + } + + private IMacro? PerformGetByAlias(string? alias) + { + IQuery query = Query().Where(x => x.Alias.Equals(alias)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByAlias(params string[]? aliases) + { + if (aliases is null || aliases.Any() is false) { - var sql = GetBaseQuery().Where(x => x.UniqueId == id); - return GetBySql(sql); + return base.GetMany(); } - private IMacro? GetBySql(Sql sql) + IQuery query = Query().Where(x => aliases.Contains(x.Alias)); + return PerformGetByQuery(query); + } + + private IEnumerable GetAllNoIds() + { + Sql sql = GetBaseQuery(false) + + // must be sorted this way for the relator to work + .OrderBy(x => x.Id); + + return Database + .FetchOneToMany(x => x.MacroPropertyDtos, sql) + .Transform(ConvertFromDtos) + .ToArray(); // do it now and once + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + foreach (IMacro entity in dtos.Select(x => MacroFactory.BuildEntity(_shortStringHelper, x))) { - var macroDto = Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .FirstOrDefault(); - - if (macroDto == null) - return null; - - var entity = MacroFactory.BuildEntity(_shortStringHelper, macroDto); - // reset dirty initial properties (U4-1946) ((BeingDirtyBase)entity).ResetDirtyProperties(false); - return entity; - } - - public IEnumerable GetMany(params Guid[]? ids) - { - return ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); - } - - public bool Exists(Guid id) - { - return Get(id) != null; - } - - public IMacro? GetByAlias(string alias) - { - return _macroByAliasCachePolicy.Get(alias, PerformGetByAlias, PerformGetAllByAlias); - } - - public IEnumerable GetAllByAlias(string[] aliases) - { - if (aliases.Any() is false) - { - return base.GetMany(); - } - - return _macroByAliasCachePolicy.GetAll(aliases, PerformGetAllByAlias); - } - - private IMacro? PerformGetByAlias(string? alias) - { - var query = Query().Where(x => x.Alias.Equals(alias)); - return PerformGetByQuery(query)?.FirstOrDefault(); - } - - private IEnumerable PerformGetAllByAlias(params string[]? aliases) - { - if (aliases is null || aliases.Any() is false) - { - return base.GetMany(); - } - - var query = Query().Where(x => aliases.Contains(x.Alias)); - return PerformGetByQuery(query); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - return ids?.Length > 0 ? ids.Select(Get).WhereNotNull() : GetAllNoIds(); - } - - private IEnumerable GetAllNoIds() - { - var sql = GetBaseQuery(false) - //must be sorted this way for the relator to work - .OrderBy(x => x.Id); - - return Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .Transform(ConvertFromDtos) - .ToArray(); // do it now and once - } - - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - - foreach (var entity in dtos.Select(x => MacroFactory.BuildEntity(_shortStringHelper, x))) - { - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase)entity).ResetDirtyProperties(false); - - yield return entity; - } - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database - .FetchOneToMany(x => x.MacroPropertyDtos, sql) - .Select(x => Get(x.Id)!); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return isCount ? Sql().SelectCount().From() : GetBaseQuery(); - } - - private Sql GetBaseQuery() - { - return Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.Macro); - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Macro}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsMacroProperty WHERE macro = @id", - "DELETE FROM cmsMacro WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IMacro entity) - { - entity.AddingEntity(); - - var dto = MacroFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - if (dto.MacroPropertyDtos is not null) - { - foreach (var propDto in dto.MacroPropertyDtos) - { - //need to set the id explicitly here - propDto.Macro = id; - var propId = Convert.ToInt32(Database.Insert(propDto)); - entity.Properties[propDto.Alias].Id = propId; - } - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMacro entity) - { - entity.UpdatingEntity(); - var dto = MacroFactory.BuildDto(entity); - - Database.Update(dto); - - //update the properties if they've changed - var macro = (Macro)entity; - if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) - { - var ids = dto.MacroPropertyDtos?.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); - if (ids?.Length > 0) - Database.Delete("WHERE macro=@macro AND id NOT IN (@ids)", new { macro = dto.Id, ids }); - else - Database.Delete("WHERE macro=@macro", new { macro = dto.Id }); - - // detect new aliases, replace with temp aliases - // this ensures that we don't have collisions, ever - var aliases = new Dictionary(); - if (dto.MacroPropertyDtos is null) - { - return; - } - foreach (var propDto in dto.MacroPropertyDtos) - { - var prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); - if (prop == null) throw new Exception("oops: property."); - if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) - { - var tempAlias = Guid.NewGuid().ToString("N").Substring(0, 8); - aliases[tempAlias] = propDto.Alias; - propDto.Alias = tempAlias; - } - } - - // insert or update existing properties, with temp aliases - foreach (var propDto in dto.MacroPropertyDtos) - { - if (propDto.Id == 0) - { - // insert - propDto.Id = Convert.ToInt32(Database.Insert(propDto)); - macro.Properties[aliases[propDto.Alias]].Id = propDto.Id; - } - else - { - // update - var property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); - if (property == null) throw new Exception("oops: property."); - if (property.IsDirty()) - Database.Update(propDto); - } - } - - // replace the temp aliases with the real ones - foreach (var propDto in dto.MacroPropertyDtos) - { - if (aliases.ContainsKey(propDto.Alias) == false) continue; - - propDto.Alias = aliases[propDto.Alias]; - Database.Update(propDto); - } - } - - entity.ResetDirtyProperties(); + yield return entity; } } + + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Macro}.id = @id"; + + private Sql GetBaseQuery() => + Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.Macro); + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM cmsMacroProperty WHERE macro = @id", "DELETE FROM cmsMacro WHERE id = @id", + }; + return list; + } + + protected override void PersistNewItem(IMacro entity) + { + entity.AddingEntity(); + + MacroDto dto = MacroFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + if (dto.MacroPropertyDtos is not null) + { + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + // need to set the id explicitly here + propDto.Macro = id; + var propId = Convert.ToInt32(Database.Insert(propDto)); + entity.Properties[propDto.Alias].Id = propId; + } + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMacro entity) + { + entity.UpdatingEntity(); + MacroDto dto = MacroFactory.BuildDto(entity); + + Database.Update(dto); + + // update the properties if they've changed + var macro = (Macro)entity; + if (macro.IsPropertyDirty("Properties") || macro.Properties.Values.Any(x => x.IsDirty())) + { + var ids = dto.MacroPropertyDtos?.Where(x => x.Id > 0).Select(x => x.Id).ToArray(); + if (ids?.Length > 0) + { + Database.Delete("WHERE macro=@macro AND id NOT IN (@ids)", new { macro = dto.Id, ids }); + } + else + { + Database.Delete("WHERE macro=@macro", new { macro = dto.Id }); + } + + // detect new aliases, replace with temp aliases + // this ensures that we don't have collisions, ever + var aliases = new Dictionary(); + if (dto.MacroPropertyDtos is null) + { + return; + } + + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + IMacroProperty? prop = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); + if (prop == null) + { + throw new Exception("oops: property."); + } + + if (propDto.Id == 0 || prop.IsPropertyDirty("Alias")) + { + var tempAlias = Guid.NewGuid().ToString("N")[..8]; + aliases[tempAlias] = propDto.Alias; + propDto.Alias = tempAlias; + } + } + + // insert or update existing properties, with temp aliases + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + if (propDto.Id == 0) + { + // insert + propDto.Id = Convert.ToInt32(Database.Insert(propDto)); + macro.Properties[aliases[propDto.Alias]].Id = propDto.Id; + } + else + { + // update + IMacroProperty? property = macro.Properties.Values.FirstOrDefault(x => x.Id == propDto.Id); + if (property == null) + { + throw new Exception("oops: property."); + } + + if (property.IsDirty()) + { + Database.Update(propDto); + } + } + } + + // replace the temp aliases with the real ones + foreach (MacroPropertyDto propDto in dto.MacroPropertyDtos) + { + if (aliases.ContainsKey(propDto.Alias) == false) + { + continue; + } + + propDto.Alias = aliases[propDto.Alias]; + Database.Update(propDto); + } + } + + entity.ResetDirtyProperties(); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs index c763b3481a..73cb423837 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaRepository.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -21,564 +19,557 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +public class MediaRepository : ContentRepositoryBase, IMediaRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - public class MediaRepository : ContentRepositoryBase, IMediaRepository + private readonly AppCaches _cache; + private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; + private readonly IMediaTypeRepository _mediaTypeRepository; + private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; + private readonly IJsonSerializer _serializer; + private readonly ITagRepository _tagRepository; + + public MediaRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + ILoggerFactory loggerFactory, + IMediaTypeRepository mediaTypeRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + PropertyEditorCollection propertyEditorCollection, + MediaUrlGeneratorCollection mediaUrlGenerators, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditorCollection, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly AppCaches _cache; - private readonly IMediaTypeRepository _mediaTypeRepository; - private readonly ITagRepository _tagRepository; - private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly IJsonSerializer _serializer; - private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; + _cache = cache; + _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _mediaUrlGenerators = mediaUrlGenerators; + _serializer = serializer; + _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, + loggerFactory.CreateLogger()); + } - public MediaRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - ILogger logger, - ILoggerFactory loggerFactory, - IMediaTypeRepository mediaTypeRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - PropertyEditorCollection propertyEditorCollection, - MediaUrlGeneratorCollection mediaUrlGenerators, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories, dataTypeService, eventAggregator) + protected override MediaRepository This => this; + + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, Ordering? ordering) + { + Sql? filterSql = null; + + if (filter != null) { - _cache = cache; - _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _mediaUrlGenerators = mediaUrlGenerators; - _serializer = serializer; - _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, loggerFactory.CreateLogger()); - } - - protected override MediaRepository This => this; - - - #region Repository Base - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Media; - - protected override IMedia? PerformGet(int id) - { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); - - return MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(QueryType.Many); - - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - sql - .OrderBy(x => x.Level) - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(sql)); - } - - protected override Sql GetBaseQuery(QueryType queryType) - { - return GetBaseQuery(queryType); - } - - protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, bool joinMediaVersion = false) - { - var sql = SqlContext.Sql(); - - switch (queryType) + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - sql = sql.Select(r => + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } + + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Core.Models.Media[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) + { + ContentDto dto = dtos[i]; + + if (withCache) + { + // if the cache contains the (proper version of the) item, use it + IMedia? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) + { + content[i] = (Core.Models.Media)cached; + continue; + } + } + + // else, need to build it + + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMediaType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _mediaTypeRepository.Get(contentTypeId); + } + + Core.Models.Media c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); + } + + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); + + // assign properties + foreach (TempContent temp in temps) + { + if (temp.Content is not null) + { + temp.Content.Properties = properties[temp.VersionId]; + + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); + } + } + + return content; + } + + private IMedia MapDtoToContent(ContentDto dto) + { + IMediaType? contentType = _mediaTypeRepository.Get(dto.ContentTypeId); + Core.Models.Media media = ContentBaseFactory.BuildEntity(dto, contentType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.NodeId, versionId, 0, contentType); + IDictionary properties = + GetPropertyCollections(new List> { temp }); + media.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + media.ResetDirtyProperties(false); + return media; + } + + #region Repository Base + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Media; + + protected override IMedia? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); + + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + sql + .OrderBy(x => x.Level) + .OrderBy(x => x.SortOrder); + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType); + + protected virtual Sql GetBaseQuery(QueryType queryType, bool current = true, bool joinMediaVersion = false) + { + Sql sql = SqlContext.Sql(); + + switch (queryType) + { + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + sql = sql.Select(r => r.Select(x => x.NodeDto) - .Select(x => x.ContentVersionDto)) + .Select(x => x.ContentVersionDto)) - // ContentRepositoryBase expects a variantName field to order by name - // for now, just return the plain invariant node name - .AndSelect(x => Alias(x.Text, "variantName")); - break; - } - - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId); - - if (joinMediaVersion) - sql.InnerJoin().On((left, right) => left.Id == right.Id); - - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); - - if (current) - sql.Where(x => x.Current); // always get the current version - - return sql; + // ContentRepositoryBase expects a variantName field to order by name + // for now, just return the plain invariant node name + .AndSelect(x => Alias(x.Text, "variantName")); + break; } - protected override Sql GetBaseQuery(bool isCount) + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId); + + if (joinMediaVersion) { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + sql.InnerJoin() + .On((left, right) => left.Id == right.Id); } - // ah maybe not, that what's used for eg Exists in base repo - protected override string GetBaseWhereClause() + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + + if (current) { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; + sql.Where(x => x.Current); // always get the current version } - protected override IEnumerable GetDeleteClauses() + return sql; + } + + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + + // ah maybe not, that what's used for eg Exists in base repo + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - var list = new List - { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup + " SET startContentId = NULL WHERE startContentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" - }; - return list; + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserStartNode + " WHERE startNode = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.UserGroup + + " SET startContentId = NULL WHERE startContentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE parentId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Relation + " WHERE childId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Document + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.MediaVersion + " WHERE id IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE versionId IN (SELECT id FROM " + + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id", + }; + return list; + } + + #endregion + + #region Versions + + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + return MapDtosToContent(Database.Fetch(sql), true); + } + + public override IMedia? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.Id == versionId); + + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } + + public IMedia? GetMediaByPath(string mediaPath) + { + var umbracoFileValue = mediaPath; + const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; + var isResized = Regex.IsMatch(mediaPath, pattern); + + // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" URL. + if (isResized) + { + var underscoreIndex = mediaPath.LastIndexOf('_'); + var dotIndex = mediaPath.LastIndexOf('.'); + umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); } - #endregion + Sql sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) + .Where(x => x.Path == umbracoFileValue) + .SelectTop(1); - #region Versions + ContentDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } - public override IEnumerable GetAllVersions(int nodeId) + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @versionId", new { versionId }); + Database.Delete("WHERE versionId = @versionId", new { versionId }); + } + + #endregion + + #region Persist + + protected override void PersistNewItem(IMedia entity) + { + entity.AddingEntity(); + + // ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name)!; + + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // create the dto + MediaDto dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); + + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; + + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + + // persist the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; + + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) { - var sql = GetBaseQuery(QueryType.Many, current: false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - return MapDtosToContent(Database.Fetch(sql), true); + nodeDto.NodeId = id; + } + else + { + Database.Insert(nodeDto); } - public override IMedia? GetVersion(int versionId) + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + // persist the content dto + ContentDto contentDto = dto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + // persist the content version dto + // assumes a new version id and version date (modified date) has been set + ContentVersionDto contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = true; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the media version dto + MediaVersionDto mediaVersionDto = dto.MediaVersionDto; + mediaVersionDto.Id = entity.VersionId; + Database.Insert(mediaVersionDto); + + // persist the property data + InsertPropertyValues(entity, 0, out _, out _); + + // set tags + SetEntityTags(entity, _tagRepository, _serializer); + + PersistRelations(entity); + + OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMedia entity) + { + // update + entity.UpdatingEntity(); + + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + + if (!isMoving) { - var sql = GetBaseQuery(QueryType.Single) - .Where(x => x.Id == versionId); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); - } - - public IMedia? GetMediaByPath(string mediaPath) - { - var umbracoFileValue = mediaPath; - const string pattern = ".*[_][0-9]+[x][0-9]+[.].*"; - var isResized = Regex.IsMatch(mediaPath, pattern); - - // If the image has been resized we strip the "_403x328" of the original "/media/1024/koala_403x328.jpg" URL. - if (isResized) - { - var underscoreIndex = mediaPath.LastIndexOf('_'); - var dotIndex = mediaPath.LastIndexOf('.'); - umbracoFileValue = string.Concat(mediaPath.Substring(0, underscoreIndex), mediaPath.Substring(dotIndex)); - } - - var sql = GetBaseQuery(QueryType.Single, joinMediaVersion: true) - .Where(x => x.Path == umbracoFileValue) - .SelectTop(1); - - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); - } - - protected override void PerformDeleteVersion(int id, int versionId) - { - Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE versionId = @versionId", new { versionId }); - } - - #endregion - - #region Persist - - protected override void PersistNewItem(IMedia entity) - { - entity.AddingEntity(); - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name)!; + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id)!; // ensure that strings don't contain characters that are invalid in xml // TODO: do we really want to keep doing this here? entity.SanitizeEntityPropertiesForXmlStorage(); - // create the dto - var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty(nameof(entity.ParentId))) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); - // derive path and level from parent - var parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } + } - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // create the dto + MediaDto dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); - // persist the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; + // update the node dto + NodeDto nodeDto = dto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - nodeDto.NodeId = id; - else - Database.Insert(nodeDto); + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - var contentDto = dto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - // assumes a new version id and version date (modified date) has been set - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; + // update the content & media version dtos + ContentVersionDto contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + MediaVersionDto mediaVersionDto = dto.MediaVersionDto; contentVersionDto.Current = true; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; + Database.Update(contentVersionDto); + Database.Update(mediaVersionDto); - // persist the media version dto - var mediaVersionDto = dto.MediaVersionDto; - mediaVersionDto.Id = entity.VersionId; - Database.Insert(mediaVersionDto); + // replace the property data + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - // persist the property data - InsertPropertyValues(entity, 0, out _, out _); - - // set tags SetEntityTags(entity, _tagRepository, _serializer); PersistRelations(entity); - - OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); } - protected override void PersistUpdatedItem(IMedia entity) + OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); + } + + protected override void PersistDeletedItem(IMedia entity) + { + // Raise event first else potential FK issues + OnUowRemovingEntity(entity); + base.PersistDeletedItem(entity); + } + + #endregion + + #region Recycle Bin + + public override int RecycleBinId => Constants.System.RecycleBinMedia; + + public bool RecycleBinSmells() + { + IAppPolicyCache cache = _cache.RuntimeCache; + var cacheKey = CacheKeys.MediaRecycleBinCacheKey; + + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } + + #endregion + + #region Read Repository implementation for Guid keys + + public IMedia? Get(Guid id) => _mediaByGuidReadRepository.Get(id); + + IEnumerable IReadRepository.GetMany(params Guid[]? ids) => + _mediaByGuidReadRepository.GetMany(ids); + + public bool Exists(Guid id) => _mediaByGuidReadRepository.Exists(id); + + // A reading repository purely for looking up by GUID + // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this + private class MediaByGuidReadRepository : EntityRepositoryBase + { + private readonly MediaRepository _outerRepo; + + public MediaByGuidReadRepository(MediaRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _outerRepo = outerRepo; + + protected override IMedia? PerformGet(Guid id) { - // update - entity.UpdatingEntity(); + Sql sql = _outerRepo.GetBaseQuery(QueryType.Single) + .Where(x => x.UniqueId == id); - // Check if this entity is being moved as a descendant as part of a bulk moving operations. - // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. - // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. - var isMoving = entity.IsMoving(); + ContentDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - if (!isMoving) + if (dto == null) { - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id)!; - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty(nameof(entity.ParentId))) - { - var parent = GetParentNodeDto(entity.ParentId); - - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } + return null; } - // create the dto - var dto = ContentBaseFactory.BuildDto(_mediaUrlGenerators, entity); - - // update the node dto - var nodeDto = dto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - if (!isMoving) - { - // update the content dto - Database.Update(dto.ContentDto); - - // update the content & media version dtos - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - var mediaVersionDto = dto.MediaVersionDto; - contentVersionDto.Current = true; - Database.Update(contentVersionDto); - Database.Update(mediaVersionDto); - - // replace the property data - ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - - SetEntityTags(entity, _tagRepository, _serializer); - - PersistRelations(entity); - } - - OnUowRefreshedEntity(new MediaRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); - } - - protected override void PersistDeletedItem(IMedia entity) - { - // Raise event first else potential FK issues - OnUowRemovingEntity(entity); - base.PersistDeletedItem(entity); - } - - #endregion - - #region Recycle Bin - - public override int RecycleBinId => Cms.Core.Constants.System.RecycleBinMedia; - - public bool RecycleBinSmells() - { - var cache = _cache.RuntimeCache; - var cacheKey = CacheKeys.MediaRecycleBinCacheKey; - - // always cache either true or false - return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); - } - - #endregion - - #region Read Repository implementation for Guid keys - - public IMedia? Get(Guid id) - { - return _mediaByGuidReadRepository.Get(id); - } - - IEnumerable IReadRepository.GetMany(params Guid[]? ids) - { - return _mediaByGuidReadRepository.GetMany(ids); - } - - public bool Exists(Guid id) - { - return _mediaByGuidReadRepository.Exists(id); - } - - - // A reading repository purely for looking up by GUID - // TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - // This sub-repository pattern is super old and totally unecessary anymore, caching can be handled in much nicer ways without this - private class MediaByGuidReadRepository : EntityRepositoryBase - { - private readonly MediaRepository _outerRepo; - - public MediaByGuidReadRepository(MediaRepository outerRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _outerRepo = outerRepo; - } - - protected override IMedia? PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Single) - .Where(x => x.UniqueId == id); - - var dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = _outerRepo.MapDtoToContent(dto); - - return content; - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = _outerRepo.GetBaseQuery(QueryType.Many); - if (ids?.Length > 0) - sql.WhereIn(x => x.UniqueId, ids); - - return _outerRepo.MapDtosToContent(Database.Fetch(sql)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistNewItem(IMedia entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override void PersistUpdatedItem(IMedia entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } - } - - #endregion - - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, Ordering? ordering) - { - Sql? filterSql = null; - - if (filter != null) - { - filterSql = Sql(); - foreach (var clause in filter.GetWhereClauses()) - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Core.Models.Media[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - var dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Core.Models.Media)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out IMediaType? contentType) == false) - contentTypes[contentTypeId] = contentType = _mediaTypeRepository.Get(contentTypeId); - - var c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } - - // load all properties for all documents from database in 1 query - indexed by version id - var properties = GetPropertyCollections(temps); - - // assign properties - foreach (var temp in temps) - { - if (temp.Content is not null) - { - temp.Content.Properties = properties[temp.VersionId]; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } - } + IMedia content = _outerRepo.MapDtoToContent(dto); return content; } - private IMedia MapDtoToContent(ContentDto dto) + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - var contentType = _mediaTypeRepository.Get(dto.ContentTypeId); - var media = ContentBaseFactory.BuildEntity(dto, contentType); + Sql sql = _outerRepo.GetBaseQuery(QueryType.Many); + if (ids?.Length > 0) + { + sql.WhereIn(x => x.UniqueId, ids); + } - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.NodeId, versionId, 0, contentType); - var properties = GetPropertyCollections(new List> { temp }); - media.Properties = properties[versionId]; - - // reset dirty initial properties (U4-1946) - media.ResetDirtyProperties(false); - return media; + return _outerRepo.MapDtosToContent(Database.Fetch(sql)); } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistNewItem(IMedia entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override void PersistUpdatedItem(IMedia entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs index 069b49de2f..260cebef9f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeContainerRepository.cs @@ -1,14 +1,15 @@ using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository { - class MediaTypeContainerRepository : EntityContainerRepository, IMediaTypeContainerRepository + public MediaTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger, Constants.ObjectTypes.MediaTypeContainer) { - public MediaTypeContainerRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger, Cms.Core.Constants.ObjectTypes.MediaTypeContainer) - { } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs index 6742d2457d..51a4c36752 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,126 +11,124 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository + public MediaTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) { - public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) - { } + } - protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; + protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MediaType; + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job + protected override IMediaType? PerformGet(int id) + => GetMany().FirstOrDefault(x => x.Id == id); + + protected override IMediaType? PerformGet(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id); + + protected override bool PerformExists(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id) != null; + + protected override IMediaType? PerformGet(string alias) + => GetMany().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + IEnumerable all = GetMany(); + return ids?.Any() ?? false ? all.Where(x => ids.Contains(x.Key)) : all; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate(); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name).WhereNotNull() : Enumerable.Empty(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(x => x.NodeId); + + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } + + protected override void PersistNewItem(IMediaType entity) + { + entity.AddingEntity(); + + PersistNewBaseContentType(entity); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMediaType entity) + { + ValidateAlias(entity); + + // Updates Modified date + entity.UpdatingEntity(); + + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; } - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job + PersistUpdatedBaseContentType(entity); - protected override IMediaType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IMediaType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); - - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - - protected override IMediaType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - - protected override IEnumerable? GetAllWithFullCachePolicy() - { - return CommonRepository.GetAllTypes()?.OfType(); - } - - protected override IEnumerable? PerformGetAll(params Guid[]? ids) - { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var baseQuery = GetBaseQuery(false); - var translator = new SqlTranslator(baseQuery, query); - var sql = translator.Translate(); - var ids = Database.Fetch(sql).Distinct().ToArray(); - - return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name).WhereNotNull() : Enumerable.Empty(); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(x => x.NodeId); - - sql - .From() - .InnerJoin().On( left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; - } - - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MediaType; - - protected override void PersistNewItem(IMediaType entity) - { - entity.AddingEntity(); - - PersistNewBaseContentType(entity); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMediaType entity) - { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - PersistUpdatedBaseContentType(entity); - - entity.ResetDirtyProperties(); - } + entity.ResetDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs index f94ffe03a3..5dfd164f0d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberGroupRepository.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -15,307 +13,302 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class MemberGroupRepository : EntityRepositoryBase, IMemberGroupRepository { - internal class MemberGroupRepository : EntityRepositoryBase, IMemberGroupRepository + private readonly IEventMessagesFactory _eventMessagesFactory; + + public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + IEventMessagesFactory eventMessagesFactory) + : base(scopeAccessor, cache, logger) => + _eventMessagesFactory = eventMessagesFactory; + + protected Guid NodeObjectTypeId => Constants.ObjectTypes.MemberGroup; + + public IMemberGroup? Get(Guid uniqueId) { - private readonly IEventMessagesFactory _eventMessagesFactory; + Sql sql = GetBaseQuery(false); + sql.Where(x => x.UniqueId == uniqueId); - public MemberGroupRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IEventMessagesFactory eventMessagesFactory) - : base(scopeAccessor, cache, logger) => - _eventMessagesFactory = eventMessagesFactory; + NodeDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - protected override IMemberGroup? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id = id }); + return dto == null ? null : MemberGroupFactory.BuildEntity(dto); + } - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - return dto == null ? null : MemberGroupFactory.BuildEntity(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.NodeId, ids); - - return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); - } - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From() - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Cms.Core.Constants.DatabaseSchema.Tables.Node}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new[] - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", - "DELETE FROM umbracoRelation WHERE childId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", - "DELETE FROM umbracoNode WHERE id = @id" - }; - return list; - } - - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberGroup; - - protected override void PersistNewItem(IMemberGroup entity) - { - //Save to db - entity.AddingEntity(); - var group = (MemberGroup)entity; - var dto = MemberGroupFactory.BuildDto(group); - var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); - group.Id = dto.NodeId; //Set Id on entity to ensure an Id is set - - //Update with new correct path and id - dto.Path = string.Concat("-1,", dto.NodeId); - Database.Update(dto); - //assign to entity - group.Id = o; - group.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMemberGroup entity) - { - var dto = MemberGroupFactory.BuildDto(entity); - - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - public IMemberGroup? Get(Guid uniqueId) - { - var sql = GetBaseQuery(false); - sql.Where(x => x.UniqueId == uniqueId); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - return dto == null ? null : MemberGroupFactory.BuildEntity(dto); - } - - public IMemberGroup? GetByName(string? name) - { - return IsolatedCache.GetCacheItem( - typeof(IMemberGroup).FullName + "." + name, - () => - { - var qry = Query().Where(group => group.Name!.Equals(name)); - var result = Get(qry); - return result?.FirstOrDefault(); - }, - //cache for 5 mins since that is the default in the Runtime app cache - TimeSpan.FromMinutes(5), - //sliding is true - true); - } - - public IMemberGroup? CreateIfNotExists(string roleName) - { - var qry = Query().Where(group => group.Name!.Equals(roleName)); - var result = Get(qry); - - if (result?.Any() ?? false) - return null; - - var grp = new MemberGroup + public IMemberGroup? GetByName(string? name) => + IsolatedCache.GetCacheItem( + typeof(IMemberGroup).FullName + "." + name, + () => { - Name = roleName - }; - PersistNewItem(grp); + IQuery qry = Query().Where(group => group.Name!.Equals(name)); + IEnumerable result = Get(qry); + return result.FirstOrDefault(); + }, - var evtMsgs = _eventMessagesFactory.Get(); - if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(grp, evtMsgs))) - { - return null; - } + // cache for 5 mins since that is the default in the Runtime app cache + TimeSpan.FromMinutes(5), - AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(grp, evtMsgs)); + // sliding is true + true); - return grp; + public IMemberGroup? CreateIfNotExists(string roleName) + { + IQuery qry = Query().Where(group => group.Name!.Equals(roleName)); + IEnumerable result = Get(qry); + + if (result.Any()) + { + return null; } - public IEnumerable GetMemberGroupsForMember(int memberId) + var grp = new MemberGroup { Name = roleName }; + PersistNewItem(grp); + + EventMessages evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(grp, evtMsgs))) { - var sql = Sql() - .Select("umbracoNode.*") + return null; + } + + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(grp, evtMsgs)); + + return grp; + } + + public IEnumerable GetMemberGroupsForMember(int memberId) + { + Sql sql = Sql() + .Select("umbracoNode.*") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.MemberGroup) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.Member == memberId); + + return Database.Fetch(sql) + .DistinctBy(dto => dto.NodeId) + .Select(x => MemberGroupFactory.BuildEntity(x)); + } + + public IEnumerable GetMemberGroupsForMember(string? username) + { + Sql? sql = Sql() + .Select("un.*") + .From("umbracoNode AS un") + .InnerJoin("cmsMember2MemberGroup") + .On("cmsMember2MemberGroup.MemberGroup = un.id") + .InnerJoin("cmsMember") + .On("cmsMember.nodeId = cmsMember2MemberGroup.Member") + .Where("un.nodeObjectType=@objectType", new { objectType = NodeObjectTypeId }) + .Where("cmsMember.LoginName=@loginName", new { loginName = username }); + + return Database.Fetch(sql) + .DistinctBy(dto => dto.NodeId) + .Select(x => MemberGroupFactory.BuildEntity(x)); + } + + public void ReplaceRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames, true); + + public void AssignRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames); + + public void DissociateRoles(int[] memberIds, string[] roleNames) => DissociateRolesInternal(memberIds, roleNames); + + protected override IMemberGroup? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + + NodeDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + return dto == null ? null : MemberGroupFactory.BuildEntity(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + return Database.Fetch(sql).Select(x => MemberGroupFactory.BuildEntity(x)); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new[] + { + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoRelation WHERE parentId = @id", "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM cmsMember2MemberGroup WHERE MemberGroup = @id", "DELETE FROM umbracoNode WHERE id = @id", + }; + return list; + } + + protected override void PersistNewItem(IMemberGroup entity) + { + // Save to db + entity.AddingEntity(); + var group = (MemberGroup)entity; + NodeDto dto = MemberGroupFactory.BuildDto(group); + var o = Database.IsNew(dto) ? Convert.ToInt32(Database.Insert(dto)) : Database.Update(dto); + group.Id = dto.NodeId; // Set Id on entity to ensure an Id is set + + // Update with new correct path and id + dto.Path = string.Concat("-1,", dto.NodeId); + Database.Update(dto); + + // assign to entity + group.Id = o; + group.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMemberGroup entity) + { + NodeDto dto = MemberGroupFactory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + private void AssignRolesInternal(int[] memberIds, string[] roleNames, bool replace = false) + { + // ensure they're unique + memberIds = memberIds.Distinct().ToArray(); + + // create the missing roles first + Sql existingSql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId) + .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); + IEnumerable existingRoles = Database.Fetch(existingSql).Select(x => x.Text); + IEnumerable missingRoles = roleNames.Except(existingRoles, StringComparer.CurrentCultureIgnoreCase); + MemberGroup[] missingGroups = missingRoles.Select(x => new MemberGroup { Name = x }).ToArray(); + + EventMessages evtMsgs = _eventMessagesFactory.Get(); + if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(missingGroups, evtMsgs))) + { + return; + } + + foreach (MemberGroup m in missingGroups) + { + PersistNewItem(m); + } + + AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(missingGroups, evtMsgs)); + + // now go get all the dto's for roles with these role names + var rolesForNames = Database.Fetch(existingSql) + .ToDictionary(x => x.Text!, StringComparer.InvariantCultureIgnoreCase); + + AssignedRolesDto[] currentlyAssigned; + if (replace) + { + // delete all assigned groups first + Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds)", new { memberIds }); + + currentlyAssigned = Array.Empty(); + } + else + { + // get the groups that are currently assigned to any of these members + Sql assignedSql = Sql() + .Select( + $"{SqlSyntax.GetQuotedColumnName("text")},{SqlSyntax.GetQuotedColumnName("Member")},{SqlSyntax.GetQuotedColumnName("MemberGroup")}") .From() .InnerJoin() .On(dto => dto.NodeId, dto => dto.MemberGroup) .Where(x => x.NodeObjectType == NodeObjectTypeId) - .Where(x => x.Member == memberId); + .WhereIn(x => x.Member, memberIds); - return Database.Fetch(sql) - .DistinctBy(dto => dto.NodeId) - .Select(x => MemberGroupFactory.BuildEntity(x)); + currentlyAssigned = Database.Fetch(assignedSql).ToArray(); } - public IEnumerable GetMemberGroupsForMember(string? username) + // assign the roles for each member id + foreach (var memberId in memberIds) { - var sql = Sql() - .Select("un.*") - .From("umbracoNode AS un") - .InnerJoin("cmsMember2MemberGroup") - .On("cmsMember2MemberGroup.MemberGroup = un.id") - .InnerJoin("cmsMember") - .On("cmsMember.nodeId = cmsMember2MemberGroup.Member") - .Where("un.nodeObjectType=@objectType", new { objectType = NodeObjectTypeId }) - .Where("cmsMember.LoginName=@loginName", new { loginName = username }); + // find any roles for the current member that are currently assigned that + // exist in the roleNames list, then determine which ones are not currently assigned. + var mId = memberId; + AssignedRolesDto[] found = currentlyAssigned.Where(x => x.MemberId == mId).ToArray(); + IEnumerable assignedRoles = found + .Where(x => roleNames.Contains(x.RoleName, StringComparer.CurrentCultureIgnoreCase)) + .Select(x => x.RoleName); + IEnumerable nonAssignedRoles = + roleNames.Except(assignedRoles, StringComparer.CurrentCultureIgnoreCase); - return Database.Fetch(sql) - .DistinctBy(dto => dto.NodeId) - .Select(x => MemberGroupFactory.BuildEntity(x)); - } + IEnumerable dtos = nonAssignedRoles + .Select(x => new Member2MemberGroupDto { Member = mId, MemberGroup = rolesForNames[x!].NodeId }); - - - public void ReplaceRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames, true); - - public void AssignRoles(int[] memberIds, string[] roleNames) => AssignRolesInternal(memberIds, roleNames); - - private void AssignRolesInternal(int[] memberIds, string[] roleNames, bool replace = false) - { - //ensure they're unique - memberIds = memberIds.Distinct().ToArray(); - - //create the missing roles first - - Sql existingSql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId) - .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); - IEnumerable existingRoles = Database.Fetch(existingSql).Select(x => x.Text); - IEnumerable missingRoles = roleNames.Except(existingRoles, StringComparer.CurrentCultureIgnoreCase); - MemberGroup[] missingGroups = missingRoles.Select(x => new MemberGroup { Name = x }).ToArray(); - - var evtMsgs = _eventMessagesFactory.Get(); - if (AmbientScope.Notifications.PublishCancelable(new MemberGroupSavingNotification(missingGroups, evtMsgs))) - { - return; - } - - foreach (MemberGroup m in missingGroups) - { - PersistNewItem(m); - } - - AmbientScope.Notifications.Publish(new MemberGroupSavedNotification(missingGroups, evtMsgs)); - - //now go get all the dto's for roles with these role names - var rolesForNames = Database.Fetch(existingSql) - .ToDictionary(x => x.Text!, StringComparer.InvariantCultureIgnoreCase); - - AssignedRolesDto[] currentlyAssigned; - if (replace) - { - // delete all assigned groups first - Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds)", new { memberIds }); - - currentlyAssigned = Array.Empty(); - } - else - { - //get the groups that are currently assigned to any of these members - - Sql assignedSql = Sql() - .Select($"{SqlSyntax.GetQuotedColumnName("text")},{SqlSyntax.GetQuotedColumnName("Member")},{SqlSyntax.GetQuotedColumnName("MemberGroup")}") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.MemberGroup) - .Where(x => x.NodeObjectType == NodeObjectTypeId) - .WhereIn(x => x.Member, memberIds); - - currentlyAssigned = Database.Fetch(assignedSql).ToArray(); - } - - //assign the roles for each member id - - foreach (var memberId in memberIds) - { - //find any roles for the current member that are currently assigned that - //exist in the roleNames list, then determine which ones are not currently assigned. - var mId = memberId; - AssignedRolesDto[] found = currentlyAssigned.Where(x => x.MemberId == mId).ToArray(); - IEnumerable assignedRoles = found.Where(x => roleNames.Contains(x.RoleName, StringComparer.CurrentCultureIgnoreCase)).Select(x => x.RoleName); - IEnumerable nonAssignedRoles = roleNames.Except(assignedRoles, StringComparer.CurrentCultureIgnoreCase); - - IEnumerable dtos = nonAssignedRoles - .Select(x => new Member2MemberGroupDto - { - Member = mId, - MemberGroup = rolesForNames[x!].NodeId - }); - - Database.InsertBulk(dtos); - } - } - - public void DissociateRoles(int[] memberIds, string[] roleNames) - { - DissociateRolesInternal(memberIds, roleNames); - } - - private void DissociateRolesInternal(int[] memberIds, string[] roleNames) - { - var existingSql = Sql() - .SelectAll() - .From() - .Where(dto => dto.NodeObjectType == NodeObjectTypeId) - .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); - var existingRolesIds = Database.Fetch(existingSql).Select(x => x.NodeId).ToArray(); - - Database.Execute("DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds) AND MemberGroup IN (@memberGroups)", - new { /*memberIds =*/ memberIds, memberGroups = existingRolesIds }); - } - - private class AssignedRolesDto - { - [Column("text")] - public string? RoleName { get; set; } - - [Column("Member")] - public int MemberId { get; set; } - - [Column("MemberGroup")] - public int MemberGroupId { get; set; } + Database.InsertBulk(dtos); } } + + private void DissociateRolesInternal(int[] memberIds, string[] roleNames) + { + Sql? existingSql = Sql() + .SelectAll() + .From() + .Where(dto => dto.NodeObjectType == NodeObjectTypeId) + .Where("umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + " in (@names)", new { names = roleNames }); + var existingRolesIds = Database.Fetch(existingSql).Select(x => x.NodeId).ToArray(); + + Database.Execute( + "DELETE FROM cmsMember2MemberGroup WHERE Member IN (@memberIds) AND MemberGroup IN (@memberGroups)", + new + { + /*memberIds =*/ + memberIds, + memberGroups = existingRolesIds, + }); + } + + private class AssignedRolesDto + { + [Column("text")] + public string? RoleName { get; set; } + + [Column("Member")] + public int MemberId { get; set; } + + [Column("MemberGroup")] + public int MemberGroupId { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 9c41482436..817a26aac1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; @@ -15,7 +12,6 @@ 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.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -26,800 +22,794 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +public class MemberRepository : ContentRepositoryBase, IMemberRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - public class MemberRepository : ContentRepositoryBase, IMemberRepository + private readonly IJsonSerializer _jsonSerializer; + private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; + private readonly IMemberGroupRepository _memberGroupRepository; + private readonly IMemberTypeRepository _memberTypeRepository; + private readonly MemberPasswordConfigurationSettings _passwordConfiguration; + private readonly IPasswordHasher _passwordHasher; + private readonly ITagRepository _tagRepository; + private bool _passwordConfigInitialized; + private string? _passwordConfigJson; + + public MemberRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IMemberTypeRepository memberTypeRepository, + IMemberGroupRepository memberGroupRepository, + ITagRepository tagRepository, + ILanguageRepository languageRepository, + IRelationRepository relationRepository, + IRelationTypeRepository relationTypeRepository, + IPasswordHasher passwordHasher, + PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, + IDataTypeService dataTypeService, + IJsonSerializer serializer, + IEventAggregator eventAggregator, + IOptions passwordConfiguration) + : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, + propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) { - private readonly IJsonSerializer _jsonSerializer; - private readonly IRepositoryCachePolicy _memberByUsernameCachePolicy; - private readonly IMemberGroupRepository _memberGroupRepository; - private readonly IMemberTypeRepository _memberTypeRepository; - private readonly MemberPasswordConfigurationSettings _passwordConfiguration; - private readonly IPasswordHasher _passwordHasher; - private readonly ITagRepository _tagRepository; - private bool _passwordConfigInitialized; - private string? _passwordConfigJson; + _memberTypeRepository = + memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); + _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); + _passwordHasher = passwordHasher; + _jsonSerializer = serializer; + _memberGroupRepository = memberGroupRepository; + _passwordConfiguration = passwordConfiguration.Value; + _memberByUsernameCachePolicy = + new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); + } - public MemberRepository( - IScopeAccessor scopeAccessor, - AppCaches cache, - ILogger logger, - IMemberTypeRepository memberTypeRepository, - IMemberGroupRepository memberGroupRepository, - ITagRepository tagRepository, - ILanguageRepository languageRepository, - IRelationRepository relationRepository, - IRelationTypeRepository relationTypeRepository, - IPasswordHasher passwordHasher, - PropertyEditorCollection propertyEditors, - DataValueReferenceFactoryCollection dataValueReferenceFactories, - IDataTypeService dataTypeService, - IJsonSerializer serializer, - IEventAggregator eventAggregator, - IOptions passwordConfiguration) - : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, - propertyEditors, dataValueReferenceFactories, dataTypeService, eventAggregator) + /// + /// Returns a serialized dictionary of the password configuration that is stored against the member in the database + /// + private string? DefaultPasswordConfigJson + { + get { - _memberTypeRepository = - memberTypeRepository ?? throw new ArgumentNullException(nameof(memberTypeRepository)); - _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); - _passwordHasher = passwordHasher; - _jsonSerializer = serializer; - _memberGroupRepository = memberGroupRepository; - _passwordConfiguration = passwordConfiguration.Value; - _memberByUsernameCachePolicy = - new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions); - } - - /// - /// Returns a serialized dictionary of the password configuration that is stored against the member in the database - /// - private string? DefaultPasswordConfigJson - { - get + if (_passwordConfigInitialized) { - if (_passwordConfigInitialized) - { - return _passwordConfigJson; - } - - var passwordConfig = new PersistedPasswordSettings - { - HashAlgorithm = _passwordConfiguration.HashAlgorithmType - }; - - _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); - _passwordConfigInitialized = true; return _passwordConfigJson; } + + var passwordConfig = new PersistedPasswordSettings + { + HashAlgorithm = _passwordConfiguration.HashAlgorithmType + }; + + _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); + _passwordConfigInitialized = true; + return _passwordConfigJson; } + } - protected override MemberRepository This => this; + protected override MemberRepository This => this; - public override int RecycleBinId => throw new NotSupportedException(); + public override int RecycleBinId => throw new NotSupportedException(); - public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, - StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, + StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) + { + //get the group id + IQuery grpQry = Query().Where(group => group.Name!.Equals(roleName)); + IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); + if (memberGroup == null) { - //get the group id - IQuery grpQry = Query().Where(group => group.Name!.Equals(roleName)); - IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); - if (memberGroup == null) - { - return Enumerable.Empty(); - } - - // get the members by username - IQuery query = Query(); - switch (matchType) - { - case StringPropertyMatchType.Exact: - query.Where(member => member.Username.Equals(usernameToMatch)); - break; - case StringPropertyMatchType.Contains: - query.Where(member => member.Username.Contains(usernameToMatch)); - break; - case StringPropertyMatchType.StartsWith: - query.Where(member => member.Username.StartsWith(usernameToMatch)); - break; - case StringPropertyMatchType.EndsWith: - query.Where(member => member.Username.EndsWith(usernameToMatch)); - break; - case StringPropertyMatchType.Wildcard: - query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); - break; - default: - throw new ArgumentOutOfRangeException(nameof(matchType)); - } - - IMember[]? matchedMembers = Get(query)?.ToArray(); - - var membersInGroup = new List(); - - if (matchedMembers is null) - { - return membersInGroup; - } - //then we need to filter the matched members that are in the role - foreach (IEnumerable group in matchedMembers.Select(x => x.Id) - .InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql().SelectAll().From() - .Where(dto => dto.MemberGroup == memberGroup.Id) - .WhereIn(dto => dto.Member, group); - - var memberIdsInGroup = Database.Fetch(sql) - .Select(x => x.Member).ToArray(); - - membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); - } - - return membersInGroup; + return Enumerable.Empty(); } - /// - /// Get all members in a specific group - /// - /// - /// - public IEnumerable GetByMemberGroup(string groupName) + // get the members by username + IQuery query = Query(); + switch (matchType) { - IQuery grpQry = Query().Where(group => group.Name!.Equals(groupName)); - IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); - if (memberGroup == null) + case StringPropertyMatchType.Exact: + query.Where(member => member.Username.Equals(usernameToMatch)); + break; + case StringPropertyMatchType.Contains: + query.Where(member => member.Username.Contains(usernameToMatch)); + break; + case StringPropertyMatchType.StartsWith: + query.Where(member => member.Username.StartsWith(usernameToMatch)); + break; + case StringPropertyMatchType.EndsWith: + query.Where(member => member.Username.EndsWith(usernameToMatch)); + break; + case StringPropertyMatchType.Wildcard: + query.Where(member => member.Username.SqlWildcard(usernameToMatch, TextColumnType.NVarchar)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(matchType)); + } + + IMember[] matchedMembers = Get(query).ToArray(); + + var membersInGroup = new List(); + + // Then we need to filter the matched members that are in the role + foreach (IEnumerable group in matchedMembers.Select(x => x.Id) + .InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Sql sql = Sql().SelectAll().From() + .Where(dto => dto.MemberGroup == memberGroup.Id) + .WhereIn(dto => dto.Member, group); + + var memberIdsInGroup = Database.Fetch(sql) + .Select(x => x.Member).ToArray(); + + membersInGroup.AddRange(matchedMembers.Where(x => memberIdsInGroup.Contains(x.Id))); + } + + return membersInGroup; + } + + /// + /// Get all members in a specific group + /// + /// + /// + public IEnumerable GetByMemberGroup(string groupName) + { + IQuery grpQry = Query().Where(group => group.Name!.Equals(groupName)); + IMemberGroup? memberGroup = _memberGroupRepository.Get(grpQry)?.FirstOrDefault(); + if (memberGroup == null) + { + return Enumerable.Empty(); + } + + Sql subQuery = Sql().Select("Member").From() + .Where(dto => dto.MemberGroup == memberGroup.Id); + + Sql sql = GetBaseQuery(false) + // TODO: An inner join would be better, though I've read that the query optimizer will always turn a + // subquery with an IN clause into an inner join anyways. + .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) + .OrderByDescending(x => x.VersionDate) + .OrderBy(x => x.SortOrder); + + return MapDtosToContent(Database.Fetch(sql)); + } + + public bool Exists(string username) + { + Sql sql = Sql() + .SelectCount() + .From() + .Where(x => x.LoginName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public int GetCountByQuery(IQuery? query) + { + Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); + var translator = new SqlTranslator(sqlWithProps, query); + Sql sql = translator.Translate(); + + //get the COUNT base query + Sql fullSql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + + return Database.ExecuteScalar(fullSql); + } + + /// + [Obsolete( + "This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")] + public void SetLastLogin(string username, DateTime date) + { + } + + /// + /// Gets paged member results. + /// + public override IEnumerable GetPage(IQuery? query, + long pageIndex, int pageSize, out long totalRecords, + IQuery? filter, + Ordering? ordering) + { + Sql? filterSql = null; + + if (filter != null) + { + filterSql = Sql(); + foreach (Tuple clause in filter.GetWhereClauses()) { - return Enumerable.Empty(); + filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + return GetPage(query, pageIndex, pageSize, out totalRecords, + x => MapDtosToContent(x), + filterSql, + ordering); + } + + public IMember? GetByUsername(string? username) => + _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); + + public int[] GetMemberIds(string[] usernames) + { + Guid memberObjectType = Constants.ObjectTypes.Member; + + Sql memberSql = Sql() + .Select("umbracoNode.id") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(x => x.NodeObjectType == memberObjectType) + .Where("cmsMember.LoginName in (@usernames)", new + { + /*usernames =*/ + usernames + }); + return Database.Fetch(memberSql).ToArray(); + } + + protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) + { + if (ordering.OrderBy.InvariantEquals("email")) + { + return SqlSyntax.GetFieldName(x => x.Email); + } + + if (ordering.OrderBy.InvariantEquals("loginName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("userName")) + { + return SqlSyntax.GetFieldName(x => x.LoginName); + } + + if (ordering.OrderBy.InvariantEquals("updateDate")) + { + return SqlSyntax.GetFieldName(x => x.VersionDate); + } + + if (ordering.OrderBy.InvariantEquals("createDate")) + { + return SqlSyntax.GetFieldName(x => x.CreateDate); + } + + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + { + return SqlSyntax.GetFieldName(x => x.Alias); + } + + return base.ApplySystemOrdering(ref sql, ordering); + } + + private IEnumerable MapDtosToContent(List dtos, bool withCache = false) + { + var temps = new List>(); + var contentTypes = new Dictionary(); + var content = new Member[dtos.Count]; + + for (var i = 0; i < dtos.Count; i++) + { + MemberDto dto = dtos[i]; + + if (withCache) + { + // if the cache contains the (proper version of the) item, use it + IMember? cached = + IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) + { + content[i] = (Member)cached; + continue; + } } - Sql subQuery = Sql().Select("Member").From() - .Where(dto => dto.MemberGroup == memberGroup.Id); + // else, need to build it - Sql sql = GetBaseQuery(false) - // TODO: An inner join would be better, though I've read that the query optimizer will always turn a - // subquery with an IN clause into an inner join anyways. - .Append("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); + // get the content type - the repository is full cache *but* still deep-clones + // whatever comes out of it, so use our own local index here to avoid this + var contentTypeId = dto.ContentDto.ContentTypeId; + if (contentTypes.TryGetValue(contentTypeId, out IMemberType? contentType) == false) + { + contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); + } - return MapDtosToContent(Database.Fetch(sql)); + Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); + + // need properties + var versionId = dto.ContentVersionDto.Id; + temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); } - public bool Exists(string username) + // load all properties for all documents from database in 1 query - indexed by version id + IDictionary properties = GetPropertyCollections(temps); + + // assign properties + foreach (TempContent temp in temps) { - Sql sql = Sql() - .SelectCount() - .From() - .Where(x => x.LoginName == username); + if (temp.Content is not null) + { + temp.Content.Properties = properties[temp.VersionId]; - return Database.ExecuteScalar(sql) > 0; + // reset dirty initial properties (U4-1946) + temp.Content.ResetDirtyProperties(false); + } } - public int GetCountByQuery(IQuery? query) + return content; + } + + private IMember MapDtoToContent(MemberDto dto) + { + IMemberType? memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); + Member member = ContentBaseFactory.BuildEntity(dto, memberType); + + // get properties - indexed by version id + var versionId = dto.ContentVersionDto.Id; + var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); + IDictionary properties = + GetPropertyCollections(new List> {temp}); + member.Properties = properties[versionId]; + + // reset dirty initial properties (U4-1946) + member.ResetDirtyProperties(false); + return member; + } + + private IMember? PerformGetByUsername(string? username) + { + IQuery query = Query().Where(x => x.Username.Equals(username)); + return PerformGetByQuery(query).FirstOrDefault(); + } + + private IEnumerable PerformGetAllByUsername(params string[]? usernames) + { + IQuery query = Query().WhereIn(x => x.Username, usernames); + return PerformGetByQuery(query); + } + + #region Repository Base + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; + + protected override IMember? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.NodeId == id) + .SelectTop(1); + + MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null + ? null + : MapDtoToContent(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.NodeId, ids); + } + + return MapDtosToContent(Database.Fetch(sql)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql baseQuery = GetBaseQuery(false); + + // TODO: why is this different from content/media?! + // check if the query is based on properties or not + + IEnumerable> wheres = query.GetWhereClauses(); + //this is a pretty rudimentary check but will work, we just need to know if this query requires property + // level queries + if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) { Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); var translator = new SqlTranslator(sqlWithProps, query); Sql sql = translator.Translate(); - //get the COUNT base query - Sql fullSql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); + baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) + .OrderBy(x => x.SortOrder); - return Database.ExecuteScalar(fullSql); + return MapDtosToContent(Database.Fetch(baseQuery)); } - - /// - [Obsolete( - "This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")] - public void SetLastLogin(string username, DateTime date) + else { - - } - - /// - /// Gets paged member results. - /// - public override IEnumerable GetPage(IQuery? query, - long pageIndex, int pageSize, out long totalRecords, - IQuery? filter, - Ordering? ordering) - { - Sql? filterSql = null; - - if (filter != null) - { - filterSql = Sql(); - foreach (Tuple clause in filter.GetWhereClauses()) - { - filterSql = filterSql.Append($"AND ({clause.Item1})", clause.Item2); - } - } - - return GetPage(query, pageIndex, pageSize, out totalRecords, - x => MapDtosToContent(x), - filterSql, - ordering); - } - - public IMember? GetByUsername(string? username) => - _memberByUsernameCachePolicy.Get(username, PerformGetByUsername, PerformGetAllByUsername); - - public int[] GetMemberIds(string[] usernames) - { - Guid memberObjectType = Constants.ObjectTypes.Member; - - Sql memberSql = Sql() - .Select("umbracoNode.id") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(x => x.NodeObjectType == memberObjectType) - .Where("cmsMember.LoginName in (@usernames)", new - { - /*usernames =*/ - usernames - }); - return Database.Fetch(memberSql).ToArray(); - } - - protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering) - { - if (ordering.OrderBy.InvariantEquals("email")) - { - return SqlSyntax.GetFieldName(x => x.Email); - } - - if (ordering.OrderBy.InvariantEquals("loginName")) - { - return SqlSyntax.GetFieldName(x => x.LoginName); - } - - if (ordering.OrderBy.InvariantEquals("userName")) - { - return SqlSyntax.GetFieldName(x => x.LoginName); - } - - if (ordering.OrderBy.InvariantEquals("updateDate")) - { - return SqlSyntax.GetFieldName(x => x.VersionDate); - } - - if (ordering.OrderBy.InvariantEquals("createDate")) - { - return SqlSyntax.GetFieldName(x => x.CreateDate); - } - - if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) - { - return SqlSyntax.GetFieldName(x => x.Alias); - } - - return base.ApplySystemOrdering(ref sql, ordering); - } - - private IEnumerable MapDtosToContent(List dtos, bool withCache = false) - { - var temps = new List>(); - var contentTypes = new Dictionary(); - var content = new Member[dtos.Count]; - - for (var i = 0; i < dtos.Count; i++) - { - MemberDto dto = dtos[i]; - - if (withCache) - { - // if the cache contains the (proper version of the) item, use it - IMember? cached = - IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); - if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) - { - content[i] = (Member)cached; - continue; - } - } - - // else, need to build it - - // get the content type - the repository is full cache *but* still deep-clones - // whatever comes out of it, so use our own local index here to avoid this - var contentTypeId = dto.ContentDto.ContentTypeId; - if (contentTypes.TryGetValue(contentTypeId, out IMemberType? contentType) == false) - { - contentTypes[contentTypeId] = contentType = _memberTypeRepository.Get(contentTypeId); - } - - Member c = content[i] = ContentBaseFactory.BuildEntity(dto, contentType); - - // need properties - var versionId = dto.ContentVersionDto.Id; - temps.Add(new TempContent(dto.NodeId, versionId, 0, contentType, c)); - } - - // load all properties for all documents from database in 1 query - indexed by version id - IDictionary properties = GetPropertyCollections(temps); - - // assign properties - foreach (TempContent temp in temps) - { - if (temp.Content is not null) - { - temp.Content.Properties = properties[temp.VersionId]; - - // reset dirty initial properties (U4-1946) - temp.Content.ResetDirtyProperties(false); - } - } - - return content; - } - - private IMember MapDtoToContent(MemberDto dto) - { - IMemberType? memberType = _memberTypeRepository.Get(dto.ContentDto.ContentTypeId); - Member member = ContentBaseFactory.BuildEntity(dto, memberType); - - // get properties - indexed by version id - var versionId = dto.ContentVersionDto.Id; - var temp = new TempContent(dto.ContentDto.NodeId, versionId, 0, memberType); - IDictionary properties = - GetPropertyCollections(new List> { temp }); - member.Properties = properties[versionId]; - - // reset dirty initial properties (U4-1946) - member.ResetDirtyProperties(false); - return member; - } - - private IMember? PerformGetByUsername(string? username) - { - IQuery query = Query().Where(x => x.Username.Equals(username)); - return PerformGetByQuery(query).FirstOrDefault(); - } - - private IEnumerable PerformGetAllByUsername(params string[]? usernames) - { - IQuery query = Query().WhereIn(x => x.Username, usernames); - return PerformGetByQuery(query); - } - - #region Repository Base - - protected override Guid NodeObjectTypeId => Constants.ObjectTypes.Member; - - protected override IMember? PerformGet(int id) - { - Sql sql = GetBaseQuery(QueryType.Single) - .Where(x => x.NodeId == id) - .SelectTop(1); - - MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null - ? null - : MapDtoToContent(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - Sql sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.NodeId, ids); - } + var translator = new SqlTranslator(baseQuery, query); + Sql sql = translator.Translate() + .OrderBy(x => x.SortOrder); return MapDtosToContent(Database.Fetch(sql)); } + } - protected override IEnumerable PerformGetByQuery(IQuery query) + protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); + + protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + { + Sql sql = SqlContext.Sql(); + + switch (queryType) // TODO: pretend we still need these queries for now { - Sql baseQuery = GetBaseQuery(false); + case QueryType.Count: + sql = sql.SelectCount(); + break; + case QueryType.Ids: + sql = sql.Select(x => x.NodeId); + break; + case QueryType.Single: + case QueryType.Many: + sql = sql.Select(r => + r.Select(x => x.ContentVersionDto) + .Select(x => x.ContentDto, r1 => + r1.Select(x => x.NodeDto))) - // TODO: why is this different from content/media?! - // check if the query is based on properties or not - - IEnumerable> wheres = query.GetWhereClauses(); - //this is a pretty rudimentary check but will work, we just need to know if this query requires property - // level queries - if (wheres.Any(x => x.Item1.Contains("cmsPropertyType"))) - { - Sql sqlWithProps = GetNodeIdQueryWithPropertyData(); - var translator = new SqlTranslator(sqlWithProps, query); - Sql sql = translator.Translate(); - - baseQuery.Append("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments) - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(baseQuery)); - } - else - { - var translator = new SqlTranslator(baseQuery, query); - Sql sql = translator.Translate() - .OrderBy(x => x.SortOrder); - - return MapDtosToContent(Database.Fetch(sql)); - } + // ContentRepositoryBase expects a variantName field to order by name + // so get it here, though for members it's just the plain node name + .AndSelect(x => Alias(x.Text, "variantName")); + break; } - protected override Sql GetBaseQuery(QueryType queryType) => GetBaseQuery(queryType, true); + sql + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) - protected virtual Sql GetBaseQuery(QueryType queryType, bool current) + // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? + // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content + // types by default on the document and media repos so we can query by content type there too. + .InnerJoin() + .On(left => left.ContentTypeId, right => right.NodeId); + + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + + if (current) { - Sql sql = SqlContext.Sql(); - - switch (queryType) // TODO: pretend we still need these queries for now - { - case QueryType.Count: - sql = sql.SelectCount(); - break; - case QueryType.Ids: - sql = sql.Select(x => x.NodeId); - break; - case QueryType.Single: - case QueryType.Many: - sql = sql.Select(r => - r.Select(x => x.ContentVersionDto) - .Select(x => x.ContentDto, r1 => - r1.Select(x => x.NodeDto))) - - // ContentRepositoryBase expects a variantName field to order by name - // so get it here, though for members it's just the plain node name - .AndSelect(x => Alias(x.Text, "variantName")); - break; - } - - sql - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - - // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? - // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content - // types by default on the document and media repos so we can query by content type there too. - .InnerJoin() - .On(left => left.ContentTypeId, right => right.NodeId); - - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); - - if (current) - { - sql.Where(x => x.Current); // always get the current version - } - - return sql; + sql.Where(x => x.Current); // always get the current version } - // TODO: move that one up to Versionable! or better: kill it! - protected override Sql GetBaseQuery(bool isCount) => - GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); + return sql; + } - protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? - => - "umbracoNode.id = @id"; + // TODO: move that one up to Versionable! or better: kill it! + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Single); - // TODO: document/understand that one - protected Sql GetNodeIdQueryWithPropertyData() => - Sql() - .Select("DISTINCT(umbracoNode.id)") - .From() - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.ContentTypeId == right.NodeId) - .InnerJoin() - .On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .LeftJoin() - .On(left => left.ContentTypeId, right => right.ContentTypeId) - .LeftJoin() - .On(left => left.DataTypeId, right => right.NodeId) - .LeftJoin().On(x => x - .Where((left, right) => left.PropertyTypeId == right.Id) - .Where((left, right) => left.VersionId == right.Id)) - .Where(x => x.NodeObjectType == NodeObjectTypeId); + protected override string GetBaseWhereClause() // TODO: can we kill / refactor this? + => + "umbracoNode.id = @id"; - protected override IEnumerable GetDeleteClauses() + // TODO: document/understand that one + protected Sql GetNodeIdQueryWithPropertyData() => + Sql() + .Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.ContentTypeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .LeftJoin() + .On(left => left.ContentTypeId, right => right.ContentTypeId) + .LeftJoin() + .On(left => left.DataTypeId, right => right.NodeId) + .LeftJoin().On(x => x + .Where((left, right) => left.PropertyTypeId == right.Id) + .Where((left, right) => left.VersionId == right.Id)) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - var list = new List - { - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", - "DELETE FROM umbracoRelation WHERE parentId = @id", - "DELETE FROM umbracoRelation WHERE childId = @id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + - " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + - " WHERE nodeId = @id)", - "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", - "DELETE FROM cmsMember WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", - "DELETE FROM umbracoNode WHERE id = @id" - }; - return list; + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE nodeId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @id", + "DELETE FROM umbracoRelation WHERE parentId = @id", + "DELETE FROM umbracoRelation WHERE childId = @id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + + " WHERE versionId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.ContentVersion + + " WHERE nodeId = @id)", + "DELETE FROM cmsMember2MemberGroup WHERE Member = @id", + "DELETE FROM cmsMember WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersion + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Content + " WHERE nodeId = @id", + "DELETE FROM umbracoNode WHERE id = @id" + }; + return list; + } + + #endregion + + #region Versions + + public override IEnumerable GetAllVersions(int nodeId) + { + Sql sql = GetBaseQuery(QueryType.Many, false) + .Where(x => x.NodeId == nodeId) + .OrderByDescending(x => x.Current) + .AndByDescending(x => x.VersionDate); + + return MapDtosToContent(Database.Fetch(sql), true); + } + + public override IMember? GetVersion(int versionId) + { + Sql sql = GetBaseQuery(QueryType.Single) + .Where(x => x.Id == versionId); + + MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : MapDtoToContent(dto); + } + + protected override void PerformDeleteVersion(int id, int versionId) + { + Database.Delete("WHERE versionId = @VersionId", new {versionId}); + Database.Delete("WHERE versionId = @VersionId", new {versionId}); + } + + #endregion + + #region Persist + + protected override void PersistNewItem(IMember entity) + { + entity.AddingEntity(); + + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) + { + entity.SecurityStamp = Guid.NewGuid().ToString(); } - #endregion + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - #region Versions + // create the dto + MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - public override IEnumerable GetAllVersions(int nodeId) + // check if we have a user config else use the default + memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + + // derive path and level from parent + NodeDto parent = GetParentNodeDto(entity.ParentId); + var level = parent.Level + 1; + + // get sort order + var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); + + // persist the node dto + NodeDto nodeDto = memberDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = Convert.ToInt16(level); + nodeDto.SortOrder = sortOrder; + + // see if there's a reserved identifier for this unique id + // and then either update or insert the node dto + var id = GetReservedId(nodeDto.UniqueId); + if (id > 0) { - Sql sql = GetBaseQuery(QueryType.Many, false) - .Where(x => x.NodeId == nodeId) - .OrderByDescending(x => x.Current) - .AndByDescending(x => x.VersionDate); - - return MapDtosToContent(Database.Fetch(sql), true); - } - - public override IMember? GetVersion(int versionId) - { - Sql sql = GetBaseQuery(QueryType.Single) - .Where(x => x.Id == versionId); - - MemberDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : MapDtoToContent(dto); - } - - protected override void PerformDeleteVersion(int id, int versionId) - { - Database.Delete("WHERE versionId = @VersionId", new { versionId }); - Database.Delete("WHERE versionId = @VersionId", new { versionId }); - } - - #endregion - - #region Persist - - protected override void PersistNewItem(IMember entity) - { - entity.AddingEntity(); - - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // create the dto - MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - - // check if we have a user config else use the default - memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - - // derive path and level from parent - NodeDto parent = GetParentNodeDto(entity.ParentId); - var level = parent.Level + 1; - - // get sort order - var sortOrder = GetNewChildSortOrder(entity.ParentId, 0); - - // persist the node dto - NodeDto nodeDto = memberDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = Convert.ToInt16(level); - nodeDto.SortOrder = sortOrder; - - // see if there's a reserved identifier for this unique id - // and then either update or insert the node dto - var id = GetReservedId(nodeDto.UniqueId); - if (id > 0) - { - nodeDto.NodeId = id; - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } - else - { - Database.Insert(nodeDto); - - // update path, now that we have an id - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - } - - // update entity - entity.Id = nodeDto.NodeId; - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - // persist the content dto - ContentDto contentDto = memberDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - // persist the content version dto - // assumes a new version id and version date (modified date) has been set - ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - contentVersionDto.Current = true; - Database.Insert(contentVersionDto); - entity.VersionId = contentVersionDto.Id; - - // persist the member dto - memberDto.NodeId = nodeDto.NodeId; - - // if the password is empty, generate one with the special prefix - // this will hash the guid with a salt so should be nicely random - if (entity.RawPasswordValue.IsNullOrWhiteSpace()) - { - memberDto.Password = Constants.Security.EmptyPasswordPrefix + - _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); - entity.RawPasswordValue = memberDto.Password; - } - - Database.Insert(memberDto); - - // persist the property data - InsertPropertyValues(entity, 0, out _, out _); - - SetEntityTags(entity, _tagRepository, _jsonSerializer); - - PersistRelations(entity); - - OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMember entity) - { - // update - entity.UpdatingEntity(); - - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - NodeDto parent = GetParentNodeDto(entity.ParentId); - - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); - } - - // create the dto - MemberDto memberDto = ContentBaseFactory.BuildDto(entity); - - // update the node dto - NodeDto nodeDto = memberDto.ContentDto.NodeDto; + nodeDto.NodeId = id; + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); - // update the content dto - Database.Update(memberDto.ContentDto); + // update path, now that we have an id + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + } - // update the content version dto - Database.Update(memberDto.ContentVersionDto); + // update entity + entity.Id = nodeDto.NodeId; + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; - // update the member dto - // but only the changed columns, 'cos we cannot update password if empty - var changedCols = new List(); + // persist the content dto + ContentDto contentDto = memberDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); - if (entity.IsPropertyDirty("SecurityStamp")) + // persist the content version dto + // assumes a new version id and version date (modified date) has been set + ContentVersionDto contentVersionDto = memberDto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + contentVersionDto.Current = true; + Database.Insert(contentVersionDto); + entity.VersionId = contentVersionDto.Id; + + // persist the member dto + memberDto.NodeId = nodeDto.NodeId; + + // if the password is empty, generate one with the special prefix + // this will hash the guid with a salt so should be nicely random + if (entity.RawPasswordValue.IsNullOrWhiteSpace()) + { + memberDto.Password = Constants.Security.EmptyPasswordPrefix + + _passwordHasher.HashPassword(Guid.NewGuid().ToString("N")); + entity.RawPasswordValue = memberDto.Password; + } + + Database.Insert(memberDto); + + // persist the property data + InsertPropertyValues(entity, 0, out _, out _); + + SetEntityTags(entity, _tagRepository, _jsonSerializer); + + PersistRelations(entity); + + OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IMember entity) + { + // update + entity.UpdatingEntity(); + + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) + { + entity.SecurityStamp = Guid.NewGuid().ToString(); + } + + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + NodeDto parent = GetParentNodeDto(entity.ParentId); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } + + // create the dto + MemberDto memberDto = ContentBaseFactory.BuildDto(entity); + + // update the node dto + NodeDto nodeDto = memberDto.ContentDto.NodeDto; + Database.Update(nodeDto); + + // update the content dto + Database.Update(memberDto.ContentDto); + + // update the content version dto + Database.Update(memberDto.ContentVersionDto); + + // update the member dto + // but only the changed columns, 'cos we cannot update password if empty + var changedCols = new List(); + + if (entity.IsPropertyDirty("SecurityStamp")) + { + changedCols.Add("securityStampToken"); + } + + if (entity.IsPropertyDirty("Email")) + { + changedCols.Add("Email"); + } + + if (entity.IsPropertyDirty("Username")) + { + changedCols.Add("LoginName"); + } + + if (entity.IsPropertyDirty(nameof(entity.FailedPasswordAttempts))) + { + changedCols.Add(nameof(entity.FailedPasswordAttempts)); + } + + if (entity.IsPropertyDirty(nameof(entity.IsApproved))) + { + changedCols.Add(nameof(entity.IsApproved)); + } + + if (entity.IsPropertyDirty(nameof(entity.IsLockedOut))) + { + changedCols.Add(nameof(entity.IsLockedOut)); + } + + if (entity.IsPropertyDirty(nameof(entity.LastLockoutDate))) + { + changedCols.Add(nameof(entity.LastLockoutDate)); + } + + if (entity.IsPropertyDirty(nameof(entity.LastLoginDate))) + { + changedCols.Add(nameof(entity.LastLoginDate)); + } + + if (entity.IsPropertyDirty(nameof(entity.LastPasswordChangeDate))) + { + changedCols.Add(nameof(entity.LastPasswordChangeDate)); + } + + // this can occur from an upgrade + if (memberDto.PasswordConfig.IsNullOrWhiteSpace()) + { + memberDto.PasswordConfig = DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); + } + else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson) + { + changedCols.Add("passwordConfig"); + } + + // do NOT update the password if it has not changed or if it is null or empty + if (entity.IsPropertyDirty("RawPasswordValue") && !string.IsNullOrWhiteSpace(entity.RawPasswordValue)) + { + changedCols.Add("Password"); + + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { + memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); changedCols.Add("securityStampToken"); } - if (entity.IsPropertyDirty("Email")) - { - changedCols.Add("Email"); - } - - if (entity.IsPropertyDirty("Username")) - { - changedCols.Add("LoginName"); - } - - if (entity.IsPropertyDirty(nameof(entity.FailedPasswordAttempts))) - { - changedCols.Add(nameof(entity.FailedPasswordAttempts)); - } - - if (entity.IsPropertyDirty(nameof(entity.IsApproved))) - { - changedCols.Add(nameof(entity.IsApproved)); - } - - if (entity.IsPropertyDirty(nameof(entity.IsLockedOut))) - { - changedCols.Add(nameof(entity.IsLockedOut)); - } - - if (entity.IsPropertyDirty(nameof(entity.LastLockoutDate))) - { - changedCols.Add(nameof(entity.LastLockoutDate)); - } - - if (entity.IsPropertyDirty(nameof(entity.LastLoginDate))) - { - changedCols.Add(nameof(entity.LastLoginDate)); - } - - if (entity.IsPropertyDirty(nameof(entity.LastPasswordChangeDate))) - { - changedCols.Add(nameof(entity.LastPasswordChangeDate)); - } - - // this can occur from an upgrade - if (memberDto.PasswordConfig.IsNullOrWhiteSpace()) - { - memberDto.PasswordConfig = DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } - else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson) - { - changedCols.Add("passwordConfig"); - } - - // do NOT update the password if it has not changed or if it is null or empty - if (entity.IsPropertyDirty("RawPasswordValue") && !string.IsNullOrWhiteSpace(entity.RawPasswordValue)) - { - changedCols.Add("Password"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - - // check if we have a user config else use the default - memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } - - // If userlogin or the email has changed then need to reset security stamp - if (changedCols.Contains("Email") || changedCols.Contains("LoginName")) - { - memberDto.EmailConfirmedDate = null; - changedCols.Add("emailConfirmedDate"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - } - - if (changedCols.Count > 0) - { - Database.Update(memberDto, changedCols); - } - - ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); - - SetEntityTags(entity, _tagRepository, _jsonSerializer); - - PersistRelations(entity); - - OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); - - entity.ResetDirtyProperties(); + // check if we have a user config else use the default + memberDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); } - #endregion + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("Email") || changedCols.Contains("LoginName")) + { + memberDto.EmailConfirmedDate = null; + changedCols.Add("emailConfirmedDate"); + + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) + { + memberDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); + } + } + + if (changedCols.Count > 0) + { + Database.Update(memberDto, changedCols); + } + + ReplacePropertyValues(entity, entity.VersionId, 0, out _, out _); + + SetEntityTags(entity, _tagRepository, _jsonSerializer); + + PersistRelations(entity); + + OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages())); + + entity.ResetDirtyProperties(); } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs index e26e30f21b..d4790a387a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,229 +12,234 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository + private readonly IShortStringHelper _shortStringHelper; + + public MemberTypeRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + IContentTypeCommonRepository commonRepository, + ILanguageRepository languageRepository, + IShortStringHelper shortStringHelper) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) => + _shortStringHelper = shortStringHelper; + + protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; + + protected override Guid NodeObjectTypeId => Constants.ObjectTypes.MemberType; + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, + // since this is a FullDataSet policy - and everything is cached + // so here, + // every PerformGet/Exists just GetMany() and then filters + // except PerformGetAll which is the one really doing the job + protected override IMemberType? PerformGet(int id) + => GetMany().FirstOrDefault(x => x.Id == id); + + protected override IMemberType? PerformGet(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) { - private readonly IShortStringHelper _shortStringHelper; + IEnumerable all = GetMany(); + return ids?.Any() ?? false ? all.Where(x => ids.Contains(x.Key)) : all; + } - public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository, IShortStringHelper shortStringHelper) - : base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) + protected override bool PerformExists(Guid id) + => GetMany().FirstOrDefault(x => x.Key == id) != null; + + protected override IMemberType? PerformGet(string alias) + => GetMany().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + protected override IEnumerable? GetAllWithFullCachePolicy() => + CommonRepository.GetAllTypes()?.OfType(); + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql subQuery = GetSubquery(); + var translator = new SqlTranslator(subQuery, query); + Sql subSql = translator.Translate(); + Sql sql = GetBaseQuery(false) + .WhereIn(x => x.NodeId, subSql) + .OrderBy(x => x.SortOrder); + var ids = Database.Fetch(sql).Distinct().ToArray(); + + return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name) : Enumerable.Empty(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) { - _shortStringHelper = shortStringHelper; - } - - protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); - } - - // every GetExists method goes cachePolicy.GetSomething which in turns goes PerformGetAll, - // since this is a FullDataSet policy - and everything is cached - // so here, - // every PerformGet/Exists just GetMany() and then filters - // except PerformGetAll which is the one really doing the job - - protected override IMemberType? PerformGet(int id) - => GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IMemberType? PerformGet(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id); - - protected override IEnumerable? PerformGetAll(params Guid[]? ids) - { - var all = GetMany(); - return ids?.Any() ?? false ? all?.Where(x => ids.Contains(x.Key)) : all; - } - - protected override bool PerformExists(Guid id) - => GetMany()?.FirstOrDefault(x => x.Key == id) != null; - - protected override IMemberType? PerformGet(string alias) - => GetMany()?.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); - - protected override IEnumerable? GetAllWithFullCachePolicy() - { - return CommonRepository.GetAllTypes()?.OfType(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var subQuery = GetSubquery(); - var translator = new SqlTranslator(subQuery, query); - var subSql = translator.Translate(); - var sql = GetBaseQuery(false) - .WhereIn(x => x.NodeId, subSql) - .OrderBy(x => x.SortOrder); - var ids = Database.Fetch(sql).Distinct().ToArray(); - - return ids.Length > 0 ? GetMany(ids).OrderBy(x => x.Name) : Enumerable.Empty(); - } - - protected override Sql GetBaseQuery(bool isCount) - { - if (isCount) - { - return Sql() - .SelectCount() - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - } - - var sql = Sql() - .Select(x => x.NodeId) + return Sql() + .SelectCount() .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; } - protected Sql GetSubquery() + Sql sql = Sql() + .Select(x => x.NodeId) + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) + .LeftJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected Sql GetSubquery() + { + Sql sql = Sql() + .Select("DISTINCT(umbracoNode.id)") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) + .LeftJoin() + .On(left => left.PropertyTypeId, right => right.Id) + .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) + .LeftJoin() + .On(left => left.ContentTypeNodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var l = (List)base.GetDeleteClauses(); // we know it's a list + l.Add("DELETE FROM cmsMemberType WHERE NodeId = @id"); + l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); + l.Add("DELETE FROM umbracoNode WHERE id = @id"); + return l; + } + + protected override void PersistNewItem(IMemberType entity) + { + ValidateAlias(entity); + + entity.AddingEntity(); + + // set a default icon if one is not specified + if (entity.Icon.IsNullOrWhiteSpace()) { - var sql = Sql() - .Select("DISTINCT(umbracoNode.id)") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .LeftJoin().On(left => left.ContentTypeId, right => right.NodeId) - .LeftJoin().On(left => left.PropertyTypeId, right => right.Id) - .LeftJoin().On(left => left.NodeId, right => right.DataTypeId) - .LeftJoin().On(left => left.ContentTypeNodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; + entity.Icon = Constants.Icons.Member; } - protected override string GetBaseWhereClause() + // By Convention we add 9 standard PropertyTypes to an Umbraco MemberType + Dictionary standardPropertyTypes = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + foreach (KeyValuePair standardPropertyType in standardPropertyTypes) { - return $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + entity.AddPropertyType( + standardPropertyType.Value, + Constants.Conventions.Member.StandardPropertiesGroupAlias, + Constants.Conventions.Member.StandardPropertiesGroupName); } - protected override IEnumerable GetDeleteClauses() + EnsureExplicitDataTypeForBuiltInProperties(entity); + PersistNewBaseContentType(entity); + + // Handles the MemberTypeDto (cmsMemberType table) + IEnumerable memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); + foreach (MemberPropertyTypeDto memberTypeDto in memberTypeDtos) { - var l = (List) base.GetDeleteClauses(); // we know it's a list - l.Add("DELETE FROM cmsMemberType WHERE NodeId = @id"); - l.Add("DELETE FROM cmsContentType WHERE nodeId = @id"); - l.Add("DELETE FROM umbracoNode WHERE id = @id"); - return l; + Database.Insert(memberTypeDto); } - protected override Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.MemberType; + entity.ResetDirtyProperties(); + } - protected override void PersistNewItem(IMemberType entity) + protected override void PersistUpdatedItem(IMemberType entity) + { + ValidateAlias(entity); + + // Updates Modified date + entity.UpdatingEntity(); + + // Look up parent to get and set the correct Path if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) { - ValidateAlias(entity); + NodeDto? parent = Database.First("WHERE id = @ParentId", new { entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + } - entity.AddingEntity(); + EnsureExplicitDataTypeForBuiltInProperties(entity); + PersistUpdatedBaseContentType(entity); - //set a default icon if one is not specified - if (entity.Icon.IsNullOrWhiteSpace()) + // remove and insert - handle cmsMemberType table + Database.Delete("WHERE NodeId = @Id", new { entity.Id }); + IEnumerable memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); + foreach (MemberPropertyTypeDto memberTypeDto in memberTypeDtos) + { + Database.Insert(memberTypeDto); + } + + entity.ResetDirtyProperties(); + } + + /// + /// Override so we can specify explicit db type's on any property types that are built-in. + /// + /// + /// + /// + /// + protected override PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, string propertyTypeAlias) + { + // custom property type constructor logic to set explicit dbtype's for built in properties + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + var readonlyStorageType = builtinProperties.TryGetValue(propertyTypeAlias, out PropertyType? propertyType); + storageType = readonlyStorageType ? propertyType!.ValueStorageType : storageType; + return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, readonlyStorageType, propertyTypeAlias); + } + + /// + /// Ensure that all the built-in membership provider properties have their correct data type + /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. + /// + /// + private void EnsureExplicitDataTypeForBuiltInProperties(IContentTypeBase memberType) + { + Dictionary builtinProperties = + ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + foreach (IPropertyType propertyType in memberType.PropertyTypes) + { + if (builtinProperties.ContainsKey(propertyType.Alias)) { - entity.Icon = Cms.Core.Constants.Icons.Member; - } - - //By Convention we add 9 standard PropertyTypes to an Umbraco MemberType - var standardPropertyTypes = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - foreach (var standardPropertyType in standardPropertyTypes) - { - entity.AddPropertyType(standardPropertyType.Value, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName); - } - - EnsureExplicitDataTypeForBuiltInProperties(entity); - PersistNewBaseContentType(entity); - - //Handles the MemberTypeDto (cmsMemberType table) - var memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); - foreach (var memberTypeDto in memberTypeDtos) - { - Database.Insert(memberTypeDto); - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IMemberType entity) - { - ValidateAlias(entity); - - //Updates Modified date - entity.UpdatingEntity(); - - //Look up parent to get and set the correct Path if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - } - - EnsureExplicitDataTypeForBuiltInProperties(entity); - PersistUpdatedBaseContentType(entity); - - // remove and insert - handle cmsMemberType table - Database.Delete("WHERE NodeId = @Id", new { Id = entity.Id }); - var memberTypeDtos = ContentTypeFactory.BuildMemberPropertyTypeDtos(entity); - foreach (var memberTypeDto in memberTypeDtos) - { - Database.Insert(memberTypeDto); - } - - entity.ResetDirtyProperties(); - } - - /// - /// Override so we can specify explicit db type's on any property types that are built-in. - /// - /// - /// - /// - /// - protected override PropertyType CreatePropertyType(string propertyEditorAlias, ValueStorageType storageType, string propertyTypeAlias) - { - //custom property type constructor logic to set explicit dbtype's for built in properties - var builtinProperties = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - var readonlyStorageType = builtinProperties.TryGetValue(propertyTypeAlias, out var propertyType); - storageType = readonlyStorageType ? propertyType!.ValueStorageType : storageType; - return new PropertyType(_shortStringHelper, propertyEditorAlias, storageType, readonlyStorageType, propertyTypeAlias); - } - - /// - /// Ensure that all the built-in membership provider properties have their correct data type - /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. - /// - /// - private void EnsureExplicitDataTypeForBuiltInProperties(IContentTypeBase memberType) - { - var builtinProperties = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); - foreach (var propertyType in memberType.PropertyTypes) - { - if (builtinProperties.ContainsKey(propertyType.Alias)) + // this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line + if (builtinProperties.TryGetValue(propertyType.Alias, out PropertyType? propDefinition)) { - //this reset's its current data type reference which will be re-assigned based on the property editor assigned on the next line - if (builtinProperties.TryGetValue(propertyType.Alias, out var propDefinition) && propDefinition != null) - { - propertyType.DataTypeId = propDefinition.DataTypeId; - propertyType.DataTypeKey = propDefinition.DataTypeKey; - } - else - { - propertyType.DataTypeId = 0; - propertyType.DataTypeKey = default; - } + propertyType.DataTypeId = propDefinition.DataTypeId; + propertyType.DataTypeKey = propDefinition.DataTypeKey; + } + else + { + propertyType.DataTypeId = 0; + propertyType.DataTypeKey = default; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs index 7c910d7485..47d43a9a4e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs @@ -1,4 +1,4 @@ -using System; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -13,22 +13,20 @@ public class NodeCountRepository : INodeCountRepository public NodeCountRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; - /// - + /// public int GetNodeCount(Guid nodeType) { - var query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + Sql? query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectCount() .From() .Where(x => x.NodeObjectType == nodeType && x.Trashed == false); return _scopeAccessor.AmbientScope?.Database.ExecuteScalar(query) ?? 0; - } public int GetMediaCount() { - var query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + Sql? query = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectCount() .From() .InnerJoin() diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs index be42f7b74f..8e279735d5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs @@ -1,116 +1,110 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class NotificationsRepository : INotificationsRepository { - public class NotificationsRepository : INotificationsRepository + private readonly IScopeAccessor _scopeAccessor; + + public NotificationsRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + private IScope? AmbientScope => _scopeAccessor.AmbientScope; + + public IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType) { - private readonly IScopeAccessor _scopeAccessor; - - public NotificationsRepository(IScopeAccessor scopeAccessor) + var nodeIdsA = nodeIds.ToArray(); + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin().On(left => left.NodeId, right => right.NodeId) + .InnerJoin().On(left => left.UserId, right => right.Id) + .Where(x => x.NodeObjectType == objectType) + .Where(x => x.Disabled == false) // only approved users + .Where(x => x.Action == action); // on the specified action + if (nodeIdsA.Length > 0) { - _scopeAccessor = scopeAccessor; - } - - private Scoping.IScope? AmbientScope => _scopeAccessor.AmbientScope; - - public IEnumerable? GetUsersNotifications(IEnumerable userIds, string? action, IEnumerable nodeIds, Guid objectType) - { - var nodeIdsA = nodeIds.ToArray(); - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin().On(left => left.NodeId, right => right.NodeId) - .InnerJoin().On(left => left.UserId, right => right.Id) - .Where(x => x.NodeObjectType == objectType) - .Where(x => x.Disabled == false) // only approved users - .Where(x => x.Action == action); // on the specified action - if (nodeIdsA.Length > 0) - sql? - .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes sql? - .OrderBy(x => x.Id) - .OrderBy(dto => dto.NodeId); - return AmbientScope?.Database.Fetch(sql).Select(x => new Notification(x.NodeId, x.UserId, x.Action, objectType)); + .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes } - public IEnumerable? GetUserNotifications(IUser user) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id AS nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => dto.UserId == (int)user.Id) - .OrderBy(dto => dto.NodeId); + sql? + .OrderBy(x => x.Id) + .OrderBy(dto => dto.NodeId); + return AmbientScope?.Database.Fetch(sql) + .Select(x => new Notification(x.NodeId, x.UserId, x.Action, objectType)); + } - var dtos = AmbientScope?.Database.Fetch(sql); - //need to map the results - return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); - } + public IEnumerable? GetUserNotifications(IUser user) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id AS nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => dto.UserId == user.Id) + .OrderBy(dto => dto.NodeId); - public IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions) - { - DeleteNotifications(user, entity); - return actions.Select(action => CreateNotification(user, entity, action)).ToList(); - } + List? dtos = AmbientScope?.Database.Fetch(sql); - public IEnumerable? GetEntityNotifications(IEntity entity) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT umbracoNode.id as nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => dto.NodeId == entity.Id) - .OrderBy(dto => dto.NodeId); + // need to map the results + return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); + } - var dtos = AmbientScope?.Database.Fetch(sql); - //need to map the results - return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); - } + public IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions) + { + DeleteNotifications(user, entity); + return actions.Select(action => CreateNotification(user, entity, action)).ToList(); + } - public int DeleteNotifications(IEntity entity) - { - return AmbientScope?.Database.Delete("WHERE nodeId = @nodeId", new { nodeId = entity.Id }) ?? 0; - } + public IEnumerable? GetEntityNotifications(IEntity entity) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select( + "DISTINCT umbracoNode.id as nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + .Where(dto => dto.NodeId == entity.Id) + .OrderBy(dto => dto.NodeId); - public int DeleteNotifications(IUser user) - { - return AmbientScope?.Database.Delete("WHERE userId = @userId", new { userId = user.Id }) ?? 0; - } + List? dtos = AmbientScope?.Database.Fetch(sql); - public int DeleteNotifications(IUser user, IEntity entity) - { - // delete all settings on the node for this user - return AmbientScope?.Database.Delete("WHERE userId = @userId AND nodeId = @nodeId", new { userId = user.Id, nodeId = entity.Id }) ?? 0; - } + // need to map the results + return dtos?.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList(); + } - public Notification CreateNotification(IUser user, IEntity entity, string action) - { - var sql = AmbientScope?.SqlContext.Sql() - .Select("DISTINCT nodeObjectType") - .From() - .Where(nodeDto => nodeDto.NodeId == entity.Id); - var nodeType = AmbientScope?.Database.ExecuteScalar(sql); + public int DeleteNotifications(IEntity entity) => + AmbientScope?.Database.Delete("WHERE nodeId = @nodeId", new { nodeId = entity.Id }) ?? 0; - var dto = new User2NodeNotifyDto - { - Action = action, - NodeId = entity.Id, - UserId = user.Id - }; - AmbientScope?.Database.Insert(dto); - return new Notification(dto.NodeId, dto.UserId, dto.Action, nodeType); - } + public int DeleteNotifications(IUser user) => + AmbientScope?.Database.Delete("WHERE userId = @userId", new { userId = user.Id }) ?? 0; + + public int DeleteNotifications(IUser user, IEntity entity) => + + // delete all settings on the node for this user + AmbientScope?.Database.Delete( + "WHERE userId = @userId AND nodeId = @nodeId", + new { userId = user.Id, nodeId = entity.Id }) ?? 0; + + public Notification CreateNotification(IUser user, IEntity entity, string action) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select("DISTINCT nodeObjectType") + .From() + .Where(nodeDto => nodeDto.NodeId == entity.Id); + Guid? nodeType = AmbientScope?.Database.ExecuteScalar(sql); + + var dto = new User2NodeNotifyDto { Action = action, NodeId = entity.Id, UserId = user.Id }; + AmbientScope?.Database.Insert(dto); + return new Notification(dto.NodeId, dto.UserId, dto.Action, nodeType); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs index b4c8ce4f6c..37c6d67228 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewMacroRepository.cs @@ -1,15 +1,15 @@ -using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement -{ - internal class PartialViewMacroRepository : PartialViewRepository, IPartialViewMacroRepository - { - public PartialViewMacroRepository(FileSystems fileSystems) - : base(fileSystems.MacroPartialsFileSystem) - { } +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; - protected override PartialViewType ViewType => PartialViewType.PartialViewMacro; +internal class PartialViewMacroRepository : PartialViewRepository, IPartialViewMacroRepository +{ + public PartialViewMacroRepository(FileSystems fileSystems) + : base(fileSystems.MacroPartialsFileSystem) + { } + + protected override PartialViewType ViewType => PartialViewType.PartialViewMacro; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs index 9fbd5af5cd..ff751a9fe6 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PartialViewRepository.cs @@ -1,141 +1,142 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PartialViewRepository : FileRepository, IPartialViewRepository { - internal class PartialViewRepository : FileRepository, IPartialViewRepository + public PartialViewRepository(FileSystems fileSystems) + : base(fileSystems.PartialViewsFileSystem) { - public PartialViewRepository(FileSystems fileSystems) - : base(fileSystems.PartialViewsFileSystem) + } + + protected PartialViewRepository(IFileSystem? fileSystem) + : base(fileSystem) + { + } + + protected virtual PartialViewType ViewType => PartialViewType.PartialView; + + public override IPartialView? Get(string? id) + { + if (FileSystem is null) { + return null; } - protected PartialViewRepository(IFileSystem? fileSystem) - : base(fileSystem) + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id!); + + if (FileSystem.FileExists(path) == false) { + return null; } - protected virtual PartialViewType ViewType => PartialViewType.PartialView; + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; - public override IPartialView? Get(string? id) + // var content = GetFileContent(path); + var view = new PartialView(ViewType, path, file => GetFileContent(file.OriginalPath)) { - if (FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id!); + // id can be the hash + Id = path.GetHashCode(), + Key = path.EncodeAsGuid(), - if (FileSystem.FileExists(path) == false) - return null; + // Content = content, + CreateDate = created, + UpdateDate = updated, + VirtualPath = FileSystem.GetUrl(id), + }; - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); + // reset dirty initial properties (U4-1946) + view.ResetDirtyProperties(false); - var view = new PartialView(ViewType, path, file => GetFileContent(file.OriginalPath)) - { - //id can be the hash - Id = path.GetHashCode(), - Key = path.EncodeAsGuid(), - //Content = content, - CreateDate = created, - UpdateDate = updated, - VirtualPath = FileSystem.GetUrl(id) - }; + return view; + } - // reset dirty initial properties (U4-1946) - view.ResetDirtyProperties(false); - - return view; + public override void Save(IPartialView entity) + { + var partialView = entity as PartialView; + if (partialView != null) + { + partialView.ViewType = ViewType; } - public override void Save(IPartialView entity) + base.Save(entity); + + // ensure that from now on, content is lazy-loaded + if (partialView != null && partialView.GetFileContent == null) { - var partialView = entity as PartialView; - if (partialView != null) - partialView.ViewType = ViewType; - - base.Save(entity); - - // ensure that from now on, content is lazy-loaded - if (partialView != null && partialView.GetFileContent == null) - partialView.GetFileContent = file => GetFileContent(file.OriginalPath); - } - - public override IEnumerable GetMany(params string[]? ids) - { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct().ToArray(); - - if (ids?.Any() ?? false) - { - foreach (var id in ids) - { - var partialView = Get(id); - if (partialView is not null) - { - yield return partialView; - } - } - } - else - { - var files = FindAllFiles("", "*.*"); - foreach (var file in files) - { - var partialView = Get(file); - if (partialView is not null) - { - yield return partialView; - } - } - } - } - - public Stream GetFileContentStream(string filepath) - { - if (FileSystem?.FileExists(filepath) == false) - { - return Stream.Null; - } - - try - { - return FileSystem?.OpenFile(filepath) ?? Stream.Null; - } - catch - { - return Stream.Null; // deal with race conds - } - } - - public void SetFileContent(string filepath, Stream content) - { - FileSystem?.AddFile(filepath, content, true); - } - - /// - /// Gets a stream that is used to write to the file - /// - /// - /// - /// - /// This ensures the stream includes a utf8 BOM - /// - protected override Stream GetContentStream(string content) - { - var data = Encoding.UTF8.GetBytes(content); - var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); - return new MemoryStream(withBom); + partialView.GetFileContent = file => GetFileContent(file.OriginalPath); } } + + public override IEnumerable GetMany(params string[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct().ToArray(); + + if (ids?.Any() ?? false) + { + foreach (var id in ids) + { + IPartialView? partialView = Get(id); + if (partialView is not null) + { + yield return partialView; + } + } + } + else + { + IEnumerable files = FindAllFiles(string.Empty, "*.*"); + foreach (var file in files) + { + IPartialView? partialView = Get(file); + if (partialView is not null) + { + yield return partialView; + } + } + } + } + + public Stream GetFileContentStream(string filepath) + { + if (FileSystem?.FileExists(filepath) == false) + { + return Stream.Null; + } + + try + { + return FileSystem?.OpenFile(filepath) ?? Stream.Null; + } + catch + { + return Stream.Null; // deal with race conds + } + } + + public void SetFileContent(string filepath, Stream content) => FileSystem?.AddFile(filepath, content, true); + + /// + /// Gets a stream that is used to write to the file + /// + /// + /// + /// + /// This ensures the stream includes a utf8 BOM + /// + protected override Stream GetContentStream(string content) + { + var data = Encoding.UTF8.GetBytes(content); + var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray(); + return new MemoryStream(withBom); + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs index 9919707e8a..85a168997d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PermissionRepository.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -13,275 +10,163 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// A (sub) repository that exposes functionality to modify assigned permissions to a node +/// +/// +/// +/// This repo implements the base class so that permissions can be +/// queued to be persisted +/// like the normal repository pattern but the standard repository Get commands don't apply and will throw +/// +/// +internal class PermissionRepository : EntityRepositoryBase + where TEntity : class, IEntity { - /// - /// A (sub) repository that exposes functionality to modify assigned permissions to a node - /// - /// - /// - /// This repo implements the base class so that permissions can be - /// queued to be persisted - /// like the normal repository pattern but the standard repository Get commands don't apply and will throw - /// - /// - internal class PermissionRepository : EntityRepositoryBase - where TEntity : class, IEntity + public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) + : base(scopeAccessor, cache, logger) { - public PermissionRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger> logger) - : base(scopeAccessor, cache, logger) - { - } + } - /// - /// Returns explicitly defined permissions for a user group for any number of nodes - /// - /// - /// The group ids to lookup permissions for - /// - /// - /// - /// - /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. - /// - public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) - { - var result = new EntityPermissionCollection(); + /// + /// Returns explicitly defined permissions for a user group for any number of nodes + /// + /// + /// The group ids to lookup permissions for + /// + /// + /// + /// + /// This method will not support passing in more than 2000 group IDs when also passing in entity IDs. + /// + public EntityPermissionCollection GetPermissionsForEntities(int[] groupIds, params int[] entityIds) + { + var result = new EntityPermissionCollection(); - if (entityIds.Length == 0) + if (entityIds.Length == 0) + { + foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) { - foreach (IEnumerable group in groupIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => group.Contains(dto.UserGroupId)); + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin().On( + (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => group.Contains(dto.UserGroupId)); - List permissions = - AmbientScope.Database.Fetch(sql); - foreach (EntityPermission permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) + { + result.Add(permission); } } - else + } + else + { + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - + groupIds.Length)) { - foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount - - groupIds.Length)) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin().On( - (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => - groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin().On( + (left, right) => left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => + groupIds.Contains(dto.UserGroupId) && group.Contains(dto.NodeId)); - List permissions = - AmbientScope.Database.Fetch(sql); - foreach (EntityPermission permission in ConvertToPermissionList(permissions)) - { - result.Add(permission); - } + List permissions = + AmbientScope.Database.Fetch(sql); + foreach (EntityPermission permission in ConvertToPermissionList(permissions)) + { + result.Add(permission); } } - - return result; } - /// - /// Returns permissions directly assigned to the content items for all user groups - /// - /// - /// - public IEnumerable GetPermissionsForEntities(int[] entityIds) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => entityIds.Contains(dto.NodeId)) - .OrderBy(dto => dto.NodeId); + return result; + } - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); + /// + /// Returns permissions directly assigned to the content items for all user groups + /// + /// + /// + public IEnumerable GetPermissionsForEntities(int[] entityIds) + { + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On((left, right) => + left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => entityIds.Contains(dto.NodeId)) + .OrderBy(dto => dto.NodeId); + + List result = AmbientScope.Database.Fetch(sql); + return ConvertToPermissionList(result); + } + + /// + /// Returns permissions directly assigned to the content item for all user groups + /// + /// + /// + public EntityPermissionCollection GetPermissionsForEntity(int entityId) + { + Sql sql = Sql() + .SelectAll() + .From() + .LeftJoin() + .On((left, right) => + left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) + .Where(dto => dto.NodeId == entityId) + .OrderBy(dto => dto.NodeId); + + List result = AmbientScope.Database.Fetch(sql); + return ConvertToPermissionList(result); + } + + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// + /// The permissions to assign or null to remove the connection between group and entityIds + /// + /// + /// This will first clear the permissions for this user and entities and recreate them + /// + public void ReplacePermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + { + if (entityIds.Length == 0) + { + return; } - /// - /// Returns permissions directly assigned to the content item for all user groups - /// - /// - /// - public EntityPermissionCollection GetPermissionsForEntity(int entityId) - { - Sql sql = Sql() - .SelectAll() - .From() - .LeftJoin() - .On((left, right) => - left.NodeId == right.NodeId && left.UserGroupId == right.UserGroupId) - .Where(dto => dto.NodeId == entityId) - .OrderBy(dto => dto.NodeId); + IUmbracoDatabase db = AmbientScope.Database; - List result = AmbientScope.Database.Fetch(sql); - return ConvertToPermissionList(result); + foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", new { groupId, nodeIds = group }); + + db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", new { groupId, nodeIds = group }); } - /// - /// Assigns the same permission set for a single group to any number of entities - /// - /// - /// The permissions to assign or null to remove the connection between group and entityIds - /// - /// - /// This will first clear the permissions for this user and entities and recreate them - /// - public void ReplacePermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + if (permissions is not null) { - if (entityIds.Length == 0) - { - return; - } - - IUmbracoDatabase db = AmbientScope.Database; - - foreach (IEnumerable group in entityIds.InGroupsOf(Constants.Sql.MaxParameterCount)) - { - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", - new { groupId, nodeIds = group }); - - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND nodeId in (@nodeIds)", - new { groupId, nodeIds = group }); - } - - - if (permissions is not null) - { - var toInsert = new List(); - var toInsertPermissions = new List(); - - foreach (var e in entityIds) - { - toInsert.Add(new UserGroup2NodeDto() { NodeId = e, UserGroupId = groupId }); - foreach (var p in permissions) - { - toInsertPermissions.Add(new UserGroup2NodePermissionDto - { - NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId - }); - } - } - - db.BulkInsertRecords(toInsert); - db.BulkInsertRecords(toInsertPermissions); - } - } - - /// - /// Assigns one permission for a user to many entities - /// - /// - /// - /// - public void AssignPermission(int groupId, char permission, params int[] entityIds) - { - IUmbracoDatabase db = AmbientScope.Database; - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@entityIds)", - new { groupId, entityIds }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)", - new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); - - UserGroup2NodeDto[] actionsPermissions = entityIds.Select(id => new UserGroup2NodeDto - { - NodeId = id, UserGroupId = groupId - }).ToArray(); - - UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto - { - NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId - }).ToArray(); - - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); - } - - /// - /// Assigns one permission to an entity for multiple groups - /// - /// - /// - /// - public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) - { - IUmbracoDatabase db = AmbientScope.Database; - var groupIdsA = groupIds.ToArray(); - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId AND userGroupId in (@groupIds)", - new { - nodeId = entity.Id, - groupIds = groupIdsA - }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)", - new - { - nodeId = entity.Id, - permission = permission.ToString(CultureInfo.InvariantCulture), - groupIds = groupIdsA - }); - - UserGroup2NodePermissionDto[] actionsPermissions = groupIdsA.Select(id => new UserGroup2NodePermissionDto - { - NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id - }).ToArray(); - - UserGroup2NodeDto[] actions = groupIdsA.Select(id => new UserGroup2NodeDto - { - NodeId = entity.Id, UserGroupId = id - }).ToArray(); - - db.BulkInsertRecords(actions); - db.BulkInsertRecords(actionsPermissions); - } - - /// - /// Assigns permissions to an entity for multiple group/permission entries - /// - /// - /// - /// - /// This will first clear the permissions for this entity then re-create them - /// - public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) - { - IUmbracoDatabase db = AmbientScope.Database; - - db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); - db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); - var toInsert = new List(); var toInsertPermissions = new List(); - foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) + + foreach (var e in entityIds) { - toInsert.Add(new UserGroup2NodeDto - { - NodeId = permissionSet.EntityId, - UserGroupId = entityPermission.UserGroupId - }); - foreach (var permission in entityPermission.AssignedPermissions) + toInsert.Add(new UserGroup2NodeDto { NodeId = e, UserGroupId = groupId }); + foreach (var p in permissions) { toInsertPermissions.Add(new UserGroup2NodePermissionDto { - NodeId = permissionSet.EntityId, - Permission = permission, - UserGroupId = entityPermission.UserGroupId + NodeId = e, Permission = p.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId, }); } } @@ -289,74 +174,174 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement db.BulkInsertRecords(toInsert); db.BulkInsertRecords(toInsertPermissions); } - - /// - /// Used to add or update entity permissions during a content item being updated - /// - /// - protected override void PersistNewItem(ContentPermissionSet entity) => - //does the same thing as update - PersistUpdatedItem(entity); - - /// - /// Used to add or update entity permissions during a content item being updated - /// - /// - protected override void PersistUpdatedItem(ContentPermissionSet entity) - { - var asIEntity = (IEntity)entity; - if (asIEntity.HasIdentity == false) - { - throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); - } - - ReplaceEntityPermissions(entity); - } - - private static EntityPermissionCollection ConvertToPermissionList( - IEnumerable result) - { - var permissions = new EntityPermissionCollection(); - IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); - foreach (IGrouping np in nodePermissions) - { - IEnumerable> userGroupPermissions = - np.GroupBy(x => x.UserGroupId); - foreach (IGrouping permission in userGroupPermissions) - { - var perms = permission.Select(x => x.Permission).Distinct().ToArray(); - - // perms can contain null if there are no permissions assigned, but the node is chosen in the UI. - permissions.Add(new EntityPermission(permission.Key, np.Key, - perms.WhereNotNull().ToArray())); - } - } - - return permissions; - } - - #region Not implemented (don't need to for the purposes of this repo) - - protected override ContentPermissionSet PerformGet(int id) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override IEnumerable PerformGetAll(params int[]? ids) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override Sql GetBaseQuery(bool isCount) => - throw new InvalidOperationException("This method won't be implemented."); - - protected override string GetBaseWhereClause() => - throw new InvalidOperationException("This method won't be implemented."); - - protected override IEnumerable GetDeleteClauses() => new List(); - - protected override void PersistDeletedItem(ContentPermissionSet entity) => - throw new InvalidOperationException("This method won't be implemented."); - - #endregion } + + /// + /// Assigns one permission for a user to many entities + /// + /// + /// + /// + public void AssignPermission(int groupId, char permission, params int[] entityIds) + { + IUmbracoDatabase db = AmbientScope.Database; + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @groupId AND nodeId in (@entityIds)", new { groupId, entityIds }); + db.Execute( + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @groupId AND permission=@permission AND nodeId in (@entityIds)", + new { groupId, permission = permission.ToString(CultureInfo.InvariantCulture), entityIds }); + + UserGroup2NodeDto[] actionsPermissions = + entityIds.Select(id => new UserGroup2NodeDto { NodeId = id, UserGroupId = groupId }).ToArray(); + + UserGroup2NodePermissionDto[] actions = entityIds.Select(id => new UserGroup2NodePermissionDto + { + NodeId = id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = groupId, + }).ToArray(); + + db.BulkInsertRecords(actions); + db.BulkInsertRecords(actionsPermissions); + } + + /// + /// Assigns one permission to an entity for multiple groups + /// + /// + /// + /// + public void AssignEntityPermission(TEntity entity, char permission, IEnumerable groupIds) + { + IUmbracoDatabase db = AmbientScope.Database; + var groupIdsA = groupIds.ToArray(); + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId AND userGroupId in (@groupIds)", new { nodeId = entity.Id, groupIds = groupIdsA }); + db.Execute( + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId AND permission = @permission AND userGroupId in (@groupIds)", + new + { + nodeId = entity.Id, + permission = permission.ToString(CultureInfo.InvariantCulture), + groupIds = groupIdsA, + }); + + UserGroup2NodePermissionDto[] actionsPermissions = groupIdsA.Select(id => new UserGroup2NodePermissionDto + { + NodeId = entity.Id, Permission = permission.ToString(CultureInfo.InvariantCulture), UserGroupId = id, + }).ToArray(); + + UserGroup2NodeDto[] actions = groupIdsA.Select(id => new UserGroup2NodeDto + { + NodeId = entity.Id, UserGroupId = id, + }).ToArray(); + + db.BulkInsertRecords(actions); + db.BulkInsertRecords(actionsPermissions); + } + + /// + /// Assigns permissions to an entity for multiple group/permission entries + /// + /// + /// + /// + /// This will first clear the permissions for this entity then re-create them + /// + public void ReplaceEntityPermissions(EntityPermissionSet permissionSet) + { + IUmbracoDatabase db = AmbientScope.Database; + + db.Execute("DELETE FROM umbracoUserGroup2Node WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); + db.Execute("DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @nodeId", new { nodeId = permissionSet.EntityId }); + + var toInsert = new List(); + var toInsertPermissions = new List(); + foreach (EntityPermission entityPermission in permissionSet.PermissionsSet) + { + toInsert.Add(new UserGroup2NodeDto + { + NodeId = permissionSet.EntityId, UserGroupId = entityPermission.UserGroupId, + }); + foreach (var permission in entityPermission.AssignedPermissions) + { + toInsertPermissions.Add(new UserGroup2NodePermissionDto + { + NodeId = permissionSet.EntityId, + Permission = permission, + UserGroupId = entityPermission.UserGroupId, + }); + } + } + + db.BulkInsertRecords(toInsert); + db.BulkInsertRecords(toInsertPermissions); + } + + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistNewItem(ContentPermissionSet entity) => + + // Does the same thing as update + PersistUpdatedItem(entity); + + /// + /// Used to add or update entity permissions during a content item being updated + /// + /// + protected override void PersistUpdatedItem(ContentPermissionSet entity) + { + var asIEntity = (IEntity)entity; + if (asIEntity.HasIdentity == false) + { + throw new InvalidOperationException("Cannot create permissions for an entity without an Id"); + } + + ReplaceEntityPermissions(entity); + } + + private static EntityPermissionCollection ConvertToPermissionList( + IEnumerable result) + { + var permissions = new EntityPermissionCollection(); + IEnumerable> nodePermissions = result.GroupBy(x => x.NodeId); + foreach (IGrouping np in nodePermissions) + { + IEnumerable> userGroupPermissions = + np.GroupBy(x => x.UserGroupId); + foreach (IGrouping permission in userGroupPermissions) + { + var perms = permission.Select(x => x.Permission).Distinct().ToArray(); + + // perms can contain null if there are no permissions assigned, but the node is chosen in the UI. + permissions.Add(new EntityPermission(permission.Key, np.Key, perms.WhereNotNull().ToArray())); + } + } + + return permissions; + } + + #region Not implemented (don't need to for the purposes of this repo) + + protected override ContentPermissionSet PerformGet(int id) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetAll(params int[]? ids) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => new List(); + + protected override void PersistDeletedItem(ContentPermissionSet entity) => + throw new InvalidOperationException("This method won't be implemented."); + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs index c7f7724d6d..2716df9315 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublicAccessRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -14,151 +11,149 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class PublicAccessRepository : EntityRepositoryBase, IPublicAccessRepository { - internal class PublicAccessRepository : EntityRepositoryBase, IPublicAccessRepository + public PublicAccessRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public PublicAccessRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } - - protected override IRepositoryCachePolicy CreateCachePolicy() - { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - } - - protected override PublicAccessEntry? PerformGet(Guid id) - { - //return from GetAll - this will be cached as a collection - return GetMany()?.FirstOrDefault(x => x.Key == id); - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - var sql = GetBaseQuery(false); - - if (ids?.Any() ?? false) - { - sql.WhereIn(x => x.Id, ids); - } - - sql.OrderBy(x => x.NodeId); - - var dtos = Database.FetchOneToMany(x => x.Rules, sql); - return dtos.Select(PublicAccessEntryFactory.BuildEntity); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.FetchOneToMany(x => x.Rules, sql); - return dtos.Select(PublicAccessEntryFactory.BuildEntity); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return Sql() - .SelectAll() - .From() - .LeftJoin() - .On(left => left.Id, right => right.AccessId); - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.Access}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoAccessRule WHERE accessId = @id", - "DELETE FROM umbracoAccess WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(PublicAccessEntry entity) - { - entity.AddingEntity(); - foreach (var rule in entity.Rules) - rule.AddingEntity(); - - var dto = PublicAccessEntryFactory.BuildDto(entity); - - Database.Insert(dto); - //update the id so HasEntity is correct - entity.Id = entity.Key.GetHashCode(); - - foreach (var rule in dto.Rules) - { - rule.AccessId = entity.Key; - Database.Insert(rule); - } - - //update the id so HasEntity is correct - foreach (var rule in entity.Rules) - rule.Id = rule.Key.GetHashCode(); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(PublicAccessEntry entity) - { - entity.UpdatingEntity(); - foreach (var rule in entity.Rules) - { - if (rule.HasIdentity) - rule.UpdatingEntity(); - else - rule.AddingEntity(); - } - - var dto = PublicAccessEntryFactory.BuildDto(entity); - - Database.Update(dto); - - foreach (var removedRule in entity.RemovedRules) - { - Database.Delete("WHERE id=@Id", new { Id = removedRule }); - } - - foreach (var rule in entity.Rules) - { - if (rule.HasIdentity) - { - var count = Database.Update(dto.Rules.Single(x => x.Id == rule.Key)); - if (count == 0) - { - throw new InvalidOperationException("No rows were updated for the access rule"); - } - } - else - { - Database.Insert(new AccessRuleDto - { - Id = rule.Key, - AccessId = dto.Id, - RuleValue = rule.RuleValue, - RuleType = rule.RuleType, - CreateDate = rule.CreateDate, - UpdateDate = rule.UpdateDate - }); - //update the id so HasEntity is correct - rule.Id = rule.Key.GetHashCode(); - } - } - - entity.ResetDirtyProperties(); - } - - protected override Guid GetEntityId(PublicAccessEntry entity) - { - return entity.Key; - } } + + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); + + protected override PublicAccessEntry? PerformGet(Guid id) => + + // return from GetAll - this will be cached as a collection + GetMany().FirstOrDefault(x => x.Key == id); + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + Sql sql = GetBaseQuery(false); + + if (ids?.Any() ?? false) + { + sql.WhereIn(x => x.Id, ids); + } + + sql.OrderBy(x => x.NodeId); + + List? dtos = Database.FetchOneToMany(x => x.Rules, sql); + return dtos.Select(PublicAccessEntryFactory.BuildEntity); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.FetchOneToMany(x => x.Rules, sql); + return dtos.Select(PublicAccessEntryFactory.BuildEntity); + } + + protected override Sql GetBaseQuery(bool isCount) => + Sql() + .SelectAll() + .From() + .LeftJoin() + .On(left => left.Id, right => right.AccessId); + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Access}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoAccessRule WHERE accessId = @id", "DELETE FROM umbracoAccess WHERE id = @id", + }; + return list; + } + + protected override void PersistNewItem(PublicAccessEntry entity) + { + entity.AddingEntity(); + foreach (PublicAccessRule rule in entity.Rules) + { + rule.AddingEntity(); + } + + AccessDto dto = PublicAccessEntryFactory.BuildDto(entity); + + Database.Insert(dto); + + // update the id so HasEntity is correct + entity.Id = entity.Key.GetHashCode(); + + foreach (AccessRuleDto rule in dto.Rules) + { + rule.AccessId = entity.Key; + Database.Insert(rule); + } + + // update the id so HasEntity is correct + foreach (PublicAccessRule rule in entity.Rules) + { + rule.Id = rule.Key.GetHashCode(); + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(PublicAccessEntry entity) + { + entity.UpdatingEntity(); + foreach (PublicAccessRule rule in entity.Rules) + { + if (rule.HasIdentity) + { + rule.UpdatingEntity(); + } + else + { + rule.AddingEntity(); + } + } + + AccessDto dto = PublicAccessEntryFactory.BuildDto(entity); + + Database.Update(dto); + + foreach (Guid removedRule in entity.RemovedRules) + { + Database.Delete("WHERE id=@Id", new { Id = removedRule }); + } + + foreach (PublicAccessRule rule in entity.Rules) + { + if (rule.HasIdentity) + { + var count = Database.Update(dto.Rules.Single(x => x.Id == rule.Key)); + if (count == 0) + { + throw new InvalidOperationException("No rows were updated for the access rule"); + } + } + else + { + Database.Insert(new AccessRuleDto + { + Id = rule.Key, + AccessId = dto.Id, + RuleValue = rule.RuleValue, + RuleType = rule.RuleType, + CreateDate = rule.CreateDate, + UpdateDate = rule.UpdateDate, + }); + + // update the id so HasEntity is correct + rule.Id = rule.Key.GetHashCode(); + } + } + + entity.ResetDirtyProperties(); + } + + protected override Guid GetEntityId(PublicAccessEntry entity) => entity.Key; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs index 72d7d2dfcc..630dd187c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/QueryType.cs @@ -1,28 +1,27 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Specifies the type of base query. +/// +public enum QueryType { /// - /// Specifies the type of base query. + /// Get one single complete item. /// - public enum QueryType - { - /// - /// Get one single complete item. - /// - Single, + Single, - /// - /// Get many complete items. - /// - Many, + /// + /// Get many complete items. + /// + Many, - /// - /// Get item identifiers only. - /// - Ids, + /// + /// Get item identifiers only. + /// + Ids, - /// - /// Count items. - /// - Count - } + /// + /// Count items. + /// + Count, } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index e49ccbdf77..512eed0ee9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Security.Cryptography; using Microsoft.Extensions.Logging; using NPoco; @@ -13,226 +10,225 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository { - internal class RedirectUrlRepository : EntityRepositoryBase, IRedirectUrlRepository + public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public RedirectUrlRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger) - : base(scopeAccessor, cache, logger) + } + + public IRedirectUrl? Get(string url, Guid contentKey, string? culture) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false).Where(x => + x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); + RedirectUrlDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); + + public void DeleteContentUrls(Guid contentKey) => + Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); + + public void Delete(Guid id) => Database.Delete(id); + + public IRedirectUrl? GetMostRecentUrl(string url) + { + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + public IRedirectUrl? GetMostRecentUrl(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) { + return GetMostRecentUrl(url); } - public IRedirectUrl? Get(string url, Guid contentKey, string? culture) + var urlHash = url.GenerateHash(); + Sql sql = GetBaseQuery(false) + .Where(x => x.Url == url && x.UrlHash == urlHash && + (x.Culture == culture.ToLower() || x.Culture == null || + x.Culture == string.Empty)) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); + + if (dto == null) { - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false).Where(x => - x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); - RedirectUrlDto? dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); + dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); } - public void DeleteAll() => Database.Execute("DELETE FROM umbracoRedirectUrl"); + return dto == null ? null : Map(dto); + } - public void DeleteContentUrls(Guid contentKey) => - Database.Execute("DELETE FROM umbracoRedirectUrl WHERE contentKey=@contentKey", new { contentKey }); + public IEnumerable GetContentUrls(Guid contentKey) + { + Sql sql = GetBaseQuery(false) + .Where(x => x.ContentKey == contentKey) + .OrderByDescending(x => x.CreateDateUtc); + List dtos = Database.Fetch(sql); + return dtos.Select(Map).WhereNotNull(); + } - public void Delete(Guid id) => Database.Delete(id); + public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + return result.Items.Select(Map).WhereNotNull(); + } - public IRedirectUrl? GetMostRecentUrl(string url) + public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), + SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map).WhereNotNull(); + return rules; + } + + public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) + { + Sql sql = GetBaseQuery(false) + .Where( + string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), + SqlSyntax.GetQuotedColumnName("Url")), + new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) + .OrderByDescending(x => x.CreateDateUtc); + Page result = Database.Page(pageIndex + 1, pageSize, sql); + total = Convert.ToInt32(result.TotalItems); + + IEnumerable rules = result.Items.Select(Map).WhereNotNull(); + return rules; + } + + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override bool PerformExists(Guid id) => PerformGet(id) != null; + + protected override IRedirectUrl? PerformGet(Guid id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + RedirectUrlDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + protected override IEnumerable PerformGetAll(params Guid[]? ids) + { + if (ids?.Length > Constants.Sql.MaxParameterCount) { - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - RedirectUrlDto? dto = dtos.FirstOrDefault(); - return dto == null ? null : Map(dto); + throw new NotSupportedException( + $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); } - public IRedirectUrl? GetMostRecentUrl(string url, string culture) + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + if (isCount) { - if (string.IsNullOrWhiteSpace(culture)) - { - return GetMostRecentUrl(url); - } - - var urlHash = url.GenerateHash(); - Sql sql = GetBaseQuery(false) - .Where(x => x.Url == url && x.UrlHash == urlHash && - (x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty)) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); - - if (dto == null) - { - dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); - } - - return dto == null ? null : Map(dto); - } - - public IEnumerable GetContentUrls(Guid contentKey) - { - Sql sql = GetBaseQuery(false) - .Where(x => x.ContentKey == contentKey) - .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - return dtos.Select(Map).WhereNotNull(); - } - - public IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total) - { - Sql sql = GetBaseQuery(false) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - return result.Items.Select(Map).WhereNotNull(); - } - - public IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total) - { - Sql sql = GetBaseQuery(false) - .Where( - string.Format("{0}.{1} LIKE @path", SqlSyntax.GetQuotedTableName("umbracoNode"), - SqlSyntax.GetQuotedColumnName("path")), new { path = "%," + rootContentId + ",%" }) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - IEnumerable rules = result.Items.Select(Map).WhereNotNull(); - return rules; - } - - public IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total) - { - Sql sql = GetBaseQuery(false) - .Where( - string.Format("{0}.{1} LIKE @url", SqlSyntax.GetQuotedTableName("umbracoRedirectUrl"), - SqlSyntax.GetQuotedColumnName("Url")), - new { url = "%" + searchTerm.Trim().ToLowerInvariant() + "%" }) - .OrderByDescending(x => x.CreateDateUtc); - Page result = Database.Page(pageIndex + 1, pageSize, sql); - total = Convert.ToInt32(result.TotalItems); - - IEnumerable rules = result.Items.Select(Map).WhereNotNull(); - return rules; - } - - protected override int PerformCount(IQuery query) => - throw new NotSupportedException("This repository does not support this method."); - - protected override bool PerformExists(Guid id) => PerformGet(id) != null; - - protected override IRedirectUrl? PerformGet(Guid id) - { - Sql sql = GetBaseQuery(false).Where(x => x.Id == id); - RedirectUrlDto? dto = Database.Fetch(sql.SelectTop(1)).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - protected override IEnumerable PerformGetAll(params Guid[]? ids) - { - if (ids?.Length > Constants.Sql.MaxParameterCount) - { - throw new NotSupportedException( - $"This repository does not support more than {Constants.Sql.MaxParameterCount} ids."); - } - - Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - List dtos = Database.Fetch(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) => - throw new NotSupportedException("This repository does not support this method."); - - protected override Sql GetBaseQuery(bool isCount) - { - Sql sql = Sql(); - if (isCount) - { - sql.Select(@"COUNT(*) + sql.Select(@"COUNT(*) FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); - } - else - { - sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId + } + else + { + sql.Select(@"umbracoRedirectUrl.*, umbracoNode.id AS contentId FROM umbracoRedirectUrl JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); - } - - return sql; } - protected override string GetBaseWhereClause() => "id = @id"; + return sql; + } - protected override IEnumerable GetDeleteClauses() + protected override string GetBaseWhereClause() => "id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoRedirectUrl WHERE id = @id" }; + return list; + } + + protected override void PersistNewItem(IRedirectUrl entity) + { + RedirectUrlDto? dto = Map(entity); + Database.Insert(dto); + entity.Id = entity.Key.GetHashCode(); + } + + protected override void PersistUpdatedItem(IRedirectUrl entity) + { + RedirectUrlDto? dto = Map(entity); + if (dto is not null) { - var list = new List { "DELETE FROM umbracoRedirectUrl WHERE id = @id" }; - return list; + Database.Update(dto); + } + } + + private static RedirectUrlDto? Map(IRedirectUrl redirectUrl) + { + if (redirectUrl == null) + { + return null; } - protected override void PersistNewItem(IRedirectUrl entity) + return new RedirectUrlDto { - RedirectUrlDto? dto = Map(entity); - Database.Insert(dto); - entity.Id = entity.Key.GetHashCode(); + Id = redirectUrl.Key, + ContentKey = redirectUrl.ContentKey, + CreateDateUtc = redirectUrl.CreateDateUtc, + Url = redirectUrl.Url, + Culture = redirectUrl.Culture, + UrlHash = redirectUrl.Url.GenerateHash(), + }; + } + + private static IRedirectUrl? Map(RedirectUrlDto dto) + { + if (dto == null) + { + return null; } - protected override void PersistUpdatedItem(IRedirectUrl entity) + var url = new RedirectUrl(); + try { - RedirectUrlDto? dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } + url.DisableChangeTracking(); + url.Key = dto.Id; + url.Id = dto.Id.GetHashCode(); + url.ContentId = dto.ContentId; + url.ContentKey = dto.ContentKey; + url.CreateDateUtc = dto.CreateDateUtc; + url.Culture = dto.Culture; + url.Url = dto.Url; + return url; } - - private static RedirectUrlDto? Map(IRedirectUrl redirectUrl) + finally { - if (redirectUrl == null) - { - return null; - } - - return new RedirectUrlDto - { - Id = redirectUrl.Key, - ContentKey = redirectUrl.ContentKey, - CreateDateUtc = redirectUrl.CreateDateUtc, - Url = redirectUrl.Url, - Culture = redirectUrl.Culture, - UrlHash = redirectUrl.Url.GenerateHash() - }; - } - - private static IRedirectUrl? Map(RedirectUrlDto dto) - { - if (dto == null) - { - return null; - } - - var url = new RedirectUrl(); - try - { - url.DisableChangeTracking(); - url.Key = dto.Id; - url.Id = dto.Id.GetHashCode(); - url.ContentId = dto.ContentId; - url.ContentKey = dto.ContentKey; - url.CreateDateUtc = dto.CreateDateUtc; - url.Culture = dto.Culture; - url.Url = dto.Url; - return url; - } - finally - { - url.EnableChangeTracking(); - } + url.EnableChangeTracking(); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index d7e65adaf4..88f1a6fee9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -19,176 +15,214 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class RelationRepository : EntityRepositoryBase, IRelationRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class RelationRepository : EntityRepositoryBase, IRelationRepository + private readonly IEntityRepositoryExtended _entityRepository; + private readonly IRelationTypeRepository _relationTypeRepository; + + public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository) + : base(scopeAccessor, AppCaches.NoCache, logger) { - private readonly IRelationTypeRepository _relationTypeRepository; - private readonly IEntityRepositoryExtended _entityRepository; + _relationTypeRepository = relationTypeRepository; + _entityRepository = entityRepository; + } - public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepositoryExtended entityRepository) - : base(scopeAccessor, AppCaches.NoCache, logger) + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + => GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + => GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + + public void Save(IEnumerable relations) + { + foreach (IGrouping hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) { - _relationTypeRepository = relationTypeRepository; - _entityRepository = entityRepository; - } - - #region Overrides of RepositoryBase - - protected override IRelation? PerformGet(int id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { id }); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - if (dto == null) - return null; - - var relationType = _relationTypeRepository.Get(dto.RelationType); - if (relationType == null) - throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); - - return DtoToEntity(dto, relationType); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false); - if (ids?.Length > 0) - sql.WhereIn(x => x.Id, ids); - sql.OrderBy(x => x.RelationType); - var dtos = Database.Fetch(sql); - return DtosToEntities(dtos); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - sql.OrderBy(x => x.RelationType); - var dtos = Database.Fetch(sql); - return DtosToEntities(dtos); - } - - private IEnumerable DtosToEntities(IEnumerable dtos) - { - //NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter - - return dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).WhereNotNull().ToList(); - } - - private static IRelation? DtoToEntity(RelationDto dto, IRelationType? relationType) - { - if (relationType is null) + if (hasIdentityGroup.Key) { - return null; + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + IRelation[] asArray = hasIdentityGroup.ToArray(); + foreach (IRelation relation in hasIdentityGroup) + { + relation.UpdatingEntity(); + RelationDto dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + + PopulateObjectTypes(asArray); } - var entity = RelationFactory.BuildEntity(dto, relationType); + else + { + // Do bulk inserts + var entitiesAndDtos = hasIdentityGroup.ToDictionary( + r => // key = entity + { + r.AddingEntity(); + return r; + }, + RelationFactory.BuildDto); // value = DTO - // reset dirty initial properties (U4-1946) - entity.ResetDirtyProperties(false); + foreach (RelationDto dto in entitiesAndDtos.Values) + { + Database.Insert(dto); + } - return entity; + // All dtos now have IDs assigned + foreach (KeyValuePair de in entitiesAndDtos) + { + // re-assign ID to the entity + de.Key.Id = de.Value.Id; + } + + PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); + } + } + } + + public void SaveBulk(IEnumerable relations) + { + foreach (IGrouping hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) + { + if (hasIdentityGroup.Key) + { + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + foreach (ReadOnlyRelation relation in hasIdentityGroup) + { + RelationDto dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + } + else + { + // Do bulk inserts + IEnumerable dtos = hasIdentityGroup.Select(RelationFactory.BuildDto); + + Database.InsertBulk(dtos); + } + } + } + + public IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering) + { + Sql sql = GetBaseQuery(false); + + if (ordering == null || ordering.IsEmpty) + { + ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Constants.DatabaseSchema.Tables.Relation, "id")); } - #endregion + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); - #region Overrides of EntityRepositoryBase + // apply ordering + ApplyOrdering(ref sql, ordering); - protected override Sql GetBaseQuery(bool isCount) + var pageIndexToFetch = pageIndex + 1; + Page? page = Database.Page(pageIndexToFetch, pageSize, sql); + List? dtos = page.Items; + totalRecords = page.TotalItems; + + var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray())? + .ToDictionary(x => x.Id, x => x); + + var result = dtos.Select(r => { - if (isCount) + if (relTypes is null || !relTypes.TryGetValue(r.RelationType, out IRelationType? relType)) { - return Sql().SelectCount().From(); + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); } - var sql = Sql().Select() - .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) - .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) + return DtoToEntity(r, relType); + }).WhereNotNull().ToList(); + + return result; + } + + public void DeleteByParent(int parentId, params string[] relationTypeAliases) + { + // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. + if (Database.DatabaseType.IsSqlite()) + { + Sql? query = Sql().Append(@"delete from umbracoRelation"); + + Sql subQuery = Sql().Select(x => x.Id) .From() - .InnerJoin("uchild").On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") - .InnerJoin("uparent").On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == parentId); + if (relationTypeAliases.Length > 0) + { + subQuery.WhereIn(x => x.Alias, relationTypeAliases); + } - return sql; + Sql fullQuery = query.WhereIn(x => x.Id, subQuery); + + Database.Execute(fullQuery); } - - protected override string GetBaseWhereClause() + else { - return $"{Constants.DatabaseSchema.Tables.Relation}.id = @id"; + if (relationTypeAliases.Length > 0) + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.RelationRepository.DeleteByParentIn, + tsql => Sql().Delete() + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == SqlTemplate.Arg("parentId")) + .WhereIn(x => x.Alias, SqlTemplate.ArgIn("relationTypeAliases"))); + + Sql sql = template.Sql(parentId, relationTypeAliases); + + Database.Execute(sql); + } + else + { + SqlTemplate template = SqlContext.Templates.Get( + Constants.SqlTemplates.RelationRepository.DeleteByParentAll, + tsql => Sql().Delete() + .From() + .InnerJoin().On(x => x.RelationType, x => x.Id) + .Where(x => x.ParentId == SqlTemplate.Arg("parentId"))); + + Sql sql = template.Sql(parentId); + + Database.Execute(sql); + } } + } - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoRelation WHERE id = @id" - }; - return list; - } + /// + /// Used for joining the entity query with relations for the paging methods + /// + /// + private void SqlJoinRelations(Sql sql) + { + // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for + // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). + sql.LeftJoin() + .On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); + sql.LeftJoin() + .On((left, right) => left.RelationType == right.Id); + } - #endregion + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) => - #region Unit of Work Implementation - - protected override void PersistNewItem(IRelation entity) - { - entity.AddingEntity(); - - var dto = RelationFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - - entity.Id = id; - PopulateObjectTypes(entity); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IRelation entity) - { - entity.UpdatingEntity(); - - var dto = RelationFactory.BuildDto(entity); - Database.Update(dto); - - PopulateObjectTypes(entity); - - entity.ResetDirtyProperties(); - } - - #endregion - - /// - /// Used for joining the entity query with relations for the paging methods - /// - /// - private void SqlJoinRelations(Sql sql) - { - // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for - // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). - sql.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); - sql.LeftJoin().On((left, right) => left.RelationType == right.Id); - } - - public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) - { - return GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); - } - - public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) - { - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } - // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data - // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it - // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we - // will just return the bare minimum entity data. - - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); @@ -200,22 +234,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.WhereIn(rel => rel.RelationType, relationTypes); } }); - } - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) - { - return GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); - } + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) => - public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) - { - // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } - // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data - // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it - // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we - // will just return the bare minimum entity data. - - return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } + // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data + // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it + // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we + // will just return the bare minimum entity data. + _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); @@ -227,241 +254,220 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.WhereIn(rel => rel.RelationType, relationTypes); } }); - } - public void Save(IEnumerable relations) - { - foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) - { - if (hasIdentityGroup.Key) - { - // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation - // however we can bulk populate the object types. It might be possible to bulk update - // with SQL but would be pretty ugly and we're not really too worried about that for perf, - // it's the bulk inserts we care about. - var asArray = hasIdentityGroup.ToArray(); - foreach (var relation in hasIdentityGroup) - { - relation.UpdatingEntity(); - var dto = RelationFactory.BuildDto(relation); - Database.Update(dto); - } - PopulateObjectTypes(asArray); - } - else - { - // Do bulk inserts - var entitiesAndDtos = hasIdentityGroup.ToDictionary( - r => // key = entity - { - r.AddingEntity(); - return r; - }, - RelationFactory.BuildDto); // value = DTO - - - foreach (var dto in entitiesAndDtos.Values) - { - Database.Insert(dto); - } - - // All dtos now have IDs assigned - foreach (var de in entitiesAndDtos) - { - // re-assign ID to the entity - de.Key.Id = de.Value.Id; - } - - PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); - } - } - } - - public void SaveBulk(IEnumerable relations) - { - foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) - { - if (hasIdentityGroup.Key) - { - // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation - // however we can bulk populate the object types. It might be possible to bulk update - // with SQL but would be pretty ugly and we're not really too worried about that for perf, - // it's the bulk inserts we care about. - foreach (var relation in hasIdentityGroup) - { - var dto = RelationFactory.BuildDto(relation); - Database.Update(dto); - } - } - else - { - // Do bulk inserts - var dtos = hasIdentityGroup.Select(RelationFactory.BuildDto); - - Database.InsertBulk(dtos); - - } - } - } - - public IEnumerable GetPagedRelationsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, Ordering? ordering) - { - var sql = GetBaseQuery(false); - - if (ordering == null || ordering.IsEmpty) - ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Cms.Core.Constants.DatabaseSchema.Tables.Relation, "id")); - - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - - // apply ordering - ApplyOrdering(ref sql, ordering); - - var pageIndexToFetch = pageIndex + 1; - var page = Database.Page(pageIndexToFetch, pageSize, sql); - var dtos = page.Items; - totalRecords = page.TotalItems; - - var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray())? - .ToDictionary(x => x.Id, x => x); - - var result = dtos.Select(r => - { - if (relTypes is null || !relTypes.TryGetValue(r.RelationType, out var relType)) - throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); - return DtoToEntity(r, relType); - }).WhereNotNull().ToList(); - - return result; - } - - - public void DeleteByParent(int parentId, params string[] relationTypeAliases) - { - // HACK: SQLite - hard to replace this without provider specific repositories/another ORM. - if (Database.DatabaseType.IsSqlite()) - { - var query = Sql().Append(@"delete from umbracoRelation"); - - var subQuery = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == parentId); - - if (relationTypeAliases.Length > 0) - { - subQuery.WhereIn(x => x.Alias, relationTypeAliases); - } - - var fullQuery = query.WhereIn(x => x.Id, subQuery); - - Database.Execute(fullQuery); - } - else - { - if (relationTypeAliases.Length > 0) - { - var template = SqlContext.Templates.Get( - Cms.Core.Constants.SqlTemplates.RelationRepository.DeleteByParentIn, - tsql => Sql().Delete() - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == SqlTemplate.Arg("parentId")) - .WhereIn(x => x.Alias, SqlTemplate.ArgIn("relationTypeAliases"))); - - var sql = template.Sql(parentId, relationTypeAliases); - - Database.Execute(sql); - } - else - { - var template = SqlContext.Templates.Get( - Cms.Core.Constants.SqlTemplates.RelationRepository.DeleteByParentAll, - tsql => Sql().Delete() - .From() - .InnerJoin().On(x => x.RelationType, x => x.Id) - .Where(x => x.ParentId == SqlTemplate.Arg("parentId"))); - - var sql = template.Sql(parentId); - - Database.Execute(sql); - } - } - } - - /// - /// Used to populate the object types after insert/update - /// - /// - private void PopulateObjectTypes(params IRelation[] entities) - { - var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); - - var nodes = Database.Fetch(Sql().Select().From() - .WhereIn(x => x.NodeId, entityIds)) - .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - - foreach (var e in entities) - { - if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) - { - e.ParentObjectType = parentObjectType.GetValueOrDefault(); - } - if (nodes.TryGetValue(e.ChildId, out var childObjectType)) - { - e.ChildObjectType = childObjectType.GetValueOrDefault(); - } - } - } - - private void ApplyOrdering(ref Sql sql, Ordering ordering) - { - if (sql == null) throw new ArgumentNullException(nameof(sql)); - if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - - // TODO: although this works for name, it probably doesn't work for others without an alias of some sort - var orderBy = ordering.OrderBy; - - if (ordering.Direction == Direction.Ascending) - sql.OrderBy(orderBy); - else - sql.OrderByDescending(orderBy); - } - } - - internal class RelationItemDto + /// + /// Used to populate the object types after insert/update + /// + /// + private void PopulateObjectTypes(params IRelation[] entities) { - [Column(Name = "nodeId")] - public int ChildNodeId { get; set; } + IEnumerable entityIds = + entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); - [Column(Name = "nodeKey")] - public Guid ChildNodeKey { get; set; } + var nodes = Database.Fetch(Sql().Select().From() + .WhereIn(x => x.NodeId, entityIds)) + .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - [Column(Name = "nodeName")] - public string? ChildNodeName { get; set; } + foreach (IRelation e in entities) + { + if (nodes.TryGetValue(e.ParentId, out Guid? parentObjectType)) + { + e.ParentObjectType = parentObjectType.GetValueOrDefault(); + } - [Column(Name = "nodeObjectType")] - public Guid ChildNodeObjectType { get; set; } - - [Column(Name = "contentTypeIcon")] - public string? ChildContentTypeIcon { get; set; } - - [Column(Name = "contentTypeAlias")] - public string? ChildContentTypeAlias { get; set; } - - [Column(Name = "contentTypeName")] - public string? ChildContentTypeName { get; set; } - - [Column(Name = "relationTypeName")] - public string? RelationTypeName { get; set; } - - [Column(Name = "relationTypeAlias")] - public string? RelationTypeAlias { get; set; } - - [Column(Name = "relationTypeIsDependency")] - public bool RelationTypeIsDependency { get; set; } - - [Column(Name = "relationTypeIsBidirectional")] - public bool RelationTypeIsBidirectional { get; set; } + if (nodes.TryGetValue(e.ChildId, out Guid? childObjectType)) + { + e.ChildObjectType = childObjectType.GetValueOrDefault(); + } + } } + + private void ApplyOrdering(ref Sql sql, Ordering ordering) + { + if (sql == null) + { + throw new ArgumentNullException(nameof(sql)); + } + + if (ordering == null) + { + throw new ArgumentNullException(nameof(ordering)); + } + + // TODO: although this works for name, it probably doesn't work for others without an alias of some sort + var orderBy = ordering.OrderBy; + + if (ordering.Direction == Direction.Ascending) + { + sql.OrderBy(orderBy); + } + else + { + sql.OrderByDescending(orderBy); + } + } + + #region Overrides of RepositoryBase + + protected override IRelation? PerformGet(int id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { id }); + + RelationDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + if (dto == null) + { + return null; + } + + IRelationType? relationType = _relationTypeRepository.Get(dto.RelationType); + if (relationType == null) + { + throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); + } + + return DtoToEntity(dto, relationType); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + if (ids?.Length > 0) + { + sql.WhereIn(x => x.Id, ids); + } + + sql.OrderBy(x => x.RelationType); + List? dtos = Database.Fetch(sql); + return DtosToEntities(dtos); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + sql.OrderBy(x => x.RelationType); + List? dtos = Database.Fetch(sql); + return DtosToEntities(dtos); + } + + private IEnumerable DtosToEntities(IEnumerable dtos) => + + // NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter + dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).WhereNotNull().ToList(); + + private static IRelation? DtoToEntity(RelationDto dto, IRelationType? relationType) + { + if (relationType is null) + { + return null; + } + + IRelation entity = RelationFactory.BuildEntity(dto, relationType); + + // reset dirty initial properties (U4-1946) + entity.ResetDirtyProperties(false); + + return entity; + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) + { + return Sql().SelectCount().From(); + } + + Sql sql = Sql().Select() + .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) + .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) + .From() + .InnerJoin("uchild") + .On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") + .InnerJoin("uparent") + .On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Relation}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoRelation WHERE id = @id" }; + return list; + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IRelation entity) + { + entity.AddingEntity(); + + RelationDto dto = RelationFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + + entity.Id = id; + PopulateObjectTypes(entity); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IRelation entity) + { + entity.UpdatingEntity(); + + RelationDto dto = RelationFactory.BuildDto(entity); + Database.Update(dto); + + PopulateObjectTypes(entity); + + entity.ResetDirtyProperties(); + } + + #endregion +} + +internal class RelationItemDto +{ + [Column(Name = "nodeId")] + public int ChildNodeId { get; set; } + + [Column(Name = "nodeKey")] + public Guid ChildNodeKey { get; set; } + + [Column(Name = "nodeName")] + public string? ChildNodeName { get; set; } + + [Column(Name = "nodeObjectType")] + public Guid ChildNodeObjectType { get; set; } + + [Column(Name = "contentTypeIcon")] + public string? ChildContentTypeIcon { get; set; } + + [Column(Name = "contentTypeAlias")] + public string? ChildContentTypeAlias { get; set; } + + [Column(Name = "contentTypeName")] + public string? ChildContentTypeName { get; set; } + + [Column(Name = "relationTypeName")] + public string? RelationTypeName { get; set; } + + [Column(Name = "relationTypeAlias")] + public string? RelationTypeAlias { get; set; } + + [Column(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } + + [Column(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs index 0d1b258374..af7458bab0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationTypeRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -15,151 +12,147 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents a repository for doing CRUD operations for +/// +internal class RelationTypeRepository : EntityRepositoryBase, IRelationTypeRepository { - /// - /// Represents a repository for doing CRUD operations for - /// - internal class RelationTypeRepository : EntityRepositoryBase, IRelationTypeRepository + public RelationTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public RelationTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + + private void CheckNullObjectTypeValues(IRelationType entity) + { + if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) { - return new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ true); + entity.ParentObjectType = null; } - #region Overrides of RepositoryBase - - protected override IRelationType? PerformGet(int id) + if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) { - // use the underlying GetAll which will force cache all content types - return GetMany()?.FirstOrDefault(x => x.Id == id); - } - - public IRelationType? Get(Guid id) - { - // use the underlying GetAll which will force cache all content types - return GetMany()?.FirstOrDefault(x => x.Key == id); - } - - public bool Exists(Guid id) - { - return Get(id) != null; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => DtoToEntity(x)); - } - - public IEnumerable GetMany(params Guid[]? ids) - { - // should not happen due to the cache policy - if (ids?.Any() ?? false) - throw new NotImplementedException(); - - return GetMany(new int[0]); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - var dtos = Database.Fetch(sql); - - return dtos.Select(x => DtoToEntity(x)); - } - - private static IRelationType DtoToEntity(RelationTypeDto dto) - { - var entity = RelationTypeFactory.BuildEntity(dto); - - // reset dirty initial properties (U4-1946) - ((BeingDirtyBase) entity).ResetDirtyProperties(false); - - return entity; - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql - .From(); - - return sql; - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.RelationType}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoRelation WHERE relType = @id", - "DELETE FROM umbracoRelationType WHERE id = @id" - }; - return list; - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistNewItem(IRelationType entity) - { - entity.AddingEntity(); - - CheckNullObjectTypeValues(entity); - - var dto = RelationTypeFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IRelationType entity) - { - entity.UpdatingEntity(); - - CheckNullObjectTypeValues(entity); - - var dto = RelationTypeFactory.BuildDto(entity); - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - #endregion - - private void CheckNullObjectTypeValues(IRelationType entity) - { - if (entity.ParentObjectType.HasValue && entity.ParentObjectType == Guid.Empty) - entity.ParentObjectType = null; - if (entity.ChildObjectType.HasValue && entity.ChildObjectType == Guid.Empty) - entity.ChildObjectType = null; + entity.ChildObjectType = null; } } + + #region Overrides of RepositoryBase + + protected override IRelationType? PerformGet(int id) => + + // use the underlying GetAll which will force cache all content types + GetMany()?.FirstOrDefault(x => x.Id == id); + + public IRelationType? Get(Guid id) => + + // use the underlying GetAll which will force cache all content types + GetMany()?.FirstOrDefault(x => x.Key == id); + + public bool Exists(Guid id) => Get(id) != null; + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => DtoToEntity(x)); + } + + public IEnumerable GetMany(params Guid[]? ids) + { + // should not happen due to the cache policy + if (ids?.Any() ?? false) + { + throw new NotImplementedException(); + } + + return GetMany(new int[0]); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List? dtos = Database.Fetch(sql); + + return dtos.Select(x => DtoToEntity(x)); + } + + private static IRelationType DtoToEntity(RelationTypeDto dto) + { + IRelationType entity = RelationTypeFactory.BuildEntity(dto); + + // reset dirty initial properties (U4-1946) + ((BeingDirtyBase)entity).ResetDirtyProperties(false); + + return entity; + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql + .From(); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.RelationType}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoRelation WHERE relType = @id", "DELETE FROM umbracoRelationType WHERE id = @id", + }; + return list; + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistNewItem(IRelationType entity) + { + entity.AddingEntity(); + + CheckNullObjectTypeValues(entity); + + RelationTypeDto dto = RelationTypeFactory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IRelationType entity) + { + entity.UpdatingEntity(); + + CheckNullObjectTypeValues(entity); + + RelationTypeDto dto = RelationTypeFactory.BuildDto(entity); + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs index fdcf11304b..7a1f4a2677 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RepositoryBase.cs @@ -1,4 +1,3 @@ -using System; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Persistence; @@ -6,77 +5,76 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Base repository class for all instances +/// +public abstract class RepositoryBase : IRepository { /// - /// Base repository class for all instances + /// Initializes a new instance of the class. /// - public abstract class RepositoryBase : IRepository + protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches) { - /// - /// Initializes a new instance of the class. - /// - protected RepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches) - { - ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); - AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); - } - - /// - /// Gets the - /// - protected AppCaches AppCaches { get; } - - /// - /// Gets the - /// - protected IScopeAccessor ScopeAccessor { get; } - - /// - /// Gets the AmbientScope - /// - protected IScope AmbientScope - { - get - { - IScope? scope = ScopeAccessor.AmbientScope; - if (scope == null) - { - throw new InvalidOperationException("Cannot run a repository without an ambient scope."); - } - - return scope; - } - } - - /// - /// Gets the repository's database. - /// - protected IUmbracoDatabase Database => AmbientScope.Database; - - /// - /// Gets the Sql context. - /// - protected ISqlContext SqlContext => AmbientScope.SqlContext; - - /// - /// Gets the - /// - protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; - - /// - /// Creates an expression - /// - protected Sql Sql() => SqlContext.Sql(); - - /// - /// Creates a expression - /// - protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); - - /// - /// Creates a new query expression - /// - protected IQuery Query() => SqlContext.Query(); + ScopeAccessor = scopeAccessor ?? throw new ArgumentNullException(nameof(scopeAccessor)); + AppCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); } + + /// + /// Gets the + /// + protected AppCaches AppCaches { get; } + + /// + /// Gets the + /// + protected IScopeAccessor ScopeAccessor { get; } + + /// + /// Gets the AmbientScope + /// + protected IScope AmbientScope + { + get + { + IScope? scope = ScopeAccessor.AmbientScope; + if (scope == null) + { + throw new InvalidOperationException("Cannot run a repository without an ambient scope."); + } + + return scope; + } + } + + /// + /// Gets the repository's database. + /// + protected IUmbracoDatabase Database => AmbientScope.Database; + + /// + /// Gets the Sql context. + /// + protected ISqlContext SqlContext => AmbientScope.SqlContext; + + /// + /// Gets the + /// + protected ISqlSyntaxProvider SqlSyntax => SqlContext.SqlSyntax; + + /// + /// Creates an expression + /// + protected Sql Sql() => SqlContext.Sql(); + + /// + /// Creates a expression + /// + protected Sql Sql(string sql, params object[] args) => SqlContext.Sql(sql, args); + + /// + /// Creates a new query expression + /// + protected IQuery Query() => SqlContext.Query(); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs index e50f29fd87..3094d0d04e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ScriptRepository.cs @@ -1,102 +1,99 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Script Repository +/// +internal class ScriptRepository : FileRepository, IScriptRepository { - /// - /// Represents the Script Repository - /// - internal class ScriptRepository : FileRepository, IScriptRepository + public ScriptRepository(FileSystems fileSystems) + : base(fileSystems.ScriptsFileSystem) { - public ScriptRepository(FileSystems fileSystems) - : base(fileSystems.ScriptsFileSystem) + } + + public override IScript? Get(string? id) + { + if (id is null || FileSystem is null) { + return null; } - #region Implementation of IRepository + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id); - public override IScript? Get(string? id) + if (FileSystem.FileExists(path) == false) { - if (id is null || FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id); - - if (FileSystem.FileExists(path) == false) - return null; - - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); - - var script = new Script(path, file => GetFileContent(file.OriginalPath)) - { - //id can be the hash - Id = path.GetHashCode(), - Key = path.EncodeAsGuid(), - //Content = content, - CreateDate = created, - UpdateDate = updated, - VirtualPath = FileSystem.GetUrl(path) - }; - - // reset dirty initial properties (U4-1946) - script.ResetDirtyProperties(false); - - return script; + return null; } - public override void Save(IScript entity) + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; + + var script = new Script(path, file => GetFileContent(file.OriginalPath)) { - // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later - var script = (Script) entity; + // id can be the hash + Id = path.GetHashCode(), + Key = path.EncodeAsGuid(), - base.Save(script); + // Content = content, + CreateDate = created, + UpdateDate = updated, + VirtualPath = FileSystem.GetUrl(path), + }; - // ensure that from now on, content is lazy-loaded - if (script.GetFileContent == null) - script.GetFileContent = file => GetFileContent(file.OriginalPath); + // reset dirty initial properties (U4-1946) + script.ResetDirtyProperties(false); + + return script; + } + + public override void Save(IScript entity) + { + // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later + var script = (Script)entity; + + base.Save(script); + + // ensure that from now on, content is lazy-loaded + if (script.GetFileContent == null) + { + script.GetFileContent = file => GetFileContent(file.OriginalPath); } + } - public override IEnumerable GetMany(params string[]? ids) + public override IEnumerable GetMany(params string[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids?.Distinct().ToArray(); + + if (ids?.Any() ?? false) { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids?.Distinct().ToArray(); - - if (ids?.Any() ?? false) + foreach (var id in ids) { - foreach (var id in ids) + IScript? script = Get(id); + if (script is not null) { - IScript? script = Get(id); - if (script is not null) - { - yield return script; - } - } - } - else - { - var files = FindAllFiles("", "*.*"); - foreach (var file in files) - { - IScript? script = Get(file); - if (script is not null) - { - yield return script; - } + yield return script; + } + } + } + else + { + IEnumerable files = FindAllFiles(string.Empty, "*.*"); + foreach (var file in files) + { + IScript? script = Get(file); + if (script is not null) + { + yield return script; } } } - - #endregion } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs index 2a0bc9bfa1..b6d5221b59 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ServerRegistrationRepository.cs @@ -1,129 +1,112 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.Factories; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class ServerRegistrationRepository : EntityRepositoryBase, + IServerRegistrationRepository { - internal class ServerRegistrationRepository : EntityRepositoryBase, IServerRegistrationRepository + public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger) + : base(scopeAccessor, AppCaches.NoCache, logger) { - public ServerRegistrationRepository(IScopeAccessor scopeAccessor, ILogger logger) - : base(scopeAccessor, AppCaches.NoCache, logger) - { } + } - protected override IRepositoryCachePolicy CreateCachePolicy() - { - // TODO: what are we doing with cache here? - // why are we using disabled cache helper up there? - // - // 7.6 says: - // note: this means that the ServerRegistrationRepository does *not* implement scoped cache, - // and this is because the repository is special and should not participate in scopes - // (cleanup in v8) - // - return new FullDataSetRepositoryCachePolicy(AppCaches.RuntimeCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - } + public void ClearCache() => CachePolicy.ClearAll(); - public void ClearCache() - { - CachePolicy.ClearAll(); - } + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + DateTime timeoutDate = DateTime.Now.Subtract(staleTimeout); - protected override int PerformCount(IQuery query) - { - throw new NotSupportedException("This repository does not support this method."); - } + Database.Update( + "SET isActive=0, isSchedulingPublisher=0 WHERE lastNotifiedDate < @timeoutDate", new + { + /*timeoutDate =*/ + timeoutDate, + }); + ClearCache(); + } - protected override bool PerformExists(int id) - { - // use the underlying GetAll which force-caches all registrations - return GetMany()?.Any(x => x.Id == id) ?? false; - } + protected override IRepositoryCachePolicy CreateCachePolicy() => - protected override IServerRegistration? PerformGet(int id) - { - // use the underlying GetAll which force-caches all registrations - return GetMany()?.FirstOrDefault(x => x.Id == id); - } + // TODO: what are we doing with cache here? + // why are we using disabled cache helper up there? + // + // 7.6 says: + // note: this means that the ServerRegistrationRepository does *not* implement scoped cache, + // and this is because the repository is special and should not participate in scopes + // (cleanup in v8) + new FullDataSetRepositoryCachePolicy(AppCaches.RuntimeCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - return Database.Fetch("WHERE id > 0") - .Select(x => ServerRegistrationFactory.BuildEntity(x)); - } + protected override int PerformCount(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotSupportedException("This repository does not support this method."); - } + protected override bool PerformExists(int id) => - protected override Sql GetBaseQuery(bool isCount) - { - var sql = Sql(); + // use the underlying GetAll which force-caches all registrations + GetMany().Any(x => x.Id == id); - sql = isCount - ? sql.SelectCount() - : sql.Select(); + protected override IServerRegistration? PerformGet(int id) => - sql - .From(); + // use the underlying GetAll which force-caches all registrations + GetMany().FirstOrDefault(x => x.Id == id); - return sql; - } + protected override IEnumerable PerformGetAll(params int[]? ids) => + Database.Fetch("WHERE id > 0") + .Select(x => ServerRegistrationFactory.BuildEntity(x)); - protected override string GetBaseWhereClause() - { - return "id = @id"; - } + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new NotSupportedException("This repository does not support this method."); - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoServer WHERE id = @id" - }; - return list; - } + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = Sql(); - protected override void PersistNewItem(IServerRegistration entity) - { - entity.AddingEntity(); + sql = isCount + ? sql.SelectCount() + : sql.Select(); - var dto = ServerRegistrationFactory.BuildDto(entity); + sql + .From(); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + return sql; + } - entity.ResetDirtyProperties(); - } + protected override string GetBaseWhereClause() => "id = @id"; - protected override void PersistUpdatedItem(IServerRegistration entity) - { - entity.UpdatingEntity(); + protected override IEnumerable GetDeleteClauses() + { + var list = new List { "DELETE FROM umbracoServer WHERE id = @id" }; + return list; + } - var dto = ServerRegistrationFactory.BuildDto(entity); + protected override void PersistNewItem(IServerRegistration entity) + { + entity.AddingEntity(); - Database.Update(dto); + ServerRegistrationDto dto = ServerRegistrationFactory.BuildDto(entity); - entity.ResetDirtyProperties(); - } + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; - public void DeactiveStaleServers(TimeSpan staleTimeout) - { - var timeoutDate = DateTime.Now.Subtract(staleTimeout); + entity.ResetDirtyProperties(); + } - Database.Update("SET isActive=0, isSchedulingPublisher=0 WHERE lastNotifiedDate < @timeoutDate", new { /*timeoutDate =*/ timeoutDate }); - ClearCache(); - } + protected override void PersistUpdatedItem(IServerRegistration entity) + { + entity.UpdatingEntity(); + + ServerRegistrationDto dto = ServerRegistrationFactory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs index 2621461a85..9f4bc451c9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimilarNodeName.cs @@ -1,242 +1,233 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; using static Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement.SimilarNodeName; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal static class ListExtensions { - internal class SimilarNodeName + internal static bool Contains(this IEnumerable items, StructuredName model) => + items.Any(x => x.FullName.InvariantEquals(model.FullName)); + + internal static bool SimpleNameExists(this IEnumerable items, string name) => + items.Any(x => x.FullName.InvariantEquals(name)); + + internal static bool SuffixedNameExists(this IEnumerable items) => + items.Any(x => x.Suffix.HasValue); +} + +internal class SimilarNodeName +{ + public int Id { get; set; } + + public string? Name { get; set; } + + public static string? GetUniqueName(IEnumerable names, int nodeId, string? nodeName) { - public int Id { get; set; } - public string? Name { get; set; } + IEnumerable items = names + .Where(x => x.Id != nodeId) // ignore same node + .Select(x => x.Name); - public static string? GetUniqueName(IEnumerable names, int nodeId, string? nodeName) - { - var items = names - .Where(x => x.Id != nodeId) // ignore same node - .Select(x => x.Name); + var uniqueName = GetUniqueName(items, nodeName); - var uniqueName = GetUniqueName(items, nodeName); - - return uniqueName; - } - - public static string? GetUniqueName(IEnumerable names, string? name) - { - var model = new StructuredName(name); - var items = names - .Where(x => x?.InvariantStartsWith(model.Text) ?? false) // ignore non-matching names - .Select(x => new StructuredName(x)); - - // name is empty, and there are no other names with suffixes, so just return " (1)" - if (model.IsEmptyName() && !items.Any()) - { - model.Suffix = StructuredName.INITIAL_SUFFIX; - - return model.FullName; - } - - // name is empty, and there are other names with suffixes - if (model.IsEmptyName() && items.SuffixedNameExists()) - { - var emptyNameSuffix = GetSuffixNumber(items); - - if (emptyNameSuffix > 0) - { - model.Suffix = (uint?)emptyNameSuffix; - - return model.FullName; - } - } - - // no suffix - name without suffix does NOT exist - we can just use the name without suffix. - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) - { - model.Suffix = StructuredName.NO_SUFFIX; - - return model.FullName; - } - - // suffix - name with suffix does NOT exist - // We can just return the full name as it is as there's no conflict. - if (model.Suffix.HasValue && !items.SimpleNameExists(model.FullName)) - { - return model.FullName; - } - - // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) - { - model.Suffix = StructuredName.NO_SUFFIX; - - return model.FullName; - } - - // no suffix - name without suffix exists, however name with suffix does NOT exist - if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) - { - var firstSuffix = GetFirstSuffix(items); - model.Suffix = (uint?)firstSuffix; - - return model.FullName; - } - - // no suffix - name without suffix exists, AND name with suffix does exist - if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - // no suffix - name without suffix does NOT exist, however name with suffix exists - if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - // has suffix - name without suffix exists - if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - // has suffix - name without suffix does NOT exist - // a case where the user added the suffix, so add a secondary suffix - if (model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) - { - model.Text = model.FullName; - model.Suffix = StructuredName.NO_SUFFIX; - - // filter items based on full name with suffix - items = items.Where(x => x.Text.InvariantStartsWith(model.FullName)); - var secondarySuffix = GetFirstSuffix(items); - model.Suffix = (uint?)secondarySuffix; - - return model.FullName; - } - - // has suffix - name without suffix also exists, therefore we simply increment - if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) - { - var nextSuffix = GetSuffixNumber(items); - model.Suffix = (uint?)nextSuffix; - - return model.FullName; - } - - return name; - } - - private static int GetFirstSuffix(IEnumerable items) - { - const int suffixStart = 1; - - if (!items.Any(x => x.Suffix == suffixStart)) - { - // none of the suffixes are the same as suffixStart, so we can use suffixStart! - return suffixStart; - } - - return GetSuffixNumber(items); - } - - private static int GetSuffixNumber(IEnumerable items) - { - int current = 1; - foreach (var item in items.OrderBy(x => x.Suffix)) - { - if (item.Suffix == current) - { - current++; - } - else if (item.Suffix > current) - { - // do nothing - we found our number! - // eg. when suffixes are 1 & 3, then this method is required to generate 2 - break; - } - } - - return current; - } - - internal class StructuredName - { - const string SPACE_CHARACTER = " "; - const string SUFFIXED_PATTERN = @"(.*) \(([1-9]\d*)\)$"; - internal const uint INITIAL_SUFFIX = 1; - internal static readonly uint? NO_SUFFIX = default; - - internal string Text { get; set; } - internal uint? Suffix { get; set; } - public string FullName - { - get - { - string text = (Text == SPACE_CHARACTER) ? Text.Trim() : Text; - - return Suffix > 0 ? $"{text} ({Suffix})" : text; - } - } - - internal StructuredName(string? name) - { - if (string.IsNullOrWhiteSpace(name)) - { - Text = SPACE_CHARACTER; - - return; - } - - var rg = new Regex(SUFFIXED_PATTERN); - var matches = rg.Matches(name); - if (matches.Count > 0) - { - var match = matches[0]; - Text = match.Groups[1].Value; - int number = int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) ? number : 0; - Suffix = (uint?)(number); - - return; - } - else - { - Text = name; - } - } - - internal bool IsEmptyName() - { - return string.IsNullOrWhiteSpace(Text); - } - } + return uniqueName; } - internal static class ListExtensions + public static string? GetUniqueName(IEnumerable names, string? name) { - internal static bool Contains(this IEnumerable items, StructuredName model) + var model = new StructuredName(name); + IEnumerable items = names + .Where(x => x?.InvariantStartsWith(model.Text) ?? false) // ignore non-matching names + .Select(x => new StructuredName(x)).ToArray(); + + // name is empty, and there are no other names with suffixes, so just return " (1)" + if (model.IsEmptyName() && !items.Any()) { - return items.Any(x => x.FullName.InvariantEquals(model.FullName)); + model.Suffix = StructuredName.Initialsuffix; + + return model.FullName; } - internal static bool SimpleNameExists(this IEnumerable items, string name) + // name is empty, and there are other names with suffixes + if (model.IsEmptyName() && items.SuffixedNameExists()) { - return items.Any(x => x.FullName.InvariantEquals(name)); + var emptyNameSuffix = GetSuffixNumber(items); + + if (emptyNameSuffix > 0) + { + model.Suffix = (uint?)emptyNameSuffix; + + return model.FullName; + } } - internal static bool SuffixedNameExists(this IEnumerable items) + // no suffix - name without suffix does NOT exist - we can just use the name without suffix. + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) { - return items.Any(x => x.Suffix.HasValue); + model.Suffix = StructuredName._nosuffix; + + return model.FullName; } + + // suffix - name with suffix does NOT exist + // We can just return the full name as it is as there's no conflict. + if (model.Suffix.HasValue && !items.SimpleNameExists(model.FullName)) + { + return model.FullName; + } + + // no suffix - name without suffix does NOT exist, AND name with suffix does NOT exist + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) + { + model.Suffix = StructuredName._nosuffix; + + return model.FullName; + } + + // no suffix - name without suffix exists, however name with suffix does NOT exist + if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && !items.SuffixedNameExists()) + { + var firstSuffix = GetFirstSuffix(items); + model.Suffix = (uint?)firstSuffix; + + return model.FullName; + } + + // no suffix - name without suffix exists, AND name with suffix does exist + if (!model.Suffix.HasValue && items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + // no suffix - name without suffix does NOT exist, however name with suffix exists + if (!model.Suffix.HasValue && !items.SimpleNameExists(model.Text) && items.SuffixedNameExists()) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + // has suffix - name without suffix exists + if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + // has suffix - name without suffix does NOT exist + // a case where the user added the suffix, so add a secondary suffix + if (model.Suffix.HasValue && !items.SimpleNameExists(model.Text)) + { + model.Text = model.FullName; + model.Suffix = StructuredName._nosuffix; + + // filter items based on full name with suffix + items = items.Where(x => x.Text.InvariantStartsWith(model.FullName)); + var secondarySuffix = GetFirstSuffix(items); + model.Suffix = (uint?)secondarySuffix; + + return model.FullName; + } + + // has suffix - name without suffix also exists, therefore we simply increment + if (model.Suffix.HasValue && items.SimpleNameExists(model.Text)) + { + var nextSuffix = GetSuffixNumber(items); + model.Suffix = (uint?)nextSuffix; + + return model.FullName; + } + + return name; + } + + private static int GetFirstSuffix(IEnumerable items) + { + const int suffixStart = 1; + + if (!items.Any(x => x.Suffix == suffixStart)) + { + // none of the suffixes are the same as suffixStart, so we can use suffixStart! + return suffixStart; + } + + return GetSuffixNumber(items); + } + + private static int GetSuffixNumber(IEnumerable items) + { + var current = 1; + foreach (StructuredName item in items.OrderBy(x => x.Suffix)) + { + if (item.Suffix == current) + { + current++; + } + else if (item.Suffix > current) + { + // do nothing - we found our number! + // eg. when suffixes are 1 & 3, then this method is required to generate 2 + break; + } + } + + return current; + } + + internal class StructuredName + { + internal const uint Initialsuffix = 1; + private const string Spacecharacter = " "; + private const string Suffixedpattern = @"(.*) \(([1-9]\d*)\)$"; + internal static readonly uint? _nosuffix = default; + + internal StructuredName(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + Text = Spacecharacter; + + return; + } + + var rg = new Regex(Suffixedpattern); + MatchCollection matches = rg.Matches(name); + if (matches.Count > 0) + { + Match match = matches[0]; + Text = match.Groups[1].Value; + int number = int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out number) + ? number + : 0; + Suffix = (uint?)number; + + return; + } + + Text = name; + } + + public string FullName + { + get + { + var text = Text == Spacecharacter ? Text.Trim() : Text; + + return Suffix > 0 ? $"{text} ({Suffix})" : text; + } + } + + internal string Text { get; set; } + + internal uint? Suffix { get; set; } + + internal bool IsEmptyName() => string.IsNullOrWhiteSpace(Text); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs index bf798b2845..1fe1f1e82a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core.Cache; @@ -10,87 +7,81 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +// TODO: Obsolete this, change all implementations of this like in Dictionary to just use custom Cache policies like in the member repository. + +/// +/// Simple abstract ReadOnly repository used to simply have PerformGet and PeformGetAll with an underlying cache +/// +internal abstract class SimpleGetRepository : EntityRepositoryBase + where TEntity : class, IEntity + where TDto : class { - // TODO: Obsolete this, change all implementations of this like in Dictionary to just use custom Cache policies like in the member repository. - - /// - /// Simple abstract ReadOnly repository used to simply have PerformGet and PeformGetAll with an underlying cache - /// - internal abstract class SimpleGetRepository : EntityRepositoryBase - where TEntity : class, IEntity - where TDto: class + protected SimpleGetRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) + : base(scopeAccessor, cache, logger) { - protected SimpleGetRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger> logger) - : base(scopeAccessor, cache, logger) - { } - - protected abstract TEntity ConvertToEntity(TDto dto); - protected abstract object GetBaseWhereClauseArguments(TId? id); - protected abstract string GetWhereInClauseForGetAll(); - - protected virtual IEnumerable PerformFetch(Sql sql) - { - return Database.Fetch(sql); - } - - protected override TEntity? PerformGet(TId? id) - { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), GetBaseWhereClauseArguments(id)); - - var dto = PerformFetch(sql).FirstOrDefault(); - if (dto == null) - return null; - - var entity = ConvertToEntity(dto); - - if (entity is EntityBase dirtyEntity) - { - // reset dirty initial properties (U4-1946) - dirtyEntity.ResetDirtyProperties(false); - } - - return entity; - } - - protected override IEnumerable PerformGetAll(params TId[]? ids) - { - var sql = Sql().From(); - - if (ids?.Any() ?? false) - { - sql.Where(GetWhereInClauseForGetAll(), new { /*ids =*/ ids }); - } - - return Database.Fetch(sql).Select(ConvertToEntity); - } - - protected sealed override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(ConvertToEntity); - } - - #region Not implemented and not required - - protected sealed override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected sealed override void PersistNewItem(TEntity entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - protected sealed override void PersistUpdatedItem(TEntity entity) - { - throw new InvalidOperationException("This method won't be implemented."); - } - - #endregion } + + protected abstract TEntity ConvertToEntity(TDto dto); + + protected abstract object GetBaseWhereClauseArguments(TId? id); + + protected abstract string GetWhereInClauseForGetAll(); + + protected virtual IEnumerable PerformFetch(Sql sql) => Database.Fetch(sql); + + protected override TEntity? PerformGet(TId? id) + { + Sql sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), GetBaseWhereClauseArguments(id)); + + TDto? dto = PerformFetch(sql).FirstOrDefault(); + if (dto == null) + { + return null; + } + + TEntity entity = ConvertToEntity(dto); + + if (entity is EntityBase dirtyEntity) + { + // reset dirty initial properties (U4-1946) + dirtyEntity.ResetDirtyProperties(false); + } + + return entity; + } + + protected override IEnumerable PerformGetAll(params TId[]? ids) + { + Sql sql = Sql().From(); + + if (ids?.Any() ?? false) + { + sql.Where(GetWhereInClauseForGetAll(), new + { + ids, + }); + } + + return Database.Fetch(sql).Select(ConvertToEntity); + } + + protected sealed override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(ConvertToEntity); + } + + protected sealed override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + + protected sealed override void PersistNewItem(TEntity entity) => + throw new InvalidOperationException("This method won't be implemented."); + + protected sealed override void PersistUpdatedItem(TEntity entity) => + throw new InvalidOperationException("This method won't be implemented."); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs index d22e54e76c..ca1f995e2b 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/StylesheetRepository.cs @@ -1,115 +1,117 @@ -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Stylesheet Repository +/// +internal class StylesheetRepository : FileRepository, IStylesheetRepository { - /// - /// Represents the Stylesheet Repository - /// - internal class StylesheetRepository : FileRepository, IStylesheetRepository + public StylesheetRepository(FileSystems fileSystems) + : base(fileSystems.StylesheetsFileSystem) { - public StylesheetRepository(FileSystems fileSystems) - : base(fileSystems.StylesheetsFileSystem) - { - } - - #region Overrides of FileRepository - - public override IStylesheet? Get(string? id) - { - if (id is null || FileSystem is null) - { - return null; - } - // get the relative path within the filesystem - // (though... id should be relative already) - var path = FileSystem.GetRelativePath(id); - - path = path.EnsureEndsWith(".css"); - - // if the css directory is changed, references to the old path can still exist (ie in RTE config) - // these old references will throw an error, which breaks the RTE - // try-catch here makes the request fail silently, and allows RTE to load correctly - try - { - if (FileSystem.FileExists(path) == false) - return null; - } catch - { - return null; - } - - // content will be lazy-loaded when required - var created = FileSystem.GetCreated(path).UtcDateTime; - var updated = FileSystem.GetLastModified(path).UtcDateTime; - //var content = GetFileContent(path); - - var stylesheet = new Stylesheet(path, file => GetFileContent(file.OriginalPath)) - { - //Content = content, - Key = path.EncodeAsGuid(), - CreateDate = created, - UpdateDate = updated, - Id = path.GetHashCode(), - VirtualPath = FileSystem.GetUrl(path) - }; - - // reset dirty initial properties (U4-1946) - stylesheet.ResetDirtyProperties(false); - - return stylesheet; - - } - - public override void Save(IStylesheet entity) - { - // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later - var stylesheet = (Stylesheet)entity; - - base.Save(stylesheet); - - // ensure that from now on, content is lazy-loaded - if (stylesheet.GetFileContent == null) - stylesheet.GetFileContent = file => GetFileContent(file.OriginalPath); - } - - public override IEnumerable GetMany(params string[]? ids) - { - //ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries - ids = ids? - .Select(x => x.EnsureEndsWith(".css")) - .Distinct() - .ToArray(); - - if (ids?.Any() ?? false) - { - foreach (var id in ids) - { - IStylesheet? stylesheet = Get(id); - if (stylesheet is not null) - { - yield return stylesheet; - } - } - } - else - { - var files = FindAllFiles("", "*.css"); - foreach (var file in files) - { - IStylesheet? stylesheet = Get(file); - if (stylesheet is not null) - { - yield return stylesheet; - } - } - } - } - - #endregion } + + #region Overrides of FileRepository + + public override IStylesheet? Get(string? id) + { + if (id is null || FileSystem is null) + { + return null; + } + + // get the relative path within the filesystem + // (though... id should be relative already) + var path = FileSystem.GetRelativePath(id); + + path = path.EnsureEndsWith(".css"); + + // if the css directory is changed, references to the old path can still exist (ie in RTE config) + // these old references will throw an error, which breaks the RTE + // try-catch here makes the request fail silently, and allows RTE to load correctly + try + { + if (FileSystem.FileExists(path) == false) + { + return null; + } + } + catch + { + return null; + } + + // content will be lazy-loaded when required + DateTime created = FileSystem.GetCreated(path).UtcDateTime; + DateTime updated = FileSystem.GetLastModified(path).UtcDateTime; + + // var content = GetFileContent(path); + var stylesheet = new Stylesheet(path, file => GetFileContent(file.OriginalPath)) + { + // Content = content, + Key = path.EncodeAsGuid(), + CreateDate = created, + UpdateDate = updated, + Id = path.GetHashCode(), + VirtualPath = FileSystem.GetUrl(path), + }; + + // reset dirty initial properties (U4-1946) + stylesheet.ResetDirtyProperties(false); + + return stylesheet; + } + + public override void Save(IStylesheet entity) + { + // TODO: Casting :/ Review GetFileContent and it's usages, need to look into it later + var stylesheet = (Stylesheet)entity; + + base.Save(stylesheet); + + // ensure that from now on, content is lazy-loaded + if (stylesheet.GetFileContent == null) + { + stylesheet.GetFileContent = file => GetFileContent(file.OriginalPath); + } + } + + public override IEnumerable GetMany(params string[]? ids) + { + // ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries + ids = ids? + .Select(x => x.EnsureEndsWith(".css")) + .Distinct() + .ToArray(); + + if (ids?.Any() ?? false) + { + foreach (var id in ids) + { + IStylesheet? stylesheet = Get(id); + if (stylesheet is not null) + { + yield return stylesheet; + } + } + } + else + { + IEnumerable files = FindAllFiles(string.Empty, "*.css"); + foreach (var file in files) + { + IStylesheet? stylesheet = Get(file); + if (stylesheet is not null) + { + yield return stylesheet; + } + } + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs index ca171a9b01..ecc6600d4c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TagRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; @@ -16,131 +13,131 @@ using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TagRepository : EntityRepositoryBase, ITagRepository { - internal class TagRepository : EntityRepositoryBase, ITagRepository + public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public TagRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) + } + + #region Manage Tag Entities + + /// + protected override ITag? PerformGet(int id) + { + Sql sql = Sql().Select().From().Where(x => x.Id == id); + TagDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + return dto == null ? null : TagFactory.BuildEntity(dto); + } + + /// + protected override IEnumerable PerformGetAll(params int[]? ids) + { + IEnumerable dtos = ids?.Length == 0 + ? Database.Fetch(Sql().Select().From()) + : Database.FetchByGroups(ids!, Constants.Sql.MaxParameterCount, + batch => Sql().Select().From().WhereIn(x => x.Id, batch)); + + return dtos.Select(TagFactory.BuildEntity).ToList(); + } + + /// + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sql = Sql().Select().From(); + var translator = new SqlTranslator(sql, query); + sql = translator.Translate(); + + return Database.Fetch(sql).Select(TagFactory.BuildEntity).ToList(); + } + + /// + protected override Sql GetBaseQuery(bool isCount) => + isCount ? Sql().SelectCount().From() : GetBaseQuery(); + + private Sql GetBaseQuery() => Sql().Select().From(); + + /// + protected override string GetBaseWhereClause() => "id = @id"; + + /// + protected override IEnumerable GetDeleteClauses() + { + var list = new List { + "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" + }; + return list; + } + + /// + protected override void PersistNewItem(ITag entity) + { + entity.AddingEntity(); + + TagDto dto = TagFactory.BuildDto(entity); + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + /// + protected override void PersistUpdatedItem(ITag entity) + { + entity.UpdatingEntity(); + + TagDto dto = TagFactory.BuildDto(entity); + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + + #endregion + + #region Assign and Remove Tags + + /// + // only invoked from ContentRepositoryBase with all cultures + replaceTags being true + public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) + { + // to no-duplicates array + ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); + + // replacing = clear all + if (replaceTags) + { + Sql sql0 = Sql().Delete() + .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); + Database.Execute(sql0); } - #region Manage Tag Entities - - /// - protected override ITag? PerformGet(int id) + // no tags? nothing else to do + if (tagsA.Length == 0) { - Sql sql = Sql().Select().From().Where(x => x.Id == id); - TagDto? dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - return dto == null ? null : TagFactory.BuildEntity(dto); + return; } - /// - protected override IEnumerable PerformGetAll(params int[]? ids) - { - IEnumerable dtos = ids?.Length == 0 - ? Database.Fetch(Sql().Select().From()) - : Database.FetchByGroups(ids!, Constants.Sql.MaxParameterCount, - batch => Sql().Select().From().WhereIn(x => x.Id, batch)); + // tags + // using some clever logic (?) to insert tags that don't exist in 1 query + // must coalesce languageId because equality of NULLs does not exist - return dtos.Select(TagFactory.BuildEntity).ToList(); - } + var tagSetSql = GetTagSet(tagsA); + var group = SqlSyntax.GetQuotedColumnName("group"); - /// - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sql = Sql().Select().From(); - var translator = new SqlTranslator(sql, query); - sql = translator.Translate(); - - return Database.Fetch(sql).Select(TagFactory.BuildEntity).ToList(); - } - - /// - protected override Sql GetBaseQuery(bool isCount) => - isCount ? Sql().SelectCount().From() : GetBaseQuery(); - - private Sql GetBaseQuery() => Sql().Select().From(); - - /// - protected override string GetBaseWhereClause() => "id = @id"; - - /// - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM cmsTagRelationship WHERE tagId = @id", "DELETE FROM cmsTags WHERE id = @id" - }; - return list; - } - - /// - protected override void PersistNewItem(ITag entity) - { - entity.AddingEntity(); - - TagDto dto = TagFactory.BuildDto(entity); - var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; - - entity.ResetDirtyProperties(); - } - - /// - protected override void PersistUpdatedItem(ITag entity) - { - entity.UpdatingEntity(); - - TagDto dto = TagFactory.BuildDto(entity); - Database.Update(dto); - - entity.ResetDirtyProperties(); - } - - #endregion - - #region Assign and Remove Tags - - /// - // only invoked from ContentRepositoryBase with all cultures + replaceTags being true - public void Assign(int contentId, int propertyTypeId, IEnumerable tags, bool replaceTags = true) - { - // to no-duplicates array - ITag[] tagsA = tags.Distinct(new TagComparer()).ToArray(); - - // replacing = clear all - if (replaceTags) - { - Sql sql0 = Sql().Delete() - .Where(x => x.NodeId == contentId && x.PropertyTypeId == propertyTypeId); - Database.Execute(sql0); - } - - // no tags? nothing else to do - if (tagsA.Length == 0) - { - return; - } - - // tags - // using some clever logic (?) to insert tags that don't exist in 1 query - // must coalesce languageId because equality of NULLs does not exist - - var tagSetSql = GetTagSet(tagsA); - var group = SqlSyntax.GetQuotedColumnName("group"); - - // insert tags - var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) + // insert tags + var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId) SELECT tagSet.tag, tagSet.{group}, tagSet.languageId FROM {tagSetSql} LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1)) WHERE cmsTags.id IS NULL"; - Database.Execute(sql1); + Database.Execute(sql1); - // insert relations - var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) + // insert relations + var sql2 = $@"INSERT INTO cmsTagRelationship (nodeId, propertyTypeId, tagId) SELECT {contentId}, {propertyTypeId}, tagSet2.Id FROM ( SELECT t.Id @@ -150,417 +147,416 @@ FROM ( LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId}) WHERE r.tagId IS NULL"; - Database.Execute(sql2); - } + Database.Execute(sql2); + } - /// - // only invoked from tests - public void Remove(int contentId, int propertyTypeId, IEnumerable tags) - { - var tagSetSql = GetTagSet(tags); - var group = SqlSyntax.GetQuotedColumnName("group"); + /// + // only invoked from tests + public void Remove(int contentId, int propertyTypeId, IEnumerable tags) + { + var tagSetSql = GetTagSet(tags); + var group = SqlSyntax.GetQuotedColumnName("group"); - var deleteSql = - $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( + var deleteSql = + $@"DELETE FROM cmsTagRelationship WHERE nodeId = {contentId} AND propertyTypeId = {propertyTypeId} AND tagId IN ( SELECT id FROM cmsTags INNER JOIN {tagSetSql} ON ( tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1) ) )"; - Database.Execute(deleteSql); - } + Database.Execute(deleteSql); + } - /// - public void RemoveAll(int contentId, int propertyTypeId) => - Database.Execute( - "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", - new { nodeId = contentId, propertyTypeId }); + /// + public void RemoveAll(int contentId, int propertyTypeId) => + Database.Execute( + "DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId AND propertyTypeId = @propertyTypeId", + new {nodeId = contentId, propertyTypeId}); - /// - public void RemoveAll(int contentId) => - Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", - new { nodeId = contentId }); + /// + public void RemoveAll(int contentId) => + Database.Execute("DELETE FROM cmsTagRelationship WHERE nodeId = @nodeId", + new {nodeId = contentId}); - // this is a clever way to produce an SQL statement like this: - // - // ( - // SELECT 'Spacesdd' AS Tag, 'default' AS [group] - // UNION - // SELECT 'Cool' AS tag, 'default' AS [group] - // ) AS tagSet - // - // which we can then use to reduce queries - // - private string GetTagSet(IEnumerable tags) + // this is a clever way to produce an SQL statement like this: + // + // ( + // SELECT 'Spacesdd' AS Tag, 'default' AS [group] + // UNION + // SELECT 'Cool' AS tag, 'default' AS [group] + // ) AS tagSet + // + // which we can then use to reduce queries + // + private string GetTagSet(IEnumerable tags) + { + var sql = new StringBuilder(); + var group = SqlSyntax.GetQuotedColumnName("group"); + var first = true; + + sql.Append("("); + + foreach (ITag tag in tags) { - var sql = new StringBuilder(); - var group = SqlSyntax.GetQuotedColumnName("group"); - var first = true; - - sql.Append("("); - - foreach (ITag tag in tags) + if (first) { - if (first) - { - first = false; - } - else - { - sql.Append(" UNION "); - } - - // HACK: SQLite (or rather SQL server setup was a hack) - if (SqlContext.DatabaseType.IsSqlServer()) - { - sql.Append("SELECT N'"); - } - else - { - sql.Append("SELECT '"); - } - - sql.Append(SqlSyntax.EscapeString(tag.Text)); - sql.Append("' AS tag, '"); - sql.Append(SqlSyntax.EscapeString(tag.Group)); - sql.Append("' AS "); - sql.Append(group); - sql.Append(" , "); - if (tag.LanguageId.HasValue) - { - sql.Append(tag.LanguageId); - } - else - { - sql.Append("NULL"); - } - - sql.Append(" AS languageId"); + first = false; + } + else + { + sql.Append(" UNION "); } - sql.Append(") AS tagSet"); + // HACK: SQLite (or rather SQL server setup was a hack) + if (SqlContext.DatabaseType.IsSqlServer()) + { + sql.Append("SELECT N'"); + } + else + { + sql.Append("SELECT '"); + } - return sql.ToString(); + sql.Append(SqlSyntax.EscapeString(tag.Text)); + sql.Append("' AS tag, '"); + sql.Append(SqlSyntax.EscapeString(tag.Group)); + sql.Append("' AS "); + sql.Append(group); + sql.Append(" , "); + if (tag.LanguageId.HasValue) + { + sql.Append(tag.LanguageId); + } + else + { + sql.Append("NULL"); + } + + sql.Append(" AS languageId"); } - // used to run Distinct() on tags - private class TagComparer : IEqualityComparer - { - public bool Equals(ITag? x, ITag? y) => - ReferenceEquals(x, y) // takes care of both being null - || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); + sql.Append(") AS tagSet"); - public int GetHashCode(ITag obj) + return sql.ToString(); + } + + // used to run Distinct() on tags + private class TagComparer : IEqualityComparer + { + public bool Equals(ITag? x, ITag? y) => + ReferenceEquals(x, y) // takes care of both being null + || (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId); + + public int GetHashCode(ITag obj) + { + unchecked { - unchecked - { - var h = obj.Text?.GetHashCode() ?? 1; - h = (h * 397) ^ obj.Group?.GetHashCode() ?? 0; - h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); - return h; - } + var h = obj.Text.GetHashCode(); + h = (h * 397) ^ obj.Group.GetHashCode(); + h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0); + return h; } } + } - #endregion + #endregion - #region Queries + #region Queries - // TODO: consider caching implications - // add lookups for parentId or path (ie get content in tag group, that are descendants of x) + // TODO: consider caching implications + // add lookups for parentId or path (ie get content in tag group, that are descendants of x) - // ReSharper disable once ClassNeverInstantiated.Local - // ReSharper disable UnusedAutoPropertyAccessor.Local - private class TaggedEntityDto + // ReSharper disable once ClassNeverInstantiated.Local + // ReSharper disable UnusedAutoPropertyAccessor.Local + private class TaggedEntityDto + { + public int NodeId { get; set; } + public string? PropertyTypeAlias { get; set; } + public int PropertyTypeId { get; set; } + public int TagId { get; set; } + public string TagText { get; set; } = null!; + public string TagGroup { get; set; } = null!; + public int? TagLanguage { get; set; } + } + // ReSharper restore UnusedAutoPropertyAccessor.Local + + /// + public TaggedEntity? GetTaggedEntityByKey(Guid key) + { + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql + .Where(dto => dto.UniqueId == key); + + return Map(Database.Fetch(sql)).FirstOrDefault(); + } + + /// + public TaggedEntity? GetTaggedEntityById(int id) + { + Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + + sql = sql + .Where(dto => dto.NodeId == id); + + return Map(Database.Fetch(sql)).FirstOrDefault(); + } + + /// + public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, + string? culture = null) + { + Sql sql = GetTaggedEntitiesSql(objectType, culture); + + sql = sql + .Where(x => x.Group == group); + + return Map(Database.Fetch(sql)); + } + + /// + public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, + string? group = null, string? culture = null) + { + Sql sql = GetTaggedEntitiesSql(objectType, culture); + + sql = sql + .Where(dto => dto.Text == tag); + + if (group.IsNullOrWhiteSpace() == false) { - public int NodeId { get; set; } - public string? PropertyTypeAlias { get; set; } - public int PropertyTypeId { get; set; } - public int TagId { get; set; } - public string TagText { get; set; } = null!; - public string TagGroup { get; set; } = null!; - public int? TagLanguage { get; set; } - } - // ReSharper restore UnusedAutoPropertyAccessor.Local - - /// - public TaggedEntity? GetTaggedEntityByKey(Guid key) - { - Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); - sql = sql - .Where(dto => dto.UniqueId == key); - - return Map(Database.Fetch(sql)).FirstOrDefault(); + .Where(dto => dto.Group == group); } - /// - public TaggedEntity? GetTaggedEntityById(int id) - { - Sql sql = GetTaggedEntitiesSql(TaggableObjectTypes.All, "*"); + return Map(Database.Fetch(sql)); + } + private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string? culture) + { + Sql sql = Sql() + .Select(x => Alias(x.NodeId, "NodeId")) + .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), + x => Alias(x.Id, "PropertyTypeId")) + .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), + x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) + .From() + .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) + .InnerJoin() + .On((rel, content) => rel.NodeId == content.NodeId) + .InnerJoin() + .On((rel, prop) => rel.PropertyTypeId == prop.Id) + .InnerJoin().On((content, node) => content.NodeId == node.NodeId); + + if (culture == null) + { sql = sql - .Where(dto => dto.NodeId == id); - - return Map(Database.Fetch(sql)).FirstOrDefault(); + .Where(dto => dto.LanguageId == null); } - - /// - public IEnumerable GetTaggedEntitiesByTagGroup(TaggableObjectTypes objectType, string group, - string? culture = null) + else if (culture != "*") { - Sql sql = GetTaggedEntitiesSql(objectType, culture); - sql = sql - .Where(x => x.Group == group); - - return Map(Database.Fetch(sql)); + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) + .Where(x => x.IsoCode == culture); } - /// - public IEnumerable GetTaggedEntitiesByTag(TaggableObjectTypes objectType, string tag, - string? group = null, string? culture = null) + if (objectType != TaggableObjectTypes.All) { - Sql sql = GetTaggedEntitiesSql(objectType, culture); - - sql = sql - .Where(dto => dto.Text == tag); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return Map(Database.Fetch(sql)); + Guid nodeObjectType = GetNodeObjectType(objectType); + sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); } - private Sql GetTaggedEntitiesSql(TaggableObjectTypes objectType, string? culture) + return sql; + } + + private static IEnumerable Map(IEnumerable dtos) => + dtos.GroupBy(x => x.NodeId).Select(dtosForNode => { - Sql sql = Sql() - .Select(x => Alias(x.NodeId, "NodeId")) - .AndSelect(x => Alias(x.Alias, "PropertyTypeAlias"), - x => Alias(x.Id, "PropertyTypeId")) - .AndSelect(x => Alias(x.Id, "TagId"), x => Alias(x.Text, "TagText"), - x => Alias(x.Group, "TagGroup"), x => Alias(x.LanguageId, "TagLanguage")) - .From() - .InnerJoin().On((tag, rel) => tag.Id == rel.TagId) - .InnerJoin() - .On((rel, content) => rel.NodeId == content.NodeId) - .InnerJoin() - .On((rel, prop) => rel.PropertyTypeId == prop.Id) - .InnerJoin().On((content, node) => content.NodeId == node.NodeId); - - if (culture == null) + var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => { - sql = sql - .Where(dto => dto.LanguageId == null); - } - else if (culture != "*") - { - sql = sql - .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id) - .Where(x => x.IsoCode == culture); - } - - if (objectType != TaggableObjectTypes.All) - { - Guid nodeObjectType = GetNodeObjectType(objectType); - sql = sql.Where(dto => dto.NodeObjectType == nodeObjectType); - } - - return sql; - } - - private static IEnumerable Map(IEnumerable dtos) => - dtos.GroupBy(x => x.NodeId).Select(dtosForNode => - { - var taggedProperties = dtosForNode.GroupBy(x => x.PropertyTypeId).Select(dtosForProperty => + string? propertyTypeAlias = null; + var tags = dtosForProperty.Select(dto => { - string? propertyTypeAlias = null; - var tags = dtosForProperty.Select(dto => - { - propertyTypeAlias = dto.PropertyTypeAlias; - return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); - }).ToList(); - return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); + propertyTypeAlias = dto.PropertyTypeAlias; + return new Tag(dto.TagId, dto.TagGroup, dto.TagText, dto.TagLanguage); }).ToList(); - - return new TaggedEntity(dtosForNode.Key, taggedProperties); + return new TaggedProperty(dtosForProperty.Key, propertyTypeAlias, tags); }).ToList(); - /// - public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, - string? culture = null) + return new TaggedEntity(dtosForNode.Key, taggedProperties); + }).ToList(); + + /// + public IEnumerable GetTagsForEntityType(TaggableObjectTypes objectType, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture, true); + + AddTagsSqlWhere(sql, culture); + + if (objectType != TaggableObjectTypes.All) { - Sql sql = GetTagsSql(culture, true); - - AddTagsSqlWhere(sql, culture); - - if (objectType != TaggableObjectTypes.All) - { - Guid nodeObjectType = GetNodeObjectType(objectType); - sql = sql - .Where(dto => dto.NodeObjectType == nodeObjectType); - } - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - + Guid nodeObjectType = GetNodeObjectType(objectType); sql = sql - .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); - - return ExecuteTagsQuery(sql); + .Where(dto => dto.NodeObjectType == nodeObjectType); } - /// - public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + if (group.IsNullOrWhiteSpace() == false) { - Sql sql = GetTagsSql(culture); - - AddTagsSqlWhere(sql, culture); - sql = sql - .Where(dto => dto.NodeId == contentId); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); + .Where(dto => dto.Group == group); } - /// - public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) - { - Sql sql = GetTagsSql(culture); + sql = sql + .GroupBy(x => x.Id, x => x.Text, x => x.Group, x => x.LanguageId); - AddTagsSqlWhere(sql, culture); - - sql = sql - .Where(dto => dto.UniqueId == contentId); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); - } - - /// - public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, - string? culture = null) - { - Sql sql = GetTagsSql(culture); - - sql = sql - .InnerJoin() - .On((prop, rel) => prop.Id == rel.PropertyTypeId) - .Where(x => x.NodeId == contentId) - .Where(x => x.Alias == propertyTypeAlias); - - AddTagsSqlWhere(sql, culture); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); - } - - /// - public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, - string? culture = null) - { - Sql sql = GetTagsSql(culture); - - sql = sql - .InnerJoin() - .On((prop, rel) => prop.Id == rel.PropertyTypeId) - .Where(dto => dto.UniqueId == contentId) - .Where(dto => dto.Alias == propertyTypeAlias); - - AddTagsSqlWhere(sql, culture); - - if (group.IsNullOrWhiteSpace() == false) - { - sql = sql - .Where(dto => dto.Group == group); - } - - return ExecuteTagsQuery(sql); - } - - private Sql GetTagsSql(string? culture, bool withGrouping = false) - { - Sql sql = Sql() - .Select(); - - if (withGrouping) - { - sql = sql - .AndSelectCount("NodeCount"); - } - - sql = sql - .From() - .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) - .InnerJoin() - .On((content, rel) => content.NodeId == rel.NodeId) - .InnerJoin().On((node, content) => node.NodeId == content.NodeId); - - if (culture != null && culture != "*") - { - sql = sql - .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); - } - - return sql; - } - - private Sql AddTagsSqlWhere(Sql sql, string? culture) - { - if (culture == null) - { - sql = sql - .Where(dto => dto.LanguageId == null); - } - else if (culture != "*") - { - sql = sql - .Where(x => x.IsoCode == culture); - } - - return sql; - } - - private IEnumerable ExecuteTagsQuery(Sql sql) => - Database.Fetch(sql).Select(TagFactory.BuildEntity); - - private Guid GetNodeObjectType(TaggableObjectTypes type) - { - switch (type) - { - case TaggableObjectTypes.Content: - return Constants.ObjectTypes.Document; - case TaggableObjectTypes.Media: - return Constants.ObjectTypes.Media; - case TaggableObjectTypes.Member: - return Constants.ObjectTypes.Member; - default: - throw new ArgumentOutOfRangeException(nameof(type)); - } - } - - #endregion + return ExecuteTagsQuery(sql); } + + /// + public IEnumerable GetTagsForEntity(int contentId, string? group = null, string? culture = null) + { + Sql sql = GetTagsSql(culture); + + AddTagsSqlWhere(sql, culture); + + sql = sql + .Where(dto => dto.NodeId == contentId); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + /// + public IEnumerable GetTagsForEntity(Guid contentId, string? group = null, string? culture = null) + { + Sql sql = GetTagsSql(culture); + + AddTagsSqlWhere(sql, culture); + + sql = sql + .Where(dto => dto.UniqueId == contentId); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + /// + public IEnumerable GetTagsForProperty(int contentId, string propertyTypeAlias, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture); + + sql = sql + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(x => x.NodeId == contentId) + .Where(x => x.Alias == propertyTypeAlias); + + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + /// + public IEnumerable GetTagsForProperty(Guid contentId, string propertyTypeAlias, string? group = null, + string? culture = null) + { + Sql sql = GetTagsSql(culture); + + sql = sql + .InnerJoin() + .On((prop, rel) => prop.Id == rel.PropertyTypeId) + .Where(dto => dto.UniqueId == contentId) + .Where(dto => dto.Alias == propertyTypeAlias); + + AddTagsSqlWhere(sql, culture); + + if (group.IsNullOrWhiteSpace() == false) + { + sql = sql + .Where(dto => dto.Group == group); + } + + return ExecuteTagsQuery(sql); + } + + private Sql GetTagsSql(string? culture, bool withGrouping = false) + { + Sql sql = Sql() + .Select(); + + if (withGrouping) + { + sql = sql + .AndSelectCount("NodeCount"); + } + + sql = sql + .From() + .InnerJoin().On((rel, tag) => tag.Id == rel.TagId) + .InnerJoin() + .On((content, rel) => content.NodeId == rel.NodeId) + .InnerJoin().On((node, content) => node.NodeId == content.NodeId); + + if (culture != null && culture != "*") + { + sql = sql + .InnerJoin().On((tag, lang) => tag.LanguageId == lang.Id); + } + + return sql; + } + + private Sql AddTagsSqlWhere(Sql sql, string? culture) + { + if (culture == null) + { + sql = sql + .Where(dto => dto.LanguageId == null); + } + else if (culture != "*") + { + sql = sql + .Where(x => x.IsoCode == culture); + } + + return sql; + } + + private IEnumerable ExecuteTagsQuery(Sql sql) => + Database.Fetch(sql).Select(TagFactory.BuildEntity); + + private Guid GetNodeObjectType(TaggableObjectTypes type) + { + switch (type) + { + case TaggableObjectTypes.Content: + return Constants.ObjectTypes.Document; + case TaggableObjectTypes.Media: + return Constants.ObjectTypes.Media; + case TaggableObjectTypes.Member: + return Constants.ObjectTypes.Member; + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs index 4dc3bb71f2..1a0a90269c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; @@ -19,613 +15,617 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the Template Repository +/// +internal class TemplateRepository : EntityRepositoryBase, ITemplateRepository { - /// - /// Represents the Template Repository - /// - internal class TemplateRepository : EntityRepositoryBase, ITemplateRepository + private readonly IIOHelper _ioHelper; + private readonly IShortStringHelper _shortStringHelper; + private readonly IViewHelper _viewHelper; + private readonly IFileSystem? _viewsFileSystem; + + public TemplateRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, + FileSystems fileSystems, IIOHelper ioHelper, IShortStringHelper shortStringHelper, IViewHelper viewHelper) + : base(scopeAccessor, cache, logger) { - private readonly IIOHelper _ioHelper; - private readonly IShortStringHelper _shortStringHelper; - private readonly IFileSystem? _viewsFileSystem; - private readonly IViewHelper _viewHelper; + _ioHelper = ioHelper; + _shortStringHelper = shortStringHelper; + _viewsFileSystem = fileSystems.MvcViewsFileSystem; + _viewHelper = viewHelper; + } - public TemplateRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, FileSystems fileSystems, IIOHelper ioHelper, IShortStringHelper shortStringHelper, IViewHelper viewHelper) - : base(scopeAccessor, cache, logger) + public Stream GetFileContentStream(string filepath) + { + IFileSystem? fileSystem = GetFileSystem(filepath); + if (fileSystem?.FileExists(filepath) == false) { - _ioHelper = ioHelper; - _shortStringHelper = shortStringHelper; - _viewsFileSystem = fileSystems.MvcViewsFileSystem; - _viewHelper = viewHelper; + return Stream.Null; } - protected override IRepositoryCachePolicy CreateCachePolicy() => - new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, GetEntityId, /*expires:*/ false); - - #region Overrides of RepositoryBase - - protected override ITemplate? PerformGet(int id) => - //use the underlying GetAll which will force cache all templates - base.GetMany()?.FirstOrDefault(x => x.Id == id); - - protected override IEnumerable PerformGetAll(params int[]? ids) + try { - Sql sql = GetBaseQuery(false); + return fileSystem!.OpenFile(filepath); + } + catch + { + return Stream.Null; // deal with race conds + } + } - if (ids?.Any() ?? false) - { - sql.Where("umbracoNode.id in (@ids)", new { ids }); - } - else - { - sql.Where(x => x.NodeObjectType == NodeObjectTypeId); - } + public void SetFileContent(string filepath, Stream content) => + GetFileSystem(filepath)?.AddFile(filepath, content, true); - List dtos = Database.Fetch(sql); + public long GetFileSize(string filename) + { + IFileSystem? fileSystem = GetFileSystem(filename); + if (fileSystem?.FileExists(filename) == false) + { + return -1; + } - if (dtos.Count == 0) - { - return Enumerable.Empty(); - } + try + { + return fileSystem!.GetSize(filename); + } + catch + { + return -1; // deal with race conds + } + } - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - IUmbracoEntity[] childIds = (ids?.Any() ?? false - ? GetAxisDefinitions(dtos.ToArray()) - : dtos.Select(x => new EntitySlim + protected override IRepositoryCachePolicy CreateCachePolicy() => + new FullDataSetRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, + GetEntityId, /*expires:*/ false); + + private IEnumerable GetAxisDefinitions(params TemplateDto[] templates) + { + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + Sql childIdsSql = SqlContext.Sql() + .Select("nodeId,alias,parentID") + .From() + .InnerJoin() + .On(dto => dto.NodeId, dto => dto.NodeId) + //lookup axis's + .Where( + "umbracoNode." + SqlContext.SqlSyntax.GetQuotedColumnName("id") + + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)", + new { - Id = x.NodeId, - ParentId = x.NodeDto.ParentId, - Name = x.Alias - })).ToArray(); - - return dtos.Select(d => MapFromDto(d, childIds)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - Sql sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - Sql sql = translator.Translate(); - - List dtos = Database.Fetch(sql); - - if (dtos.Count == 0) - { - return Enumerable.Empty(); - } - - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - IUmbracoEntity[] childIds = GetAxisDefinitions(dtos.ToArray()).ToArray(); - - return dtos.Select(d => MapFromDto(d, childIds)); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) - { - Sql sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(r => r.Select(x => x.NodeDto)); - - sql - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - - return sql; - } - - protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", - "UPDATE " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentVersion + " SET templateId = NULL WHERE templateId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.DocumentType + " WHERE templateNodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Template + " WHERE nodeId = @id", - "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" - }; - return list; - } - - protected Guid NodeObjectTypeId => Cms.Core.Constants.ObjectTypes.Template; - - protected override void PersistNewItem(ITemplate entity) - { - EnsureValidAlias(entity); - - //Save to db - var template = (Template)entity; - template.AddingEntity(); - - TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, template.Id); - - //Create the (base) node data - umbracoNode - NodeDto nodeDto = dto.NodeDto; - nodeDto.Path = "-1," + dto.NodeDto.NodeId; - int o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); - - //Update with new correct path - ITemplate? parent = Get(template.MasterTemplateId!.Value); - if (parent != null) - { - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - } - else - { - nodeDto.Path = "-1," + dto.NodeDto.NodeId; - } - Database.Update(nodeDto); - - //Insert template dto - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Update entity with correct values - template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - template.Path = nodeDto.Path; - - //now do the file work - SaveFile(template); - - template.ResetDirtyProperties(); - - // ensure that from now on, content is lazy-loaded - if (template.GetFileContent == null) - { - template.GetFileContent = file => GetFileContent((Template) file, false); - } - } - - protected override void PersistUpdatedItem(ITemplate entity) - { - EnsureValidAlias(entity); - - //store the changed alias if there is one for use with updating files later - string? originalAlias = entity.Alias; - if (entity.IsPropertyDirty("Alias")) - { - //we need to check what it currently is before saving and remove that file - ITemplate? current = Get(entity.Id); - originalAlias = current?.Alias; - } - - var template = (Template)entity; - - if (entity.IsPropertyDirty("MasterTemplateId")) - { - ITemplate? parent = Get(template.MasterTemplateId!.Value); - if (parent != null) - { - entity.Path = string.Concat(parent.Path, ",", entity.Id); - } - else - { - //this means that the master template has been removed, so we need to reset the template's - //path to be at the root - entity.Path = string.Concat("-1,", entity.Id); - } - } - - //Get TemplateDto from db to get the Primary key of the entity - TemplateDto templateDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { entity.Id }); - - //Save updated entity to db - template.UpdateDate = DateTime.Now; - TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); - Database.Update(dto.NodeDto); - Database.Update(dto); - - //re-update if this is a master template, since it could have changed! - IEnumerable axisDefs = GetAxisDefinitions(dto); - template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); - - //now do the file work - SaveFile((Template) entity, originalAlias); - - entity.ResetDirtyProperties(); - - // ensure that from now on, content is lazy-loaded - if (template.GetFileContent == null) - { - template.GetFileContent = file => GetFileContent((Template) file, false); - } - } - - private void SaveFile(Template template, string? originalAlias = null) - { - string? content; - - if (template is TemplateOnDisk templateOnDisk && templateOnDisk.IsOnDisk) - { - // if "template on disk" load content from disk - content = _viewHelper.GetFileContents(template); - } - else - { - // else, create or write template.Content to disk - content = originalAlias == null - ? _viewHelper.CreateView(template, true) - : _viewHelper.UpdateViewFile(template, originalAlias); - } - - // once content has been set, "template on disk" are not "on disk" anymore - template.Content = content; - SetVirtualPath(template); - } - - protected override void PersistDeletedItem(ITemplate entity) - { - string[] deletes = GetDeleteClauses().ToArray(); - - var descendants = GetDescendants(entity.Id).ToList(); - - //change the order so it goes bottom up! (deepest level first) - descendants.Reverse(); - - //delete the hierarchy - foreach (ITemplate descendant in descendants) - { - foreach (string delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(descendant) }); - } - } - - //now we can delete this one - foreach (string delete in deletes) - { - Database.Execute(delete, new { id = GetEntityId(entity) }); - } - - string viewName = string.Concat(entity.Alias, ".cshtml"); - _viewsFileSystem?.DeleteFile(viewName); - - entity.DeleteDate = DateTime.Now; - } - - #endregion - - private IEnumerable GetAxisDefinitions(params TemplateDto[] templates) - { - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - Sql childIdsSql = SqlContext.Sql() - .Select("nodeId,alias,parentID") - .From() - .InnerJoin() - .On(dto => dto.NodeId, dto => dto.NodeId) - //lookup axis's - .Where("umbracoNode." + SqlContext.SqlSyntax.GetQuotedColumnName("id") + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)", - new {parentIds = templates.Select(x => x.NodeDto.ParentId), childIds = templates.Select(x => x.NodeId)}); - - var childIds = Database.Fetch(childIdsSql) - .Select(x => new EntitySlim - { - Id = x.NodeId, - ParentId = x.ParentId, - Name = x.Alias + parentIds = templates.Select(x => x.NodeDto.ParentId), + childIds = templates.Select(x => x.NodeId) }); - return childIds; + IEnumerable childIds = Database.Fetch(childIdsSql) + .Select(x => new EntitySlim {Id = x.NodeId, ParentId = x.ParentId, Name = x.Alias}); + + return childIds; + } + + /// + /// Maps from a dto to an ITemplate + /// + /// + /// + /// This is a collection of template definitions ... either all templates, or the collection of child templates and + /// it's parent template + /// + /// + private ITemplate MapFromDto(TemplateDto dto, IUmbracoEntity[] axisDefinitions) + { + Template template = TemplateFactory.BuildEntity(_shortStringHelper, dto, axisDefinitions, + file => GetFileContent((Template)file, false)); + + if (dto.NodeDto.ParentId > 0) + { + IUmbracoEntity? masterTemplate = axisDefinitions.FirstOrDefault(x => x.Id == dto.NodeDto.ParentId); + if (masterTemplate != null) + { + template.MasterTemplateAlias = masterTemplate.Name; + template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); + } } - /// - /// Maps from a dto to an ITemplate - /// - /// - /// - /// This is a collection of template definitions ... either all templates, or the collection of child templates and it's parent template - /// - /// - private ITemplate MapFromDto(TemplateDto dto, IUmbracoEntity[] axisDefinitions) + // get the infos (update date and virtual path) that will change only if + // path changes - but do not get content, will get loaded only when required + GetFileContent(template, true); + + // reset dirty initial properties (U4-1946) + template.ResetDirtyProperties(false); + + return template; + } + + private void SetVirtualPath(ITemplate template) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) { - - Template template = TemplateFactory.BuildEntity(_shortStringHelper, dto, axisDefinitions, file => GetFileContent((Template) file, false)); - - if (dto.NodeDto.ParentId > 0) + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) { - IUmbracoEntity? masterTemplate = axisDefinitions.FirstOrDefault(x => x.Id == dto.NodeDto.ParentId); - if (masterTemplate != null) - { - template.MasterTemplateAlias = masterTemplate.Name; - template.MasterTemplateId = new Lazy(() => dto.NodeDto.ParentId); - } + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; } - // get the infos (update date and virtual path) that will change only if - // path changes - but do not get content, will get loaded only when required - GetFileContent(template, true); - - // reset dirty initial properties (U4-1946) - template.ResetDirtyProperties(false); - - return template; + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) + { + template.VirtualPath = _viewsFileSystem.GetUrl(path); + return; + } + } + else + { + // we know the path already + template.VirtualPath = _viewsFileSystem?.GetUrl(path); } - private void SetVirtualPath(ITemplate template) + template.VirtualPath = string.Empty; // file not found... + } + + private string? GetFileContent(ITemplate template, bool init) + { + var path = template.OriginalPath; + if (string.IsNullOrWhiteSpace(path)) { - string path = template.OriginalPath; - if (string.IsNullOrWhiteSpace(path)) + // we need to discover the path + path = string.Concat(template.Alias, ".cshtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) { - // we need to discover the path - path = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - template.VirtualPath = _viewsFileSystem.GetUrl(path); - return; - } - path = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - template.VirtualPath = _viewsFileSystem.GetUrl(path); - return; - } - } - else - { - // we know the path already - template.VirtualPath = _viewsFileSystem?.GetUrl(path); - } - - template.VirtualPath = string.Empty; // file not found... - } - - private string? GetFileContent(ITemplate template, bool init) - { - string path = template.OriginalPath; - if (string.IsNullOrWhiteSpace(path)) - { - // we need to discover the path - path = string.Concat(template.Alias, ".cshtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - return GetFileContent(template, _viewsFileSystem, path, init); - } - - path = string.Concat(template.Alias, ".vbhtml"); - if (_viewsFileSystem?.FileExists(path) ?? false) - { - return GetFileContent(template, _viewsFileSystem, path, init); - } - } - else - { - // we know the path already return GetFileContent(template, _viewsFileSystem, path, init); } - template.VirtualPath = string.Empty; // file not found... - - return string.Empty; + path = string.Concat(template.Alias, ".vbhtml"); + if (_viewsFileSystem?.FileExists(path) ?? false) + { + return GetFileContent(template, _viewsFileSystem, path, init); + } + } + else + { + // we know the path already + return GetFileContent(template, _viewsFileSystem, path, init); } - private string? GetFileContent(ITemplate template, IFileSystem? fs, string filename, bool init) + template.VirtualPath = string.Empty; // file not found... + + return string.Empty; + } + + private string? GetFileContent(ITemplate template, IFileSystem? fs, string filename, bool init) + { + // do not update .UpdateDate as that would make it dirty (side-effect) + // unless initializing, because we have to do it once + if (init && fs is not null) { - // do not update .UpdateDate as that would make it dirty (side-effect) - // unless initializing, because we have to do it once - if (init && fs is not null) - { - template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; - } - - // TODO: see if this could enable us to update UpdateDate without messing with change tracking - // and then we'd want to do it for scripts, stylesheets and partial views too (ie files) - // var xtemplate = template as Template; - // xtemplate.DisableChangeTracking(); - // template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; - // xtemplate.EnableChangeTracking(); - - template.VirtualPath = fs?.GetUrl(filename); - - return init ? null : GetFileContent(fs, filename); + template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; } - private string? GetFileContent(IFileSystem? fs, string filename) - { - if (fs is null) - { - return null; - } + // TODO: see if this could enable us to update UpdateDate without messing with change tracking + // and then we'd want to do it for scripts, stylesheets and partial views too (ie files) + // var xtemplate = template as Template; + // xtemplate.DisableChangeTracking(); + // template.UpdateDate = fs.GetLastModified(filename).UtcDateTime; + // xtemplate.EnableChangeTracking(); - using Stream stream = fs.OpenFile(filename); - using var reader = new StreamReader(stream, Encoding.UTF8, true); - return reader.ReadToEnd(); + template.VirtualPath = fs?.GetUrl(filename); + + return init ? null : GetFileContent(fs, filename); + } + + private string? GetFileContent(IFileSystem? fs, string filename) + { + if (fs is null) + { + return null; } - public Stream GetFileContentStream(string filepath) - { - IFileSystem? fileSystem = GetFileSystem(filepath); - if (fileSystem?.FileExists(filepath) == false) - { - return Stream.Null; - } + using Stream stream = fs.OpenFile(filename); + using var reader = new StreamReader(stream, Encoding.UTF8, true); + return reader.ReadToEnd(); + } - try + private IFileSystem? GetFileSystem(string filepath) + { + var ext = Path.GetExtension(filepath); + IFileSystem? fs; + switch (ext) + { + case ".cshtml": + case ".vbhtml": + fs = _viewsFileSystem; + break; + default: + throw new Exception("Unsupported extension " + ext + "."); + } + + return fs; + } + + /// + /// Ensures that there are not duplicate aliases and if so, changes it to be a numbered version and also verifies the + /// length + /// + /// + private void EnsureValidAlias(ITemplate template) + { + //ensure unique alias + template.Alias = template.Alias.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias); + + if (template.Alias.Length > 100) + { + template.Alias = template.Alias.Substring(0, 95); + } + + if (AliasAlreadExists(template)) + { + template.Alias = EnsureUniqueAlias(template, 1); + } + } + + private bool AliasAlreadExists(ITemplate template) + { + Sql sql = GetBaseQuery(true) + .Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); + var count = Database.ExecuteScalar(sql); + return count > 0; + } + + private string EnsureUniqueAlias(ITemplate template, int attempts) + { + // TODO: This is ported from the old data layer... pretty crap way of doing this but it works for now. + if (AliasAlreadExists(template)) + { + return template.Alias + attempts; + } + + attempts++; + return EnsureUniqueAlias(template, attempts); + } + + #region Overrides of RepositoryBase + + protected override ITemplate? PerformGet(int id) => + //use the underlying GetAll which will force cache all templates + GetMany().FirstOrDefault(x => x.Id == id); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false); + + if (ids?.Any() ?? false) + { + sql.Where("umbracoNode.id in (@ids)", new {ids}); + } + else + { + sql.Where(x => x.NodeObjectType == NodeObjectTypeId); + } + + List dtos = Database.Fetch(sql); + + if (dtos.Count == 0) + { + return Enumerable.Empty(); + } + + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + IUmbracoEntity[] childIds = (ids?.Any() ?? false + ? GetAxisDefinitions(dtos.ToArray()) + : dtos.Select(x => new EntitySlim {Id = x.NodeId, ParentId = x.NodeDto.ParentId, Name = x.Alias})) + .ToArray(); + + return dtos.Select(d => MapFromDto(d, childIds)); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + + List dtos = Database.Fetch(sql); + + if (dtos.Count == 0) + { + return Enumerable.Empty(); + } + + //look up the simple template definitions that have a master template assigned, this is used + // later to populate the template item's properties + IUmbracoEntity[] childIds = GetAxisDefinitions(dtos.ToArray()).ToArray(); + + return dtos.Select(d => MapFromDto(d, childIds)); + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(r => r.Select(x => x.NodeDto)); + + sql + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId); + + return sql; + } + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.Node}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM " + Constants.DatabaseSchema.Tables.User2NodeNotify + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2Node + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.UserGroup2NodePermission + " WHERE nodeId = @id", + "UPDATE " + Constants.DatabaseSchema.Tables.DocumentVersion + + " SET templateId = NULL WHERE templateId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentType + " WHERE templateNodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Template + " WHERE nodeId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Node + " WHERE id = @id" + }; + return list; + } + + protected Guid NodeObjectTypeId => Constants.ObjectTypes.Template; + + protected override void PersistNewItem(ITemplate entity) + { + EnsureValidAlias(entity); + + //Save to db + var template = (Template)entity; + template.AddingEntity(); + + TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, template.Id); + + //Create the (base) node data - umbracoNode + NodeDto nodeDto = dto.NodeDto; + nodeDto.Path = "-1," + dto.NodeDto.NodeId; + var o = Database.IsNew(nodeDto) ? Convert.ToInt32(Database.Insert(nodeDto)) : Database.Update(nodeDto); + + //Update with new correct path + ITemplate? parent = Get(template.MasterTemplateId!.Value); + if (parent != null) + { + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + } + else + { + nodeDto.Path = "-1," + dto.NodeDto.NodeId; + } + + Database.Update(nodeDto); + + //Insert template dto + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Update entity with correct values + template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + template.Path = nodeDto.Path; + + //now do the file work + SaveFile(template); + + template.ResetDirtyProperties(); + + // ensure that from now on, content is lazy-loaded + if (template.GetFileContent == null) + { + template.GetFileContent = file => GetFileContent((Template)file, false); + } + } + + protected override void PersistUpdatedItem(ITemplate entity) + { + EnsureValidAlias(entity); + + //store the changed alias if there is one for use with updating files later + var originalAlias = entity.Alias; + if (entity.IsPropertyDirty("Alias")) + { + //we need to check what it currently is before saving and remove that file + ITemplate? current = Get(entity.Id); + originalAlias = current?.Alias; + } + + var template = (Template)entity; + + if (entity.IsPropertyDirty("MasterTemplateId")) + { + ITemplate? parent = Get(template.MasterTemplateId!.Value); + if (parent != null) { - return fileSystem!.OpenFile(filepath); + entity.Path = string.Concat(parent.Path, ",", entity.Id); } - catch + else { - return Stream.Null; // deal with race conds + //this means that the master template has been removed, so we need to reset the template's + //path to be at the root + entity.Path = string.Concat("-1,", entity.Id); } } - public void SetFileContent(string filepath, Stream content) => GetFileSystem(filepath)?.AddFile(filepath, content, true); + //Get TemplateDto from db to get the Primary key of the entity + TemplateDto templateDto = Database.SingleOrDefault("WHERE nodeId = @Id", new {entity.Id}); - public long GetFileSize(string filename) + //Save updated entity to db + template.UpdateDate = DateTime.Now; + TemplateDto dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); + Database.Update(dto.NodeDto); + Database.Update(dto); + + //re-update if this is a master template, since it could have changed! + IEnumerable axisDefs = GetAxisDefinitions(dto); + template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); + + //now do the file work + SaveFile((Template)entity, originalAlias); + + entity.ResetDirtyProperties(); + + // ensure that from now on, content is lazy-loaded + if (template.GetFileContent == null) { - IFileSystem? fileSystem = GetFileSystem(filename); - if (fileSystem?.FileExists(filename) == false) - { - return -1; - } + template.GetFileContent = file => GetFileContent((Template)file, false); + } + } - try + private void SaveFile(Template template, string? originalAlias = null) + { + string? content; + + if (template is TemplateOnDisk templateOnDisk && templateOnDisk.IsOnDisk) + { + // if "template on disk" load content from disk + content = _viewHelper.GetFileContents(template); + } + else + { + // else, create or write template.Content to disk + content = originalAlias == null + ? _viewHelper.CreateView(template, true) + : _viewHelper.UpdateViewFile(template, originalAlias); + } + + // once content has been set, "template on disk" are not "on disk" anymore + template.Content = content; + SetVirtualPath(template); + } + + protected override void PersistDeletedItem(ITemplate entity) + { + var deletes = GetDeleteClauses().ToArray(); + + var descendants = GetDescendants(entity.Id).ToList(); + + //change the order so it goes bottom up! (deepest level first) + descendants.Reverse(); + + //delete the hierarchy + foreach (ITemplate descendant in descendants) + { + foreach (var delete in deletes) { - return fileSystem!.GetSize(filename); - } - catch - { - return -1; // deal with race conds + Database.Execute(delete, new {id = GetEntityId(descendant)}); } } - private IFileSystem? GetFileSystem(string filepath) + //now we can delete this one + foreach (var delete in deletes) { - string ext = Path.GetExtension(filepath); - IFileSystem? fs; - switch (ext) - { - case ".cshtml": - case ".vbhtml": - fs = _viewsFileSystem; - break; - default: - throw new Exception("Unsupported extension " + ext + "."); - } - return fs; + Database.Execute(delete, new {id = GetEntityId(entity)}); } - #region Implementation of ITemplateRepository + var viewName = string.Concat(entity.Alias, ".cshtml"); + _viewsFileSystem?.DeleteFile(viewName); - public ITemplate? Get(string? alias) => GetAll(alias)?.FirstOrDefault(); + entity.DeleteDate = DateTime.Now; + } - public IEnumerable GetAll(params string?[] aliases) + #endregion + + #region Implementation of ITemplateRepository + + public ITemplate? Get(string? alias) => GetAll(alias).FirstOrDefault(); + + public IEnumerable GetAll(params string?[] aliases) + { + //We must call the base (normal) GetAll method + // which is cached. This is a specialized method and unfortunately with the params[] it + // overlaps with the normal GetAll method. + if (aliases.Any() == false) { - //We must call the base (normal) GetAll method - // which is cached. This is a specialized method and unfortunately with the params[] it - // overlaps with the normal GetAll method. - if (aliases.Any() == false) - { - return base.GetMany(); - } - - //return from base.GetAll, this is all cached - return base.GetMany().Where(x => aliases.WhereNotNull().InvariantContains(x.Alias)); + return GetMany(); } - public IEnumerable GetChildren(int masterTemplateId) + //return from base.GetAll, this is all cached + return GetMany().Where(x => aliases.WhereNotNull().InvariantContains(x.Alias)); + } + + public IEnumerable GetChildren(int masterTemplateId) + { + //return from base.GetAll, this is all cached + ITemplate[] all = GetMany().ToArray(); + + if (masterTemplateId <= 0) { - //return from base.GetAll, this is all cached - ITemplate[] all = base.GetMany().ToArray(); + return all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); + } - if (masterTemplateId <= 0) - { - return all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); - } + ITemplate? parent = all.FirstOrDefault(x => x.Id == masterTemplateId); + if (parent == null) + { + return Enumerable.Empty(); + } + IEnumerable children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); + return children; + } + + public IEnumerable GetDescendants(int masterTemplateId) + { + //return from base.GetAll, this is all cached + ITemplate[] all = GetMany().ToArray(); + var descendants = new List(); + if (masterTemplateId > 0) + { ITemplate? parent = all.FirstOrDefault(x => x.Id == masterTemplateId); if (parent == null) { return Enumerable.Empty(); } - IEnumerable children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); - return children; + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); } - - public IEnumerable GetDescendants(int masterTemplateId) + else { - //return from base.GetAll, this is all cached - ITemplate[]? all = base.GetMany()?.ToArray(); - var descendants = new List(); - if (masterTemplateId > 0) + descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); + foreach (ITemplate parent in descendants) { - ITemplate? parent = all?.FirstOrDefault(x => x.Id == masterTemplateId); - if (parent == null) - { - return Enumerable.Empty(); - } - //recursively add all children with a level AddChildren(all, descendants, parent.Alias); } - else - { - if (all is not null) - { - descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); - foreach (ITemplate parent in descendants) - { - //recursively add all children with a level - AddChildren(all, descendants, parent.Alias); - } - } - } - - //return the list - it will be naturally ordered by level - return descendants; } - private void AddChildren(ITemplate[]? all, List descendants, string masterAlias) - { - ITemplate[]? c = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); - if (c is null || c.Any() == false) - { - return; - } - descendants.AddRange(c); + //return the list - it will be naturally ordered by level + return descendants; + } - //recurse through all children - foreach (ITemplate child in c) - { - AddChildren(all, descendants, child.Alias); - } + private void AddChildren(ITemplate[]? all, List descendants, string masterAlias) + { + ITemplate[]? c = all?.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); + if (c is null || c.Any() == false) + { + return; } - #endregion + descendants.AddRange(c); - /// - /// Ensures that there are not duplicate aliases and if so, changes it to be a numbered version and also verifies the length - /// - /// - private void EnsureValidAlias(ITemplate template) + //recurse through all children + foreach (ITemplate child in c) { - //ensure unique alias - template.Alias = template.Alias.ToCleanString(_shortStringHelper, CleanStringType.UnderscoreAlias); - - if (template.Alias.Length > 100) - { - template.Alias = template.Alias.Substring(0, 95); - } - - if (AliasAlreadExists(template)) - { - template.Alias = EnsureUniqueAlias(template, 1); - } - } - - private bool AliasAlreadExists(ITemplate template) - { - Sql sql = GetBaseQuery(true).Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); - int count = Database.ExecuteScalar(sql); - return count > 0; - } - - private string EnsureUniqueAlias(ITemplate template, int attempts) - { - // TODO: This is ported from the old data layer... pretty crap way of doing this but it works for now. - if (AliasAlreadExists(template)) - { - return template.Alias + attempts; - } - - attempts++; - return EnsureUniqueAlias(template, attempts); + AddChildren(all, descendants, child.Alias); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index 18098623cf..f6d35abf37 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -1,181 +1,193 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq.Expressions; using NPoco; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TrackedReferencesRepository : ITrackedReferencesRepository { - internal class TrackedReferencesRepository : ITrackedReferencesRepository + private readonly IScopeAccessor _scopeAccessor; + + public TrackedReferencesRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + /// + /// Gets a page of items used in any kind of relation from selected integer ids. + /// + public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) { - private readonly IScopeAccessor _scopeAccessor; + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType") + .On((left, right) => left.RelationType == right.Id, "r", "umbracoRelationType") + .InnerJoin("cn").On( + (r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), "r", "cn", "umbracoRelationType") + .InnerJoin("pn").On( + (r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), "r", "pn", "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, "pn", "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, "c", "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, "ct", "ctn"); - public TrackedReferencesRepository(IScopeAccessor scopeAccessor) + if (ids.Any()) { - _scopeAccessor = scopeAccessor; + sql = sql?.Where(x => ids.Contains(x.NodeId), "pn"); } - /// - /// Gets a page of items used in any kind of relation from selected integer ids. - /// - public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + if (filterMustBeIsDependency) { - var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[pn].[id] as nodeId", - "[pn].[uniqueId] as nodeKey", - "[pn].[text] as nodeName", - "[pn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn"); - - if (ids.Any()) - { - sql = sql?.Where(x => ids.Contains(x.NodeId), "pn"); - } - - if (filterMustBeIsDependency) - { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); - } - - // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); - - var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult?.TotalItems); - - return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); } - /// - /// Gets a page of the descending items that have any references, given a parent id. - /// - public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) - { - var syntax = _scopeAccessor.AmbientScope?.Database.SqlContext.SqlSyntax; + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias); - // Gets the path of the parent with ",%" added - var subsubQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() - .Select(syntax?.GetConcat("[node].[path]", "',%'")) - .From("node") - .Where(x => x.NodeId == parentId, "node"); + Page? pagedResult = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult?.TotalItems); - // Gets the descendants of the parent node - Sql? subQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() - .Select(x => x.NodeId) - .From() - .WhereLike(x => x.Path, subsubQuery); - - // Get all relations where parent is in the sub query - var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[pn].[id] as nodeId", - "[pn].[uniqueId] as nodeKey", - "[pn].[text] as nodeName", - "[pn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "pn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") - .WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "pn"); - - if (filterMustBeIsDependency) - { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); - } - - // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); - - var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult?.TotalItems); - - return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); - } - - /// - /// Gets a page of items which are in relation with the current item. - /// Basically, shows the items which depend on the current item. - /// - public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) - { - var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( - "[cn].[id] as nodeId", - "[cn].[uniqueId] as nodeKey", - "[cn].[text] as nodeName", - "[cn].[nodeObjectType] as nodeObjectType", - "[ct].[icon] as contentTypeIcon", - "[ct].[alias] as contentTypeAlias", - "[ctn].[text] as contentTypeName", - "[umbracoRelationType].[alias] as relationTypeAlias", - "[umbracoRelationType].[name] as relationTypeName", - "[umbracoRelationType].[isDependency] as relationTypeIsDependency", - "[umbracoRelationType].[dual] as relationTypeIsBidirectional") - .From("r") - .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight: "umbracoRelationType") - .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), aliasLeft: "r", aliasRight: "cn", aliasOther: "umbracoRelationType") - .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight: "pn", aliasOther: "cn") - .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "cn", aliasRight: "c") - .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft: "c", aliasRight: "ct") - .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft: "ct", aliasRight: "ctn") - .Where(x => x.NodeId == id, "pn") - .Where(x => x.ChildId == id || x.ParentId == id, "r"); // This last Where is purely to help SqlServer make a smarter query plan. More info https://github.com/umbraco/Umbraco-CMS/issues/12190 - - if (filterMustBeIsDependency) - { - sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); - } - - // Ordering is required for paging - sql = sql?.OrderBy(x => x.Alias); - - var pagedResult = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); - totalRecords = Convert.ToInt32(pagedResult?.TotalItems); - - return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); - } - - private RelationItem MapDtoToEntity(RelationItemDto dto) - { - return new RelationItem() - { - NodeId = dto.ChildNodeId, - NodeKey = dto.ChildNodeKey, - NodeType = ObjectTypes.GetUdiType(dto.ChildNodeObjectType), - NodeName = dto.ChildNodeName, - RelationTypeName = dto.RelationTypeName, - RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, - RelationTypeIsDependency = dto.RelationTypeIsDependency, - ContentTypeAlias = dto.ChildContentTypeAlias, - ContentTypeIcon = dto.ChildContentTypeIcon, - ContentTypeName = dto.ChildContentTypeName, - }; - } + return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); } + + /// + /// Gets a page of the descending items that have any references, given a parent id. + /// + public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + { + ISqlSyntaxProvider? syntax = _scopeAccessor.AmbientScope?.Database.SqlContext.SqlSyntax; + + // Gets the path of the parent with ",%" added + Sql? subsubQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(syntax?.GetConcat("[node].[path]", "',%'")) + .From("node") + .Where(x => x.NodeId == parentId, "node"); + + // Gets the descendants of the parent node + Sql? subQuery = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .WhereLike(x => x.Path, subsubQuery); + + // Get all relations where parent is in the sub query + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType") + .On((left, right) => left.RelationType == right.Id, "r", "umbracoRelationType") + .InnerJoin("cn").On( + (r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), "r", "cn", "umbracoRelationType") + .InnerJoin("pn").On( + (r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), "r", "pn", "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, "pn", "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, "c", "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, "ct", "ctn") + .WhereIn((Expression>)(x => x.NodeId), subQuery, "pn"); + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias); + + Page? pagedResult = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult?.TotalItems); + + return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + } + + /// + /// Gets a page of items which are in relation with the current item. + /// Basically, shows the items which depend on the current item. + /// + public IEnumerable GetPagedRelationsForItem(int id, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + { + Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct( + "[cn].[id] as nodeId", + "[cn].[uniqueId] as nodeKey", + "[cn].[text] as nodeName", + "[cn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType") + .On((left, right) => left.RelationType == right.Id, "r", "umbracoRelationType") + .InnerJoin("cn").On( + (r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId)), "r", "cn", "umbracoRelationType") + .InnerJoin("pn").On( + (r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), "r", "pn", "cn") + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, "cn", "c") + .LeftJoin("ct") + .On((left, right) => left.ContentTypeId == right.NodeId, "c", "ct") + .LeftJoin("ctn") + .On((left, right) => left.NodeId == right.NodeId, "ct", "ctn") + .Where(x => x.NodeId == id, "pn") + .Where( + x => x.ChildId == id || x.ParentId == id, + "r"); // This last Where is purely to help SqlServer make a smarter query plan. More info https://github.com/umbraco/Umbraco-CMS/issues/12190 + + if (filterMustBeIsDependency) + { + sql = sql?.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + + // Ordering is required for paging + sql = sql?.OrderBy(x => x.Alias); + + Page? pagedResult = + _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult?.TotalItems); + + return pagedResult?.Items.Select(MapDtoToEntity) ?? Enumerable.Empty(); + } + + private RelationItem MapDtoToEntity(RelationItemDto dto) => + new RelationItem + { + NodeId = dto.ChildNodeId, + NodeKey = dto.ChildNodeKey, + NodeType = ObjectTypes.GetUdiType(dto.ChildNodeObjectType), + NodeName = dto.ChildNodeName, + RelationTypeName = dto.RelationTypeName, + RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, + RelationTypeIsDependency = dto.RelationTypeIsDependency, + ContentTypeAlias = dto.ChildContentTypeAlias, + ContentTypeIcon = dto.ChildContentTypeIcon, + ContentTypeName = dto.ChildContentTypeName, + }; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs index a5a3fe4f21..fa981fa539 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TupleExtensions.cs @@ -1,19 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +internal static class TupleExtensions { - static class TupleExtensions - { - public static IEnumerable Map(this Tuple, List> t, Func relator) - { - return t.Item1.Zip(t.Item2, relator); - } + public static IEnumerable Map( + this Tuple, List> t, + Func relator) => t.Item1.Zip(t.Item2, relator); -// public static IEnumerable Map(this Tuple, List, List> t, Func relator) -// { -// return t.Item1.Zip(t.Item2, t.Item3, relator); -// } - } + // public static IEnumerable Map(this Tuple, List, List> t, Func relator) + // { + // return t.Item1.Zip(t.Item2, t.Item3, relator); + // } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs index b74063df9b..bde8bc8ba2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NPoco; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Querying; @@ -13,128 +10,129 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +internal class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository { - internal class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository + public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) { - public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, - ILogger logger) - : base(scopeAccessor, cache, logger) + } + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) => + await DeleteUserLoginsAsync(userOrMemberKey, null); + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey, string? providerName) + { + Sql sql = Sql() + .Delete() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey); + + if (providerName is not null) { + sql = sql.Where(x => x.ProviderName == providerName); } + var deletedRows = await Database.ExecuteAsync(sql); - protected override Sql GetBaseQuery(bool isCount) + return deletedRows > 0; + } + + public async Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey) + { + Sql sql = Sql() + .Select() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey); + List? dtos = await Database.FetchAsync(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } + + protected override Sql GetBaseQuery(bool isCount) + { + Sql sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql.From(); + + return sql; + } + + protected override string GetBaseWhereClause() => + Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id"; + + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); + + protected override ITwoFactorLogin? PerformGet(int id) + { + Sql sql = GetBaseQuery(false).Where(x => x.Id == id); + TwoFactorLoginDto? dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + List? dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map).WhereNotNull(); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); + return Database.Fetch(sql).Select(Map).WhereNotNull(); + } + + protected override void PersistNewItem(ITwoFactorLogin entity) + { + TwoFactorLoginDto? dto = Map(entity); + Database.Insert(dto); + } + + protected override void PersistUpdatedItem(ITwoFactorLogin entity) + { + TwoFactorLoginDto? dto = Map(entity); + if (dto is not null) { - var sql = SqlContext.Sql(); - - sql = isCount - ? sql.SelectCount() - : sql.Select(); - - sql.From(); - - return sql; - } - - protected override string GetBaseWhereClause() => - Core.Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id"; - - protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); - - protected override ITwoFactorLogin? PerformGet(int id) - { - var sql = GetBaseQuery(false).Where(x => x.Id == id); - var dto = Database.Fetch(sql).FirstOrDefault(); - return dto == null ? null : Map(dto); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); - var dtos = Database.Fetch(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - return Database.Fetch(sql).Select(Map).WhereNotNull(); - } - - protected override void PersistNewItem(ITwoFactorLogin entity) - { - var dto = Map(entity); - Database.Insert(dto); - } - - protected override void PersistUpdatedItem(ITwoFactorLogin entity) - { - var dto = Map(entity); - if (dto is not null) - { - Database.Update(dto); - } - } - - private static TwoFactorLoginDto? Map(ITwoFactorLogin entity) - { - if (entity == null) return null; - - return new TwoFactorLoginDto - { - Id = entity.Id, - UserOrMemberKey = entity.UserOrMemberKey, - ProviderName = entity.ProviderName, - Secret = entity.Secret, - }; - } - - private static ITwoFactorLogin? Map(TwoFactorLoginDto dto) - { - if (dto == null) return null; - - return new TwoFactorLogin - { - Id = dto.Id, - UserOrMemberKey = dto.UserOrMemberKey, - ProviderName = dto.ProviderName, - Secret = dto.Secret, - }; - } - - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) - { - return await DeleteUserLoginsAsync(userOrMemberKey, null); - } - - public async Task DeleteUserLoginsAsync(Guid userOrMemberKey, string? providerName) - { - var sql = Sql() - .Delete() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey); - - if (providerName is not null) - { - sql = sql.Where(x => x.ProviderName == providerName); - } - - var deletedRows = await Database.ExecuteAsync(sql); - - return deletedRows > 0; - } - - public async Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey) - { - var sql = Sql() - .Select() - .From() - .Where(x => x.UserOrMemberKey == userOrMemberKey); - var dtos = await Database.FetchAsync(sql); - return dtos.WhereNotNull().Select(Map).WhereNotNull(); + Database.Update(dto); } } + + private static TwoFactorLoginDto? Map(ITwoFactorLogin? entity) + { + if (entity == null) + { + return null; + } + + return new TwoFactorLoginDto + { + Id = entity.Id, + UserOrMemberKey = entity.UserOrMemberKey, + ProviderName = entity.ProviderName, + Secret = entity.Secret, + }; + } + + private static ITwoFactorLogin? Map(TwoFactorLoginDto? dto) + { + if (dto == null) + { + return null; + } + + return new TwoFactorLogin + { + Id = dto.Id, + UserOrMemberKey = dto.UserOrMemberKey, + ProviderName = dto.ProviderName, + Secret = dto.Secret, + }; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 7a3ca69db1..5ff1fa83f3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -17,455 +14,442 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +/// +/// Represents the UserGroupRepository for doing CRUD operations for +/// +public class UserGroupRepository : EntityRepositoryBase, IUserGroupRepository { - /// - /// Represents the UserGroupRepository for doing CRUD operations for - /// - public class UserGroupRepository : EntityRepositoryBase, IUserGroupRepository + private readonly PermissionRepository _permissionRepository; + private readonly IShortStringHelper _shortStringHelper; + private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; + + public UserGroupRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper) + : base(scopeAccessor, appCaches, logger) { - private readonly IShortStringHelper _shortStringHelper; - private readonly UserGroupWithUsersRepository _userGroupWithUsersRepository; - private readonly PermissionRepository _permissionRepository; + _shortStringHelper = shortStringHelper; + _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); + _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, loggerFactory.CreateLogger>()); + } - public UserGroupRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, ILoggerFactory loggerFactory, IShortStringHelper shortStringHelper) - : base(scopeAccessor, appCaches, logger) + public IUserGroup? Get(string alias) + { + try { - _shortStringHelper = shortStringHelper; - _userGroupWithUsersRepository = new UserGroupWithUsersRepository(this, scopeAccessor, appCaches, loggerFactory.CreateLogger()); - _permissionRepository = new PermissionRepository(scopeAccessor, appCaches, loggerFactory.CreateLogger>()); - } - - - public static string GetByAliasCacheKey(string alias) - { - return CacheKeys.UserGroupGetByAliasCacheKeyPrefix + alias; - } - - public IUserGroup? Get(string alias) - { - try + // need to do a simple query to get the id - put this cache + var id = IsolatedCache.GetCacheItem(GetByAliasCacheKey(alias), () => { - //need to do a simple query to get the id - put this cache - var id = IsolatedCache.GetCacheItem(GetByAliasCacheKey(alias), () => + var groupId = + Database.ExecuteScalar("SELECT id FROM umbracoUserGroup WHERE userGroupAlias=@alias", new { alias }); + if (groupId.HasValue == false) { - var groupId = Database.ExecuteScalar("SELECT id FROM umbracoUserGroup WHERE userGroupAlias=@alias", new { alias }); - if (groupId.HasValue == false) throw new InvalidOperationException("No group found with alias " + alias); - return groupId.Value; - }); + throw new InvalidOperationException("No group found with alias " + alias); + } - //return from the normal method which will cache - return Get(id); - } - catch (InvalidOperationException) + return groupId.Value; + }); + + // return from the normal method which will cache + return Get(id); + } + catch (InvalidOperationException) + { + // if this is caught it's because we threw this in the caching method + return null; + } + } + + public IEnumerable GetGroupsAssignedToSection(string sectionAlias) + { + // Here we're building up a query that looks like this, a sub query is required because the resulting structure + // needs to still contain all of the section rows per user group. + + // SELECT * + // FROM [umbracoUserGroup] + // LEFT JOIN [umbracoUserGroup2App] + // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + // WHERE umbracoUserGroup.id IN (SELECT umbracoUserGroup.id + // FROM [umbracoUserGroup] + // LEFT JOIN [umbracoUserGroup2App] + // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] + // WHERE umbracoUserGroup2App.app = 'content') + Sql sql = GetBaseQuery(QueryType.Many); + Sql innerSql = GetBaseQuery(QueryType.Ids); + innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); + sql.Where($"umbracoUserGroup.id IN ({innerSql.SQL})"); + AppendGroupBy(sql); + + return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } + + public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds) => + _userGroupWithUsersRepository.Save(new UserGroupWithUsers(userGroup, userIds)); + + /// + /// Gets explicitly defined permissions for the group for specified entities + /// + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + public EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds) => + _permissionRepository.GetPermissionsForEntities(groupIds, entityIds); + + /// + /// Gets explicit and default permissions (if requested) permissions for the group for specified entities + /// + /// + /// + /// If true will include the group's default permissions if no permissions are + /// explicitly assigned + /// + /// Array of entity Ids, if empty will return permissions for the group for all entities + public EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds) + { + if (groups == null) + { + throw new ArgumentNullException(nameof(groups)); + } + + var groupIds = groups.Select(x => x.Id).ToArray(); + EntityPermissionCollection explicitPermissions = GetPermissions(groupIds, nodeIds); + var result = new EntityPermissionCollection(explicitPermissions); + + // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults + if (fallbackToDefaultPermissions) + { + // if no node ids are passed in, then we need to determine the node ids for the explicit permissions set + nodeIds = nodeIds.Length == 0 + ? explicitPermissions.Select(x => x.EntityId).Distinct().ToArray() + : nodeIds; + + // if there are still no nodeids we can just exit + if (nodeIds.Length == 0) { - //if this is caught it's because we threw this in the caching method - return null; + return result; } - } - public IEnumerable GetGroupsAssignedToSection(string sectionAlias) - { - //Here we're building up a query that looks like this, a sub query is required because the resulting structure - // needs to still contain all of the section rows per user group. - - //SELECT * - //FROM [umbracoUserGroup] - //LEFT JOIN [umbracoUserGroup2App] - //ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] - //WHERE umbracoUserGroup.id IN (SELECT umbracoUserGroup.id - // FROM [umbracoUserGroup] - // LEFT JOIN [umbracoUserGroup2App] - // ON [umbracoUserGroup].[id] = [umbracoUserGroup2App].[user] - // WHERE umbracoUserGroup2App.app = 'content') - - var sql = GetBaseQuery(QueryType.Many); - var innerSql = GetBaseQuery(QueryType.Ids); - innerSql.Where("umbracoUserGroup2App.app = " + SqlSyntax.GetQuotedValue(sectionAlias)); - sql.Where($"umbracoUserGroup.id IN ({innerSql.SQL})"); - AppendGroupBy(sql); - - return Database.Fetch(sql).Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } - - public void AddOrUpdateGroupWithUsers(IUserGroup userGroup, int[]? userIds) - { - _userGroupWithUsersRepository.Save(new UserGroupWithUsers(userGroup, userIds)); - } - - - /// - /// Gets explicitly defined permissions for the group for specified entities - /// - /// - /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissions(int[] groupIds, params int[] entityIds) - { - return _permissionRepository.GetPermissionsForEntities(groupIds, entityIds); - } - - /// - /// Gets explicit and default permissions (if requested) permissions for the group for specified entities - /// - /// - /// If true will include the group's default permissions if no permissions are explicitly assigned - /// Array of entity Ids, if empty will return permissions for the group for all entities - public EntityPermissionCollection GetPermissions(IReadOnlyUserGroup[]? groups, bool fallbackToDefaultPermissions, params int[] nodeIds) - { - if (groups == null) throw new ArgumentNullException(nameof(groups)); - - var groupIds = groups.Select(x => x.Id).ToArray(); - var explicitPermissions = GetPermissions(groupIds, nodeIds); - var result = new EntityPermissionCollection(explicitPermissions); - - // If requested, and no permissions are assigned to a particular node, then we will fill in those permissions with the group's defaults - if (fallbackToDefaultPermissions) + foreach (IReadOnlyUserGroup group in groups) { - //if no node ids are passed in, then we need to determine the node ids for the explicit permissions set - nodeIds = nodeIds.Length == 0 - ? explicitPermissions.Select(x => x.EntityId).Distinct().ToArray() - : nodeIds; - - //if there are still no nodeids we can just exit - if (nodeIds.Length == 0) - return result; - - foreach (var group in groups) + foreach (var nodeId in nodeIds) { - foreach (var nodeId in nodeIds) - { - // TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be - // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not - // going to be included in the result! + // TODO: We could/should change the EntityPermissionsCollection into a KeyedCollection and they key could be + // a struct of the nodeid + groupid so then we don't actually allocate this class just to check if it's not + // going to be included in the result! + var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions?.ToArray() ?? Array.Empty(), true); - var defaultPermission = new EntityPermission(group.Id, nodeId, group.Permissions?.ToArray() ?? Array.Empty(), isDefaultPermissions: true); - //Since this is a hashset, this will not add anything that already exists by group/node combination - result.Add(defaultPermission); - } + // Since this is a hashset, this will not add anything that already exists by group/node combination + result.Add(defaultPermission); } } - - return result; } - /// - /// Replaces the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of If nothing is specified all permissions are removed. - /// Specify the nodes to replace permissions for. - public void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) + return result; + } + + /// + /// Replaces the same permission set for a single group to any number of entities + /// + /// Id of group + /// + /// Permissions as enumerable list of If nothing is specified all permissions + /// are removed. + /// + /// Specify the nodes to replace permissions for. + public void ReplaceGroupPermissions(int groupId, IEnumerable? permissions, params int[] entityIds) => + _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); + + /// + /// Assigns the same permission set for a single group to any number of entities + /// + /// Id of group + /// Permissions as enumerable list of + /// Specify the nodes to replace permissions for + public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) => + _permissionRepository.AssignPermission(groupId, permission, entityIds); + + public static string GetByAliasCacheKey(string alias) => CacheKeys.UserGroupGetByAliasCacheKeyPrefix + alias; + + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsers : EntityBase + { + public UserGroupWithUsers(IUserGroup userGroup, int[]? userIds) { - _permissionRepository.ReplacePermissions(groupId, permissions, entityIds); + UserGroup = userGroup; + UserIds = userIds; } - /// - /// Assigns the same permission set for a single group to any number of entities - /// - /// Id of group - /// Permissions as enumerable list of - /// Specify the nodes to replace permissions for - public void AssignGroupPermission(int groupId, char permission, params int[] entityIds) + public override bool HasIdentity => UserGroup.HasIdentity; + + public IUserGroup UserGroup { get; } + + public int[]? UserIds { get; } + } + + /// + /// used to persist a user group with associated users at once + /// + private class UserGroupWithUsersRepository : EntityRepositoryBase + { + private readonly UserGroupRepository _userGroupRepo; + + public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) + : base(scopeAccessor, cache, logger) => + _userGroupRepo = userGroupRepo; + + protected override void PersistNewItem(UserGroupWithUsers entity) { - _permissionRepository.AssignPermission(groupId, permission, entityIds); - } + // save the user group + _userGroupRepo.PersistNewItem(entity.UserGroup); - #region Overrides of RepositoryBase - - protected override IUserGroup? PerformGet(int id) - { - var sql = GetBaseQuery(QueryType.Single); - sql.Where(GetBaseWhereClause(), new { id = id }); - - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references - - var dto = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql).FirstOrDefault(); - - if (dto == null) - return null; - - var userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto); - return userGroup; - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var sql = GetBaseQuery(QueryType.Many); - - if (ids?.Any() ?? false) - sql.WhereIn(x => x.Id, ids); - else - sql.Where(x => x.Id >= 0); - - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references - - var dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); - return dtos.Select(x=>UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlClause = GetBaseQuery(QueryType.Many); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate(); - - AppendGroupBy(sql); - sql.OrderBy(x => x.Id); // required for references - - var dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); - return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); - } - - #endregion - - #region Overrides of EntityRepositoryBase - - protected Sql GetBaseQuery(QueryType type) - { - var sql = Sql(); - var addFrom = false; - - switch (type) + if (entity.UserIds == null) { - case QueryType.Count: - sql - .SelectCount() - .From(); - break; - case QueryType.Ids: - sql - .Select(x => x.Id); - addFrom = true; - break; - case QueryType.Single: - case QueryType.Many: - sql - .Select(r => - r.Select(x => x.UserGroup2AppDtos), - s => s.Append($", COUNT({sql.Columns(x => x.UserId)}) AS {SqlSyntax.GetQuotedColumnName("UserCount")}")); - addFrom = true; - break; - default: - throw new NotSupportedException(type.ToString()); + return; } - if (addFrom) - sql - .From() - .LeftJoin() - .On(left => left.Id, right => right.UserGroupId) - .LeftJoin() - .On(left => left.UserGroupId, right => right.Id); - - return sql; + // now the user association + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); } - protected override Sql GetBaseQuery(bool isCount) + protected override void PersistUpdatedItem(UserGroupWithUsers entity) { - return GetBaseQuery(isCount ? QueryType.Count : QueryType.Many); - } + // save the user group + _userGroupRepo.PersistUpdatedItem(entity.UserGroup); - private static void AppendGroupBy(Sql sql) - { - sql - .GroupBy(x => x.CreateDate, x => x.Icon, x => x.Id, x => x.StartContentId, x => x.StartMediaId, - x => x.UpdateDate, x => x.Alias, x => x.DefaultPermissions, x => x.Name) - .AndBy(x => x.AppAlias, x => x.UserGroupId); - } - - protected override string GetBaseWhereClause() - { - return $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id", - "DELETE FROM umbracoUserGroup WHERE id = @id" - }; - return list; - } - - protected override void PersistNewItem(IUserGroup entity) - { - entity.AddingEntity(); - - var userGroupDto = UserGroupFactory.BuildDto(entity); - - var id = Convert.ToInt32(Database.Insert(userGroupDto)); - entity.Id = id; - - PersistAllowedSections(entity); - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IUserGroup entity) - { - entity.UpdatingEntity(); - - var userGroupDto = UserGroupFactory.BuildDto(entity); - - Database.Update(userGroupDto); - - PersistAllowedSections(entity); - - entity.ResetDirtyProperties(); - } - - private void PersistAllowedSections(IUserGroup entity) - { - var userGroup = entity; - - // First delete all - Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); - - // Then re-add any associated with the group - foreach (var app in userGroup.AllowedSections) + if (entity.UserIds == null) { - var dto = new UserGroup2AppDto - { - UserGroupId = userGroup.Id, - AppAlias = app - }; + return; + } + + // now the user association + RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); + } + + /// + /// Adds a set of users to a group, first removing any that exist + /// + /// Id of group + /// Ids of users + private void RefreshUsersInGroup(int groupId, int[] userIds) + { + RemoveAllUsersFromGroup(groupId); + AddUsersToGroup(groupId, userIds); + } + + /// + /// Removes all users from a group + /// + /// Id of group + private void RemoveAllUsersFromGroup(int groupId) => + Database.Delete("WHERE userGroupId = @groupId", new { groupId }); + + /// + /// Adds a set of users to a group + /// + /// Id of group + /// Ids of users + private void AddUsersToGroup(int groupId, int[] userIds) + { + foreach (var userId in userIds) + { + var dto = new User2UserGroupDto { UserGroupId = groupId, UserId = userId }; Database.Insert(dto); } } + #region Not implemented (don't need to for the purposes of this repo) + + protected override UserGroupWithUsers PerformGet(int id) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetAll(params int[]? ids) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable PerformGetByQuery(IQuery query) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override Sql GetBaseQuery(bool isCount) => + throw new InvalidOperationException("This method won't be implemented."); + + protected override string GetBaseWhereClause() => + throw new InvalidOperationException("This method won't be implemented."); + + protected override IEnumerable GetDeleteClauses() => + throw new InvalidOperationException("This method won't be implemented."); + #endregion + } - /// - /// used to persist a user group with associated users at once - /// - private class UserGroupWithUsers : EntityBase + #region Overrides of RepositoryBase + + protected override IUserGroup? PerformGet(int id) + { + Sql sql = GetBaseQuery(QueryType.Single); + sql.Where(GetBaseWhereClause(), new { id }); + + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references + + UserGroupDto? dto = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql).FirstOrDefault(); + + if (dto == null) { - public UserGroupWithUsers(IUserGroup userGroup, int[]? userIds) - { - UserGroup = userGroup; - UserIds = userIds; - } - - public override bool HasIdentity => UserGroup.HasIdentity; - - public IUserGroup UserGroup { get; } - public int[]? UserIds { get; } + return null; } - /// - /// used to persist a user group with associated users at once - /// - private class UserGroupWithUsersRepository : EntityRepositoryBase + IUserGroup userGroup = UserGroupFactory.BuildEntity(_shortStringHelper, dto); + return userGroup; + } + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + Sql sql = GetBaseQuery(QueryType.Many); + + if (ids?.Any() ?? false) { - private readonly UserGroupRepository _userGroupRepo; + sql.WhereIn(x => x.Id, ids); + } + else + { + sql.Where(x => x.Id >= 0); + } - public UserGroupWithUsersRepository(UserGroupRepository userGroupRepo, IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) - : base(scopeAccessor, cache, logger) - { - _userGroupRepo = userGroupRepo; - } + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references - #region Not implemented (don't need to for the purposes of this repo) + List? dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } - protected override UserGroupWithUsers PerformGet(int id) - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected override IEnumerable PerformGetByQuery(IQuery query) + { + Sql sqlClause = GetBaseQuery(QueryType.Many); + var translator = new SqlTranslator(sqlClause, query); + Sql sql = translator.Translate(); - protected override IEnumerable PerformGetAll(params int[]? ids) - { - throw new InvalidOperationException("This method won't be implemented."); - } + AppendGroupBy(sql); + sql.OrderBy(x => x.Id); // required for references - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new InvalidOperationException("This method won't be implemented."); - } + List? dtos = Database.FetchOneToMany(x => x.UserGroup2AppDtos, sql); + return dtos.Select(x => UserGroupFactory.BuildEntity(_shortStringHelper, x)); + } - protected override Sql GetBaseQuery(bool isCount) - { - throw new InvalidOperationException("This method won't be implemented."); - } + #endregion - protected override string GetBaseWhereClause() - { - throw new InvalidOperationException("This method won't be implemented."); - } + #region Overrides of EntityRepositoryBase - protected override IEnumerable GetDeleteClauses() - { - throw new InvalidOperationException("This method won't be implemented."); - } + protected Sql GetBaseQuery(QueryType type) + { + Sql sql = Sql(); + var addFrom = false; - #endregion + switch (type) + { + case QueryType.Count: + sql + .SelectCount() + .From(); + break; + case QueryType.Ids: + sql + .Select(x => x.Id); + addFrom = true; + break; + case QueryType.Single: + case QueryType.Many: + sql.Select(r => r.Select(x => x.UserGroup2AppDtos), s => s.Append($", COUNT({sql.Columns(x => x.UserId)}) AS {SqlSyntax.GetQuotedColumnName("UserCount")}")); + addFrom = true; + break; + default: + throw new NotSupportedException(type.ToString()); + } - protected override void PersistNewItem(UserGroupWithUsers entity) - { - //save the user group - _userGroupRepo.PersistNewItem(entity.UserGroup); + if (addFrom) + { + sql + .From() + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId) + .LeftJoin() + .On(left => left.UserGroupId, right => right.Id); + } - if (entity.UserIds == null) - return; + return sql; + } - //now the user association - RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); - } + protected override Sql GetBaseQuery(bool isCount) => + GetBaseQuery(isCount ? QueryType.Count : QueryType.Many); - protected override void PersistUpdatedItem(UserGroupWithUsers entity) - { - //save the user group - _userGroupRepo.PersistUpdatedItem(entity.UserGroup); + private static void AppendGroupBy(Sql sql) => + sql.GroupBy( + x => x.CreateDate, + x => x.Icon, + x => x.Id, + x => x.StartContentId, + x => x.StartMediaId, + x => x.UpdateDate, + x => x.Alias, + x => x.DefaultPermissions, + x => x.Name) + .AndBy(x => x.AppAlias, x => x.UserGroupId); - if (entity.UserIds == null) - return; + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.UserGroup}.id = @id"; - //now the user association - RefreshUsersInGroup(entity.UserGroup.Id, entity.UserIds); - } + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoUser2UserGroup WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2Node WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE userGroupId = @id", + "DELETE FROM umbracoUserGroup WHERE id = @id", + }; + return list; + } - /// - /// Adds a set of users to a group, first removing any that exist - /// - /// Id of group - /// Ids of users - private void RefreshUsersInGroup(int groupId, int[] userIds) - { - RemoveAllUsersFromGroup(groupId); - AddUsersToGroup(groupId, userIds); - } + protected override void PersistNewItem(IUserGroup entity) + { + entity.AddingEntity(); - /// - /// Removes all users from a group - /// - /// Id of group - private void RemoveAllUsersFromGroup(int groupId) - { - Database.Delete("WHERE userGroupId = @groupId", new { groupId }); - } + UserGroupDto userGroupDto = UserGroupFactory.BuildDto(entity); - /// - /// Adds a set of users to a group - /// - /// Id of group - /// Ids of users - private void AddUsersToGroup(int groupId, int[] userIds) - { - foreach (var userId in userIds) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupId, - UserId = userId, - }; - Database.Insert(dto); - } - } + var id = Convert.ToInt32(Database.Insert(userGroupDto)); + entity.Id = id; + + PersistAllowedSections(entity); + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IUserGroup entity) + { + entity.UpdatingEntity(); + + UserGroupDto userGroupDto = UserGroupFactory.BuildDto(entity); + + Database.Update(userGroupDto); + + PersistAllowedSections(entity); + + entity.ResetDirtyProperties(); + } + + private void PersistAllowedSections(IUserGroup entity) + { + IUserGroup userGroup = entity; + + // First delete all + Database.Delete("WHERE UserGroupId = @UserGroupId", new { UserGroupId = userGroup.Id }); + + // Then re-add any associated with the group + foreach (var app in userGroup.AllowedSections) + { + var dto = new UserGroup2AppDto { UserGroupId = userGroup.Id, AppAlias = app }; + Database.Insert(dto); } } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs index 5f44dc6781..2df2d77a02 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -22,163 +20,171 @@ using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +/// +/// Represents the UserRepository for doing CRUD operations for +/// +internal class UserRepository : EntityRepositoryBase, IUserRepository { + private readonly IMapperCollection _mapperCollection; + private readonly GlobalSettings _globalSettings; + private readonly UserPasswordConfigurationSettings _passwordConfiguration; + private readonly IJsonSerializer _jsonSerializer; + private readonly IRuntimeState _runtimeState; + private string? _passwordConfigJson; + private bool _passwordConfigInitialized; + private readonly object _sqliteValidateSessionLock = new(); + /// - /// Represents the UserRepository for doing CRUD operations for + /// Initializes a new instance of the class. /// - internal class UserRepository : EntityRepositoryBase, IUserRepository + /// The scope accessor. + /// The application caches. + /// The logger. + /// + /// A dictionary specifying the configuration for user passwords. If this is null then no + /// password configuration will be persisted or read. + /// + /// The global settings. + /// The password configuration. + /// The JSON serializer. + /// State of the runtime. + /// + /// mapperCollection + /// or + /// globalSettings + /// or + /// passwordConfiguration + /// + public UserRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + IMapperCollection mapperCollection, + IOptions globalSettings, + IOptions passwordConfiguration, + IJsonSerializer jsonSerializer, + IRuntimeState runtimeState) + : base(scopeAccessor, appCaches, logger) { - private readonly IMapperCollection _mapperCollection; - private readonly GlobalSettings _globalSettings; - private readonly UserPasswordConfigurationSettings _passwordConfiguration; - private readonly IJsonSerializer _jsonSerializer; - private readonly IRuntimeState _runtimeState; - private string? _passwordConfigJson; - private bool _passwordConfigInitialized; - private readonly object _sqliteValidateSessionLock = new(); + _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); + _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); + _passwordConfiguration = + passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); + _jsonSerializer = jsonSerializer; + _runtimeState = runtimeState; + } - /// - /// Initializes a new instance of the class. - /// - /// The scope accessor. - /// The application caches. - /// The logger. - /// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read. - /// The global settings. - /// The password configuration. - /// The JSON serializer. - /// State of the runtime. - /// mapperCollection - /// or - /// globalSettings - /// or - /// passwordConfiguration - public UserRepository( - IScopeAccessor scopeAccessor, - AppCaches appCaches, - ILogger logger, - IMapperCollection mapperCollection, - IOptions globalSettings, - IOptions passwordConfiguration, - IJsonSerializer jsonSerializer, - IRuntimeState runtimeState) - : base(scopeAccessor, appCaches, logger) + /// + /// Returns a serialized dictionary of the password configuration that is stored against the user in the database + /// + private string? DefaultPasswordConfigJson + { + get { - _mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection)); - _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); - _passwordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration)); - _jsonSerializer = jsonSerializer; - _runtimeState = runtimeState; - } - - /// - /// Returns a serialized dictionary of the password configuration that is stored against the user in the database - /// - private string? DefaultPasswordConfigJson - { - get + if (_passwordConfigInitialized) { - if (_passwordConfigInitialized) - { - return _passwordConfigJson; - } - - var passwordConfig = new PersistedPasswordSettings - { - HashAlgorithm = _passwordConfiguration.HashAlgorithmType - }; - - _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); - _passwordConfigInitialized = true; return _passwordConfigJson; } - } - #region Overrides of RepositoryBase - - protected override IUser? PerformGet(int id) - { - // This will never resolve to a user, yet this is asked - // for all of the time (especially in cases of members). - // Don't issue a SQL call for this, we know it will not exist. - if (_runtimeState.Level == RuntimeLevel.Upgrade) + var passwordConfig = new PersistedPasswordSettings { - // when upgrading people might come from version 7 where user 0 was the default, - // only in upgrade mode do we want to fetch the user of Id 0 - if (id < -1) - { - return null; - } - } - else + HashAlgorithm = _passwordConfiguration.HashAlgorithmType + }; + + _passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig); + _passwordConfigInitialized = true; + return _passwordConfigJson; + } + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) => + dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + + #region Overrides of RepositoryBase + + protected override IUser? PerformGet(int id) + { + // This will never resolve to a user, yet this is asked + // for all of the time (especially in cases of members). + // Don't issue a SQL call for this, we know it will not exist. + if (_runtimeState.Level == RuntimeLevel.Upgrade) + { + // when upgrading people might come from version 7 where user 0 was the default, + // only in upgrade mode do we want to fetch the user of Id 0 + if (id < -1) { - if (id == default || id < -1) - { - return null; - } + return null; + } + } + else + { + if (id == default || id < -1) + { + return null; } - - var sql = SqlContext.Sql() - .Select() - .From() - .Where(x => x.Id == id); - - var dtos = Database.Fetch(sql); - if (dtos.Count == 0) return null; - - PerformGetReferencedDtos(dtos); - return UserFactory.BuildEntity(_globalSettings, dtos[0]); } - /// - /// Returns a user by username - /// - /// - /// - /// Can be used for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes). - /// This is really only used for a shim in order to upgrade to 7.6. - /// - /// - /// A non cached instance - /// - public IUser? GetByUsername(string username, bool includeSecurityData) + Sql sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.Id == id); + + List? dtos = Database.Fetch(sql); + if (dtos.Count == 0) { - return GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); + return null; } - /// - /// Returns a user by id - /// - /// - /// - /// This is really only used for a shim in order to upgrade to 7.6 but could be used - /// for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes) - /// - /// - /// A non cached instance - /// - public IUser? Get(int? id, bool includeSecurityData) - { - return GetWith(sql => sql.Where(x => x.Id == id), includeSecurityData); - } + PerformGetReferencedDtos(dtos); + return UserFactory.BuildEntity(_globalSettings, dtos[0]); + } - public IProfile? GetProfile(string username) - { - var dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); - return dto == null ? null : new UserProfile(dto.Id, dto.UserName); - } + /// + /// Returns a user by username + /// + /// + /// + /// Can be used for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start + /// nodes). + /// This is really only used for a shim in order to upgrade to 7.6. + /// + /// + /// A non cached instance + /// + public IUser? GetByUsername(string username, bool includeSecurityData) => + GetWith(sql => sql.Where(x => x.Login == username), includeSecurityData); - public IProfile? GetProfile(int id) - { - var dto = GetDtoWith(sql => sql.Where(x => x.Id == id), false); - return dto == null ? null : new UserProfile(dto.Id, dto.UserName); - } + /// + /// Returns a user by id + /// + /// + /// + /// This is really only used for a shim in order to upgrade to 7.6 but could be used + /// for slightly faster user lookups if the result doesn't require security data (i.e. groups, apps & start nodes) + /// + /// + /// A non cached instance + /// + public IUser? Get(int? id, bool includeSecurityData) => + GetWith(sql => sql.Where(x => x.Id == id), includeSecurityData); - public IDictionary GetUserStates() - { - // These keys in this query map to the `Umbraco.Core.Models.Membership.UserState` enum - var sql = @"SELECT -1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser + public IProfile? GetProfile(string username) + { + UserDto? dto = GetDtoWith(sql => sql.Where(x => x.Login == username), false); + return dto == null ? null : new UserProfile(dto.Id, dto.UserName); + } + + public IProfile? GetProfile(int id) + { + UserDto? dto = GetDtoWith(sql => sql.Where(x => x.Id == id), false); + return dto == null ? null : new UserProfile(dto.Id, dto.UserName); + } + + public IDictionary GetUserStates() + { + // These keys in this query map to the `Umbraco.Core.Models.Membership.UserState` enum + var sql = @"SELECT -1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser UNION SELECT 0 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL UNION @@ -190,36 +196,36 @@ SELECT 3 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE lastLoginDate IS UNION SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL"; - var result = Database.Dictionary(sql); + Dictionary? result = Database.Dictionary(sql); - return result.ToDictionary(x => (UserState)x.Key, x => x.Value); + return result.ToDictionary(x => (UserState)x.Key, x => x.Value); + } + + public Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true) + { + DateTime now = DateTime.UtcNow; + var dto = new UserLoginDto + { + UserId = userId, + IpAddress = requestingIpAddress, + LoggedInUtc = now, + LastValidatedUtc = now, + LoggedOutUtc = null, + SessionId = Guid.NewGuid() + }; + Database.Insert(dto); + + if (cleanStaleSessions) + { + ClearLoginSessions(TimeSpan.FromDays(15)); } - public Guid CreateLoginSession(int? userId, string requestingIpAddress, bool cleanStaleSessions = true) - { - var now = DateTime.UtcNow; - var dto = new UserLoginDto - { - UserId = userId, - IpAddress = requestingIpAddress, - LoggedInUtc = now, - LastValidatedUtc = now, - LoggedOutUtc = null, - SessionId = Guid.NewGuid() - }; - Database.Insert(dto); + return dto.SessionId; + } - if (cleanStaleSessions) - { - ClearLoginSessions(TimeSpan.FromDays(15)); - } - - return dto.SessionId; - } - - public bool ValidateLoginSession(int userId, Guid sessionId) - { - // HACK: Avoid a deadlock - BackOfficeCookieOptions OnValidatePrincipal + public bool ValidateLoginSession(int userId, Guid sessionId) + { + // HACK: Avoid a deadlock - BackOfficeCookieOptions OnValidatePrincipal // After existing session times out and user logs in again ~ 4 requests come in at once that hit the // "update the validate date" code path, check up the call stack there are a few variables that can make this not occur. // TODO: more generic fix, do something with ForUpdate? wait on a mutex? add a distributed lock? etc. @@ -237,724 +243,767 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 private bool ValidateLoginSessionInternal(int userId, Guid sessionId) { // with RepeatableRead transaction mode, read-then-update operations can - // cause deadlocks, and the ForUpdate() hint is required to tell the database - // to acquire an exclusive lock when reading + // cause deadlocks, and the ForUpdate() hint is required to tell the database + // to acquire an exclusive lock when reading - // that query is going to run a *lot*, make it a template - var t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s - .Select() - .From() - .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) - .ForUpdate() - .SelectTop(1)); // Stick at end, SQL server syntax provider will insert at start of query after "select ", but sqlite will append limit to end. + // that query is going to run a *lot*, make it a template + SqlTemplate t = SqlContext.Templates.Get("Umbraco.Core.UserRepository.ValidateLoginSession", s => s + .Select() + .From() + .Where(x => x.SessionId == SqlTemplate.Arg("sessionId")) + .ForUpdate() + .SelectTop(1)); // Stick at end, SQL server syntax provider will insert at start of query after "select ", but sqlite will append limit to end. - var sql = t.Sql(sessionId); + Sql sql = t.Sql(sessionId); - var found = Database.FirstOrDefault(sql); - if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) - return false; + UserLoginDto? found = Database.FirstOrDefault(sql); + if (found == null || found.UserId != userId || found.LoggedOutUtc.HasValue) + { + return false; + } - // now detect if there's been a timeout - if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) + //now detect if there's been a timeout + if (DateTime.UtcNow - found.LastValidatedUtc > _globalSettings.TimeOut) + { + //timeout detected, update the record + Logger.LogDebug("ClearLoginSession for sessionId {sessionId}", sessionId);ClearLoginSession(sessionId); + return false; + } + + //update the validate date + Logger.LogDebug("Updating LastValidatedUtc for sessionId {sessionId}", sessionId);found.LastValidatedUtc = DateTime.UtcNow; + Database.Update(found); + return true; + } + + public int ClearLoginSessions(int userId) => + Database.Delete(Sql().Where(x => x.UserId == userId)); + + public int ClearLoginSessions(TimeSpan timespan) + { + DateTime fromDate = DateTime.UtcNow - timespan; + return Database.Delete(Sql().Where(x => x.LastValidatedUtc < fromDate)); + } + + public void ClearLoginSession(Guid sessionId) => + // TODO: why is that one updating and not deleting? + Database.Execute(Sql() + .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow)) + .Where(x => x.SessionId == sessionId)); + + protected override IEnumerable PerformGetAll(params int[]? ids) + { + List dtos = ids?.Length == 0 + ? GetDtosWith(null, true) + : GetDtosWith(sql => sql.WhereIn(x => x.Id, ids), true); + var users = new IUser[dtos.Count]; + var i = 0; + foreach (UserDto dto in dtos) + { + users[i++] = UserFactory.BuildEntity(_globalSettings, dto); + } + + return users; + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var dtos = GetDtosWith(sql => new SqlTranslator(sql, query).Translate(), true) + .DistinctBy(x => x.Id) + .ToList(); + + var users = new IUser[dtos.Count]; + var i = 0; + foreach (UserDto dto in dtos) + { + users[i++] = UserFactory.BuildEntity(_globalSettings, dto); + } + + return users; + } + + private IUser? GetWith(Action> with, bool includeReferences) + { + UserDto? dto = GetDtoWith(with, includeReferences); + return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); + } + + private UserDto? GetDtoWith(Action> with, bool includeReferences) + { + List dtos = GetDtosWith(with, includeReferences); + return dtos.FirstOrDefault(); + } + + private List GetDtosWith(Action>? with, bool includeReferences) + { + Sql sql = SqlContext.Sql() + .Select() + .From(); + + with?.Invoke(sql); + + List? dtos = Database.Fetch(sql); + + if (includeReferences) + { + PerformGetReferencedDtos(dtos); + } + + return dtos; + } + + // NPoco cannot fetch 2+ references at a time + // plus it creates a combinatorial explosion + // better use extra queries + // unfortunately, SqlCe doesn't support multiple result sets + private void PerformGetReferencedDtos(List dtos) + { + if (dtos.Count == 0) + { + return; + } + + List userIds = dtos.Count == 1 ? new List {dtos[0].Id} : dtos.Select(x => x.Id).ToList(); + Dictionary? xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); + + // get users2groups + + Sql sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserId, userIds); + + List? user2Groups = Database.Fetch(sql); + var groupIds = user2Groups.Select(x => x.UserGroupId).ToList(); + + // get groups + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.Id, groupIds); + + var groups = Database.Fetch(sql) + .ToDictionary(x => x.Id, x => x); + + // get groups2apps + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserGroupId, groupIds); + + var groups2Apps = Database.Fetch(sql) + .GroupBy(x => x.UserGroupId) + .ToDictionary(x => x.Key, x => x); + + // get start nodes + + sql = SqlContext.Sql() + .Select() + .From() + .WhereIn(x => x.UserId, userIds); + + List? startNodes = Database.Fetch(sql); + + // map groups + + foreach (User2UserGroupDto? user2Group in user2Groups) + { + if (groups.TryGetValue(user2Group.UserGroupId, out UserGroupDto? group)) { - // timeout detected, update the record - Logger.LogDebug("ClearLoginSession for sessionId {sessionId}", sessionId); - ClearLoginSession(sessionId); - return false; - } - - // update the validate date - Logger.LogDebug("Updating LastValidatedUtc for sessionId {sessionId}", sessionId); - found.LastValidatedUtc = DateTime.UtcNow; - Database.Update(found); - return true; - } - - public int ClearLoginSessions(int userId) - { - return Database.Delete(Sql().Where(x => x.UserId == userId)); - } - - public int ClearLoginSessions(TimeSpan timespan) - { - var fromDate = DateTime.UtcNow - timespan; - return Database.Delete(Sql().Where(x => x.LastValidatedUtc < fromDate)); - } - - public void ClearLoginSession(Guid sessionId) - { - // TODO: why is that one updating and not deleting? - Database.Execute(Sql() - .Update(u => u.Set(x => x.LoggedOutUtc, DateTime.UtcNow)) - .Where(x => x.SessionId == sessionId)); - } - - protected override IEnumerable PerformGetAll(params int[]? ids) - { - var dtos = ids?.Length == 0 - ? GetDtosWith(null, true) - : GetDtosWith(sql => sql.WhereIn(x => x.Id, ids), true); - var users = new IUser[dtos.Count]; - var i = 0; - foreach (var dto in dtos) - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); - return users; - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var dtos = GetDtosWith(sql => new SqlTranslator(sql, query).Translate(), true) - .DistinctBy(x => x.Id) - .ToList(); - - var users = new IUser[dtos.Count]; - var i = 0; - foreach (var dto in dtos) - users[i++] = UserFactory.BuildEntity(_globalSettings, dto); - return users; - } - - private IUser? GetWith(Action> with, bool includeReferences) - { - var dto = GetDtoWith(with, includeReferences); - return dto == null ? null : UserFactory.BuildEntity(_globalSettings, dto); - } - - private UserDto? GetDtoWith(Action> with, bool includeReferences) - { - var dtos = GetDtosWith(with, includeReferences); - return dtos.FirstOrDefault(); - } - - private List GetDtosWith(Action>? with, bool includeReferences) - { - var sql = SqlContext.Sql() - .Select() - .From(); - - with?.Invoke(sql); - - var dtos = Database.Fetch(sql); - - if (includeReferences) - PerformGetReferencedDtos(dtos); - - return dtos; - } - - // NPoco cannot fetch 2+ references at a time - // plus it creates a combinatorial explosion - // better use extra queries - // unfortunately, SqlCe doesn't support multiple result sets - private void PerformGetReferencedDtos(List dtos) - { - if (dtos.Count == 0) return; - - var userIds = dtos.Count == 1 ? new List { dtos[0].Id } : dtos.Select(x => x.Id).ToList(); - var xUsers = dtos.Count == 1 ? null : dtos.ToDictionary(x => x.Id, x => x); - - // get users2groups - - var sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserId, userIds); - - var users2groups = Database.Fetch(sql); - var groupIds = users2groups.Select(x => x.UserGroupId).ToList(); - - // get groups - - sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.Id, groupIds); - - var groups = Database.Fetch(sql) - .ToDictionary(x => x.Id, x => x); - - // get groups2apps - - sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserGroupId, groupIds); - - var groups2apps = Database.Fetch(sql) - .GroupBy(x => x.UserGroupId) - .ToDictionary(x => x.Key, x => x); - - // get start nodes - - sql = SqlContext.Sql() - .Select() - .From() - .WhereIn(x => x.UserId, userIds); - - var startNodes = Database.Fetch(sql); - - // map groups - - foreach (var user2group in users2groups) - { - if (groups.TryGetValue(user2group.UserGroupId, out var group)) - { - var dto = xUsers == null ? dtos[0] : xUsers[user2group.UserId]; - dto.UserGroupDtos.Add(group); // user2group is distinct - } - } - - // map start nodes - - foreach (var startNode in startNodes) - { - var dto = xUsers == null ? dtos[0] : xUsers[startNode.UserId]; - dto.UserStartNodeDtos.Add(startNode); // hashset = distinct - } - - // map apps - - foreach (var group in groups.Values) - { - if (groups2apps.TryGetValue(group.Id, out var list)) - group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct + UserDto dto = xUsers == null ? dtos[0] : xUsers[user2Group.UserId]; + dto.UserGroupDtos.Add(group); // user2group is distinct } } - #endregion + // map start nodes - #region Overrides of EntityRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) + foreach (UserStartNodeDto? startNode in startNodes) { - if (isCount) - return SqlContext.Sql() - .SelectCount() - .From(); - - return SqlContext.Sql() - .Select() - .From(); + UserDto dto = xUsers == null ? dtos[0] : xUsers[startNode.UserId]; + dto.UserStartNodeDtos.Add(startNode); // hashset = distinct } - private static void AddGroupLeftJoin(Sql sql) - { - sql - .LeftJoin() - .On(left => left.UserId, right => right.Id) - .LeftJoin() - .On(left => left.Id, right => right.UserGroupId) - .LeftJoin() - .On(left => left.UserGroupId, right => right.Id) - .LeftJoin() - .On(left => left.UserId, right => right.Id); - } + // map apps - private Sql GetBaseQuery(string columns) + foreach (UserGroupDto? group in groups.Values) + { + if (groups2Apps.TryGetValue(group.Id, out IGrouping? list)) + { + group.UserGroup2AppDtos = list.ToList(); // groups2apps is distinct + } + } + } + + #endregion + + #region Overrides of EntityRepositoryBase + + protected override Sql GetBaseQuery(bool isCount) + { + if (isCount) { return SqlContext.Sql() - .Select(columns) + .SelectCount() .From(); } - protected override string GetBaseWhereClause() + return SqlContext.Sql() + .Select() + .From(); + } + + private static void AddGroupLeftJoin(Sql sql) => + sql + .LeftJoin() + .On(left => left.UserId, right => right.Id) + .LeftJoin() + .On(left => left.Id, right => right.UserGroupId) + .LeftJoin() + .On(left => left.UserGroupId, right => right.Id) + .LeftJoin() + .On(left => left.UserId, right => right.Id); + + private Sql GetBaseQuery(string columns) => + SqlContext.Sql() + .Select(columns) + .From(); + + protected override string GetBaseWhereClause() => $"{Constants.DatabaseSchema.Tables.User}.id = @id"; + + protected override IEnumerable GetDeleteClauses() + { + var list = new List { - return $"{Constants.DatabaseSchema.Tables.User}.id = @id"; + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", + $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" + }; + return list; + } + + protected override void PersistNewItem(IUser entity) + { + entity.AddingEntity(); + + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) + { + entity.SecurityStamp = Guid.NewGuid().ToString(); } - protected override IEnumerable GetDeleteClauses() + UserDto userDto = UserFactory.BuildDto(entity); + + // check if we have a user config else use the default + userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + + var id = Convert.ToInt32(Database.Insert(userDto)); + entity.Id = id; + + if (entity.IsPropertyDirty("StartContentIds")) { - var list = new List - { - $"DELETE FROM {Constants.DatabaseSchema.Tables.UserLogin} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User2UserGroup} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User2NodeNotify} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.UserStartNode} WHERE userId = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.User} WHERE id = @id", - $"DELETE FROM {Constants.DatabaseSchema.Tables.ExternalLogin} WHERE id = @id" - }; - return list; + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), + UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); } - protected override void PersistNewItem(IUser entity) + if (entity.IsPropertyDirty("StartMediaIds")) { - entity.AddingEntity(); - - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) - { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - UserDto userDto = UserFactory.BuildDto(entity); - - // check if we have a user config else use the default - userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - - var id = Convert.ToInt32(Database.Insert(userDto)); - entity.Id = id; - - if (entity.IsPropertyDirty("StartContentIds")) - { - AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); - } - - if (entity.IsPropertyDirty("StartMediaIds")) - { - AddingOrUpdateStartNodes(entity, Enumerable.Empty(), UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); - } - - if (entity.IsPropertyDirty("Groups")) - { - // lookup all assigned - var assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); - - foreach (var groupDto in assigned) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupDto.Id, - UserId = entity.Id - }; - Database.Insert(dto); - } - } - - entity.ResetDirtyProperties(); + AddingOrUpdateStartNodes(entity, Enumerable.Empty(), + UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); } - protected override void PersistUpdatedItem(IUser entity) + if (entity.IsPropertyDirty("Groups")) { - // updates Modified date - entity.UpdatingEntity(); + // lookup all assigned + List? assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", + new {aliases = entity.Groups.Select(x => x.Alias)}); - // ensure security stamp if missing - if (entity.SecurityStamp.IsNullOrWhiteSpace()) + foreach (UserGroupDto? groupDto in assigned) { - entity.SecurityStamp = Guid.NewGuid().ToString(); - } - - var userDto = UserFactory.BuildDto(entity); - - // build list of columns to check for saving - we don't want to save the password if it hasn't changed! - // list the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that - var colsToSave = new Dictionary - { - //TODO: Change these to constants + nameof - {"userDisabled", "IsApproved"}, - {"userNoConsole", "IsLockedOut"}, - {"startStructureID", "StartContentId"}, - {"startMediaID", "StartMediaId"}, - {"userName", "Name"}, - {"userLogin", "Username"}, - {"userEmail", "Email"}, - {"userLanguage", "Language"}, - {"securityStampToken", "SecurityStamp"}, - {"lastLockoutDate", "LastLockoutDate"}, - {"lastPasswordChangeDate", "LastPasswordChangeDate"}, - {"lastLoginDate", "LastLoginDate"}, - {"failedLoginAttempts", "FailedPasswordAttempts"}, - {"createDate", "CreateDate"}, - {"updateDate", "UpdateDate"}, - {"avatar", "Avatar"}, - {"emailConfirmedDate", "EmailConfirmedDate"}, - {"invitedDate", "InvitedDate"}, - {"tourData", "TourData"} - }; - - // create list of properties that have changed - var changedCols = colsToSave - .Where(col => entity.IsPropertyDirty(col.Value)) - .Select(col => col.Key) - .ToList(); - - if (entity.IsPropertyDirty("SecurityStamp")) - { - changedCols.Add("securityStampToken"); - } - - // DO NOT update the password if it has not changed or if it is null or empty - if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) - { - changedCols.Add("userPassword"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - - // check if we have a user config else use the default - userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; - changedCols.Add("passwordConfig"); - } - - // If userlogin or the email has changed then need to reset security stamp - if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) - { - userDto.EmailConfirmedDate = null; - changedCols.Add("emailConfirmedDate"); - - // If the security stamp hasn't already updated we need to force it - if (entity.IsPropertyDirty("SecurityStamp") == false) - { - userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); - changedCols.Add("securityStampToken"); - } - } - - //only update the changed cols - if (changedCols.Count > 0) - { - Database.Update(userDto, changedCols); - } - - if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) - { - var assignedStartNodes = Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", new { userId = entity.Id }); - if (entity.IsPropertyDirty("StartContentIds")) - { - AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, entity.StartContentIds); - } - if (entity.IsPropertyDirty("StartMediaIds")) - { - AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, entity.StartMediaIds); - } - } - - if (entity.IsPropertyDirty("Groups")) - { - //lookup all assigned - var assigned = entity.Groups == null || entity.Groups.Any() == false - ? new List() - : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) }); - - //first delete all - // TODO: We could do this a nicer way instead of "Nuke and Pave" - Database.Delete("WHERE UserId = @UserId", new { UserId = entity.Id }); - - foreach (var groupDto in assigned) - { - var dto = new User2UserGroupDto - { - UserGroupId = groupDto.Id, - UserId = entity.Id - }; - Database.Insert(dto); - } - } - - entity.ResetDirtyProperties(); - } - - private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds) - { - if (entityStartIds is null) - { - return; - } - var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); - - //remove the ones not assigned to the entity - var toDelete = assignedIds.Except(entityStartIds).ToArray(); - if (toDelete.Length > 0) - Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", new { UserId = entity.Id, startNodes = toDelete }); - //add the ones not currently in the db - var toAdd = entityStartIds.Except(assignedIds).ToArray(); - foreach (var i in toAdd) - { - var dto = new UserStartNodeDto - { - StartNode = i, - StartNodeType = (int)startNodeType, - UserId = entity.Id - }; + var dto = new User2UserGroupDto {UserGroupId = groupDto.Id, UserId = entity.Id}; Database.Insert(dto); } } - #endregion + entity.ResetDirtyProperties(); + } - #region Implementation of IUserRepository + protected override void PersistUpdatedItem(IUser entity) + { + // updates Modified date + entity.UpdatingEntity(); - public int GetCountByQuery(IQuery? query) + // ensure security stamp if missing + if (entity.SecurityStamp.IsNullOrWhiteSpace()) { - var sqlClause = GetBaseQuery("umbracoUser.id"); - var translator = new SqlTranslator(sqlClause, query); - var subquery = translator.Translate(); - //get the COUNT base query - var sql = GetBaseQuery(true) - .Append(new Sql("WHERE umbracoUser.id IN (" + subquery.SQL + ")", subquery.Arguments)); - - return Database.ExecuteScalar(sql); + entity.SecurityStamp = Guid.NewGuid().ToString(); } - public bool Exists(string username) + UserDto userDto = UserFactory.BuildDto(entity); + + // build list of columns to check for saving - we don't want to save the password if it hasn't changed! + // list the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that + var colsToSave = new Dictionary { - return ExistsByUserName(username); + //TODO: Change these to constants + nameof + {"userDisabled", "IsApproved"}, + {"userNoConsole", "IsLockedOut"}, + {"startStructureID", "StartContentId"}, + {"startMediaID", "StartMediaId"}, + {"userName", "Name"}, + {"userLogin", "Username"}, + {"userEmail", "Email"}, + {"userLanguage", "Language"}, + {"securityStampToken", "SecurityStamp"}, + {"lastLockoutDate", "LastLockoutDate"}, + {"lastPasswordChangeDate", "LastPasswordChangeDate"}, + {"lastLoginDate", "LastLoginDate"}, + {"failedLoginAttempts", "FailedPasswordAttempts"}, + {"createDate", "CreateDate"}, + {"updateDate", "UpdateDate"}, + {"avatar", "Avatar"}, + {"emailConfirmedDate", "EmailConfirmedDate"}, + {"invitedDate", "InvitedDate"}, + {"tourData", "TourData"} + }; + + // create list of properties that have changed + var changedCols = colsToSave + .Where(col => entity.IsPropertyDirty(col.Value)) + .Select(col => col.Key) + .ToList(); + + if (entity.IsPropertyDirty("SecurityStamp")) + { + changedCols.Add("securityStampToken"); } - public bool ExistsByUserName(string username) + // DO NOT update the password if it has not changed or if it is null or empty + if (entity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) { - var sql = SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.UserName == username); + changedCols.Add("userPassword"); - return Database.ExecuteScalar(sql) > 0; - } - - public bool ExistsByLogin(string login) - { - var sql = SqlContext.Sql() - .SelectCount() - .From() - .Where(x => x.Login == login); - - return Database.ExecuteScalar(sql) > 0; - } - - /// - /// Gets a list of objects associated with a given group - /// - /// Id of group - public IEnumerable GetAllInGroup(int groupId) - { - return GetAllInOrNotInGroup(groupId, true); - } - - /// - /// Gets a list of objects not associated with a given group - /// - /// Id of group - public IEnumerable GetAllNotInGroup(int groupId) - { - return GetAllInOrNotInGroup(groupId, false); - } - - private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) - { - var sql = SqlContext.Sql() - .Select() - .From(); - - var inSql = SqlContext.Sql() - .Select(x => x.UserId) - .From() - .Where(x => x.UserGroupId == groupId); - - if (include) - sql.WhereIn(x => x.Id, inSql); - else - sql.WhereNotIn(x => x.Id, inSql); - - - var dtos = Database.Fetch(sql); - - //adds missing bits like content and media start nodes - PerformGetReferencedDtos(dtos); - - return ConvertFromDtos(dtos); - } - - /// - /// Gets paged user results - /// - /// - /// - /// - /// - /// - /// - /// - /// A filter to only include user that belong to these user groups - /// - /// - /// A filter to only include users that do not belong to these user groups - /// - /// Optional parameter to filter by specified user state - /// - /// - /// - /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using NPoco paging (SQL paging) - /// - public IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, out long totalRecords, - Expression> orderBy, Direction orderDirection = Direction.Ascending, - string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, IQuery? filter = null) - { - if (orderBy == null) throw new ArgumentNullException(nameof(orderBy)); - - Sql? filterSql = null; - var customFilterWheres = filter?.GetWhereClauses().ToArray(); - var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; - if (hasCustomFilter - || includeUserGroups != null && includeUserGroups.Length > 0 - || excludeUserGroups != null && excludeUserGroups.Length > 0 - || userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false) - filterSql = SqlContext.Sql(); - - if (hasCustomFilter) + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { - foreach (var clause in customFilterWheres!) - filterSql?.Append($"AND ({clause.Item1})", clause.Item2); + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); } - if (includeUserGroups != null && includeUserGroups.Length > 0) + // check if we have a user config else use the default + userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson; + changedCols.Add("passwordConfig"); + } + + // If userlogin or the email has changed then need to reset security stamp + if (changedCols.Contains("userLogin") || changedCols.Contains("userEmail")) + { + userDto.EmailConfirmedDate = null; + changedCols.Add("emailConfirmedDate"); + + // If the security stamp hasn't already updated we need to force it + if (entity.IsPropertyDirty("SecurityStamp") == false) { - const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id - FROM umbracoUser - INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id - INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId - WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; - filterSql?.Append(subQuery, new { userGroups = includeUserGroups }); + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); + } + } + + //only update the changed cols + if (changedCols.Count > 0) + { + Database.Update(userDto, changedCols); + } + + if (entity.IsPropertyDirty("StartContentIds") || entity.IsPropertyDirty("StartMediaIds")) + { + List? assignedStartNodes = + Database.Fetch("SELECT * FROM umbracoUserStartNode WHERE userId = @userId", + new {userId = entity.Id}); + if (entity.IsPropertyDirty("StartContentIds")) + { + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Content, + entity.StartContentIds); } - if (excludeUserGroups != null && excludeUserGroups.Length > 0) + if (entity.IsPropertyDirty("StartMediaIds")) { - const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id - FROM umbracoUser - INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id - INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId - WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; - filterSql?.Append(subQuery, new { userGroups = excludeUserGroups }); + AddingOrUpdateStartNodes(entity, assignedStartNodes, UserStartNodeDto.StartNodeTypeValue.Media, + entity.StartMediaIds); } + } - if (userState != null && userState.Length > 0) + if (entity.IsPropertyDirty("Groups")) + { + //lookup all assigned + List? assigned = entity.Groups == null || entity.Groups.Any() == false + ? new List() + : Database.Fetch("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", + new {aliases = entity.Groups.Select(x => x.Alias)}); + + //first delete all + // TODO: We could do this a nicer way instead of "Nuke and Pave" + Database.Delete("WHERE UserId = @UserId", new {UserId = entity.Id}); + + foreach (UserGroupDto? groupDto in assigned) { - //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering - if (userState.Contains(UserState.All) == false) - { - var sb = new StringBuilder("("); - var appended = false; - - if (userState.Contains(UserState.Active)) - { - sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); - appended = true; - } - if (userState.Contains(UserState.Inactive)) - { - if (appended) sb.Append(" OR "); - sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); - appended = true; - } - if (userState.Contains(UserState.Disabled)) - { - if (appended) sb.Append(" OR "); - sb.Append("(userDisabled = 1)"); - appended = true; - } - if (userState.Contains(UserState.LockedOut)) - { - if (appended) sb.Append(" OR "); - sb.Append("(userNoConsole = 1)"); - appended = true; - } - if (userState.Contains(UserState.Invited)) - { - if (appended) sb.Append(" OR "); - sb.Append("(lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); - appended = true; - } - - sb.Append(")"); - filterSql?.Append("AND " + sb); - } + var dto = new User2UserGroupDto {UserGroupId = groupDto.Id, UserId = entity.Id}; + Database.Insert(dto); } - - // create base query - var sql = SqlContext.Sql() - .Select() - .From(); - - // apply query - if (query != null) - sql = new SqlTranslator(sql, query).Translate(); - - // get sorted and filtered sql - var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection); - - // get a page of results and total count - var pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); - - // map references - PerformGetReferencedDtos(pagedResult.Items); - return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); } - private Sql ApplyFilter(Sql sql, Sql? filterSql, bool hasWhereClause) + entity.ResetDirtyProperties(); + } + + private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable current, + UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds) + { + if (entityStartIds is null) { - if (filterSql == null) return sql; - - //ensure we don't append a WHERE if there is already one - var args = filterSql.Arguments; - var sqlFilter = hasWhereClause - ? filterSql.SQL - : " WHERE " + filterSql.SQL.TrimStart("AND "); - - sql.Append(SqlContext.Sql(sqlFilter, args)); - - return sql; + return; } - private Sql ApplySort(Sql sql, Expression> orderBy, Direction orderDirection) + var assignedIds = current.Where(x => x.StartNodeType == (int)startNodeType).Select(x => x.StartNode).ToArray(); + + //remove the ones not assigned to the entity + var toDelete = assignedIds.Except(entityStartIds).ToArray(); + if (toDelete.Length > 0) { - if (orderBy == null) return sql; - - var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); - var mapper = _mapperCollection[typeof(IUser)]; - var mappedField = mapper.Map(expressionMember?.Name); - - if (mappedField.IsNullOrWhiteSpace()) - throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); - - // 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 - - var orderByField = sql.GetAliasedField(mappedField); - - if (orderDirection == Direction.Ascending) - sql.OrderBy(orderByField); - else - sql.OrderByDescending(orderByField); - - return sql; + Database.Delete("WHERE UserId = @UserId AND startNode IN (@startNodes)", + new {UserId = entity.Id, startNodes = toDelete}); } - public IEnumerable GetNextUsers(int id, int count) + //add the ones not currently in the db + var toAdd = entityStartIds.Except(assignedIds).ToArray(); + foreach (var i in toAdd) { - var idsQuery = SqlContext.Sql() - .Select(x => x.Id) - .From() - .Where(x => x.Id >= id) - .OrderBy(x => x.Id); - - // first page is index 1, not zero - var ids = Database.Page(1, count, idsQuery).Items.ToArray(); - - // now get the actual users and ensure they are ordered properly (same clause) - return ids.Length == 0 ? Enumerable.Empty() : GetMany(ids)?.OrderBy(x => x.Id) ?? Enumerable.Empty(); - } - - #endregion - - private IEnumerable ConvertFromDtos(IEnumerable dtos) - { - return dtos.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + var dto = new UserStartNodeDto {StartNode = i, StartNodeType = (int)startNodeType, UserId = entity.Id}; + Database.Insert(dto); } } + + #endregion + + #region Implementation of IUserRepository + + public int GetCountByQuery(IQuery? query) + { + Sql sqlClause = GetBaseQuery("umbracoUser.id"); + var translator = new SqlTranslator(sqlClause, query); + Sql subquery = translator.Translate(); + //get the COUNT base query + Sql? sql = GetBaseQuery(true) + .Append(new Sql("WHERE umbracoUser.id IN (" + subquery.SQL + ")", subquery.Arguments)); + + return Database.ExecuteScalar(sql); + } + + public bool Exists(string username) => ExistsByUserName(username); + + public bool ExistsByUserName(string username) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.UserName == username); + + return Database.ExecuteScalar(sql) > 0; + } + + public bool ExistsByLogin(string login) + { + Sql sql = SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.Login == login); + + return Database.ExecuteScalar(sql) > 0; + } + + /// + /// Gets a list of objects associated with a given group + /// + /// Id of group + public IEnumerable GetAllInGroup(int groupId) => GetAllInOrNotInGroup(groupId, true); + + /// + /// Gets a list of objects not associated with a given group + /// + /// Id of group + public IEnumerable GetAllNotInGroup(int groupId) => GetAllInOrNotInGroup(groupId, false); + + private IEnumerable GetAllInOrNotInGroup(int groupId, bool include) + { + Sql sql = SqlContext.Sql() + .Select() + .From(); + + Sql inSql = SqlContext.Sql() + .Select(x => x.UserId) + .From() + .Where(x => x.UserGroupId == groupId); + + if (include) + { + sql.WhereIn(x => x.Id, inSql); + } + else + { + sql.WhereNotIn(x => x.Id, inSql); + } + + + List? dtos = Database.Fetch(sql); + + //adds missing bits like content and media start nodes + PerformGetReferencedDtos(dtos); + + return ConvertFromDtos(dtos); + } + + /// + /// Gets paged user results + /// + /// + /// + /// + /// + /// + /// + /// + /// A filter to only include user that belong to these user groups + /// + /// + /// A filter to only include users that do not belong to these user groups + /// + /// Optional parameter to filter by specified user state + /// + /// + /// + /// The query supplied will ONLY work with data specifically on the umbracoUser table because we are using NPoco paging + /// (SQL paging) + /// + public IEnumerable GetPagedResultsByQuery(IQuery? query, long pageIndex, int pageSize, + out long totalRecords, + Expression> orderBy, Direction orderDirection = Direction.Ascending, + string[]? includeUserGroups = null, string[]? excludeUserGroups = null, UserState[]? userState = null, + IQuery? filter = null) + { + if (orderBy == null) + { + throw new ArgumentNullException(nameof(orderBy)); + } + + Sql? filterSql = null; + Tuple[]? customFilterWheres = filter?.GetWhereClauses().ToArray(); + var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0; + if (hasCustomFilter + || (includeUserGroups != null && includeUserGroups.Length > 0) + || (excludeUserGroups != null && excludeUserGroups.Length > 0) + || (userState != null && userState.Length > 0 && userState.Contains(UserState.All) == false)) + { + filterSql = SqlContext.Sql(); + } + + if (hasCustomFilter) + { + foreach (Tuple clause in customFilterWheres!) + { + filterSql?.Append($"AND ({clause.Item1})", clause.Item2); + } + } + + if (includeUserGroups != null && includeUserGroups.Length > 0) + { + const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id + FROM umbracoUser + INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id + INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId + WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; + filterSql?.Append(subQuery, new {userGroups = includeUserGroups}); + } + + if (excludeUserGroups != null && excludeUserGroups.Length > 0) + { + const string subQuery = @"AND (umbracoUser.id NOT IN (SELECT DISTINCT umbracoUser.id + FROM umbracoUser + INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id + INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId + WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))"; + filterSql?.Append(subQuery, new {userGroups = excludeUserGroups}); + } + + if (userState != null && userState.Length > 0) + { + //the "ALL" state doesn't require any filtering so we ignore that, if it exists in the list we don't do any filtering + if (userState.Contains(UserState.All) == false) + { + var sb = new StringBuilder("("); + var appended = false; + + if (userState.Contains(UserState.Active)) + { + sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); + appended = true; + } + + if (userState.Contains(UserState.Inactive)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); + appended = true; + } + + if (userState.Contains(UserState.Disabled)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(userDisabled = 1)"); + appended = true; + } + + if (userState.Contains(UserState.LockedOut)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(userNoConsole = 1)"); + appended = true; + } + + if (userState.Contains(UserState.Invited)) + { + if (appended) + { + sb.Append(" OR "); + } + + sb.Append("(lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL)"); + appended = true; + } + + sb.Append(")"); + filterSql?.Append("AND " + sb); + } + } + + // create base query + Sql sql = SqlContext.Sql() + .Select() + .From(); + + // apply query + if (query != null) + { + sql = new SqlTranslator(sql, query).Translate(); + } + + // get sorted and filtered sql + Sql sqlNodeIdsWithSort = + ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection); + + // get a page of results and total count + Page? pagedResult = Database.Page(pageIndex + 1, pageSize, sqlNodeIdsWithSort); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + // map references + PerformGetReferencedDtos(pagedResult.Items); + return pagedResult.Items.Select(x => UserFactory.BuildEntity(_globalSettings, x)); + } + + private Sql ApplyFilter(Sql sql, Sql? filterSql, bool hasWhereClause) + { + if (filterSql == null) + { + return sql; + } + + //ensure we don't append a WHERE if there is already one + var args = filterSql.Arguments; + var sqlFilter = hasWhereClause + ? filterSql.SQL + : " WHERE " + filterSql.SQL.TrimStart("AND "); + + sql.Append(SqlContext.Sql(sqlFilter, args)); + + return sql; + } + + private Sql ApplySort(Sql sql, Expression>? orderBy, + Direction orderDirection) + { + if (orderBy == null) + { + return sql; + } + + MemberInfo? expressionMember = ExpressionHelper.GetMemberInfo(orderBy); + BaseMapper mapper = _mapperCollection[typeof(IUser)]; + var mappedField = mapper.Map(expressionMember?.Name); + + if (mappedField.IsNullOrWhiteSpace()) + { + throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); + } + + // 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 + + var orderByField = sql.GetAliasedField(mappedField); + + if (orderDirection == Direction.Ascending) + { + sql.OrderBy(orderByField); + } + else + { + sql.OrderByDescending(orderByField); + } + + return sql; + } + + public IEnumerable GetNextUsers(int id, int count) + { + Sql idsQuery = SqlContext.Sql() + .Select(x => x.Id) + .From() + .Where(x => x.Id >= id) + .OrderBy(x => x.Id); + + // first page is index 1, not zero + var ids = Database.Page(1, count, idsQuery).Items.ToArray(); + + // now get the actual users and ensure they are ordered properly (same clause) + return ids.Length == 0 + ? Enumerable.Empty() + : GetMany(ids).OrderBy(x => x.Id) ?? Enumerable.Empty(); + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs index 827aef1932..83c585dfd2 100644 --- a/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/ScalarMapper.cs @@ -1,14 +1,12 @@ -using System; - namespace Umbraco.Cms.Infrastructure.Persistence; public abstract class ScalarMapper : IScalarMapper { - /// - /// Performs a strongly typed mapping operation for a scalar value. - /// - protected abstract T Map(object value); - /// object IScalarMapper.Map(object value) => Map(value)!; + + /// + /// Performs a strongly typed mapping operation for a scalar value. + /// + protected abstract T Map(object value); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs index 6eb903c1f5..57e82770c6 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlContext.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlContext.cs @@ -1,60 +1,55 @@ -using System; -using System.Linq; using NPoco; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using MapperCollection = Umbraco.Cms.Infrastructure.Persistence.Mappers.MapperCollection; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Implements . +/// +public class SqlContext : ISqlContext { /// - /// Implements . + /// Initializes a new instance of the class. /// - public class SqlContext : ISqlContext + /// The sql syntax provider. + /// The Poco data factory. + /// The database type. + /// The mappers. + public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection? mappers = null) { - /// - /// Initializes a new instance of the class. - /// - /// The sql syntax provider. - /// The Poco data factory. - /// The database type. - /// The mappers. - public SqlContext(ISqlSyntaxProvider sqlSyntax, DatabaseType databaseType, IPocoDataFactory pocoDataFactory, IMapperCollection? mappers = null) - { - // for tests - Mappers = mappers; + // for tests + Mappers = mappers; - SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); - PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); - DatabaseType = databaseType ?? throw new ArgumentNullException(nameof(databaseType)); - Templates = new SqlTemplates(this); - } - - /// - public ISqlSyntaxProvider SqlSyntax { get; } - - /// - public DatabaseType DatabaseType { get; } - - /// - public Sql Sql() => NPoco.Sql.BuilderFor((ISqlContext) this); - - /// - public Sql Sql(string sql, params object[] args) => Sql().Append(sql, args); - - /// - public IQuery Query() => new Query(this); - - /// - public SqlTemplates Templates { get; } - - /// - public IPocoDataFactory PocoDataFactory { get; } - - /// - public IMapperCollection? Mappers { get; } + SqlSyntax = sqlSyntax ?? throw new ArgumentNullException(nameof(sqlSyntax)); + PocoDataFactory = pocoDataFactory ?? throw new ArgumentNullException(nameof(pocoDataFactory)); + DatabaseType = databaseType ?? throw new ArgumentNullException(nameof(databaseType)); + Templates = new SqlTemplates(this); } + + /// + public ISqlSyntaxProvider SqlSyntax { get; } + + /// + public DatabaseType DatabaseType { get; } + + /// + public SqlTemplates Templates { get; } + + /// + public IPocoDataFactory PocoDataFactory { get; } + + /// + public IMapperCollection? Mappers { get; } + + /// + public Sql Sql() => NPoco.Sql.BuilderFor((ISqlContext)this); + + /// + public Sql Sql(string sql, params object[] args) => Sql().Append(sql, args); + + /// + public IQuery Query() => new Query(this); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs index b929b0e7ec..e46963d738 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlContextExtensions.cs @@ -1,110 +1,110 @@ -using System; using System.Linq.Expressions; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Querying; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class SqlContextExtensions { /// - /// Provides extension methods to . + /// Visit an expression. /// - public static class SqlContextExtensions + /// The type of the DTO. + /// An . + /// An expression to visit. + /// An optional table alias. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) { - /// - /// Visit an expression. - /// - /// The type of the DTO. - /// An . - /// An expression to visit. - /// An optional table alias. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the DTO. - /// The type returned by the expression. - /// An . - /// An expression to visit. - /// An optional table alias. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the DTO. + /// The type returned by the expression. + /// An . + /// An expression to visit. + /// An optional table alias. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the first DTO. - /// The type of the second DTO. - /// An . - /// An expression to visit. - /// An optional table alias for the first DTO. - /// An optional table alias for the second DTO. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the first DTO. + /// The type of the second DTO. + /// An . + /// An expression to visit. + /// An optional table alias for the first DTO. + /// An optional table alias for the second DTO. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit an expression. - /// - /// The type of the first DTO. - /// The type of the second DTO. - /// The type returned by the expression. - /// An . - /// An expression to visit. - /// An optional table alias for the first DTO. - /// An optional table alias for the second DTO. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) - { - var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit an expression. + /// + /// The type of the first DTO. + /// The type of the second DTO. + /// The type returned by the expression. + /// An . + /// An expression to visit. + /// An optional table alias for the first DTO. + /// An optional table alias for the second DTO. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitDto(this ISqlContext sqlContext, Expression> expression, string? alias1 = null, string? alias2 = null) + { + var visitor = new PocoToSqlExpressionVisitor(sqlContext, alias1, alias2); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit a model expression. - /// - /// The type of the model. - /// An . - /// An expression to visit. - /// A SQL statement, and arguments, corresponding to the expression. - public static (string Sql, object[] Args) VisitModel(this ISqlContext sqlContext, Expression> expression) - { - var visitor = new ModelToSqlExpressionVisitor(sqlContext.SqlSyntax, sqlContext.Mappers); - var visited = visitor.Visit(expression); - return (visited, visitor.GetSqlParameters()); - } + /// + /// Visit a model expression. + /// + /// The type of the model. + /// An . + /// An expression to visit. + /// A SQL statement, and arguments, corresponding to the expression. + public static (string Sql, object[] Args) VisitModel( + this ISqlContext sqlContext, + Expression> expression) + { + var visitor = new ModelToSqlExpressionVisitor(sqlContext.SqlSyntax, sqlContext.Mappers); + var visited = visitor.Visit(expression); + return (visited, visitor.GetSqlParameters()); + } - /// - /// Visit a model expression representing a field. - /// - /// The type of the model. - /// An . - /// An expression to visit, representing a field. - /// The name of the field. - public static string VisitModelField(this ISqlContext sqlContext, Expression> field) - { - var (sql, _) = sqlContext.VisitModel(field); + /// + /// Visit a model expression representing a field. + /// + /// The type of the model. + /// An . + /// An expression to visit, representing a field. + /// The name of the field. + public static string VisitModelField(this ISqlContext sqlContext, Expression> field) + { + (string sql, object[] _) = sqlContext.VisitModel(field); - // going to return " = @0" - // take the first part only - var pos = sql.IndexOf(' '); - return sql.Substring(0, pos); - } + // going to return " = @0" + // take the first part only + var pos = sql.IndexOf(' '); + return sql.Substring(0, pos); } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs index fed6e221b5..26aea8965f 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ColumnInfo.cs @@ -1,40 +1,44 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +public class ColumnInfo { - public class ColumnInfo + public ColumnInfo(string tableName, string columnName, int ordinal, string columnDefault, string isNullable, string dataType) { - public ColumnInfo(string tableName, string columnName, int ordinal, string columnDefault, string isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - ColumnDefault = columnDefault; - IsNullable = isNullable.Equals("YES"); - DataType = dataType; - } - - public ColumnInfo(string tableName, string columnName, int ordinal, string isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - IsNullable = isNullable.Equals("YES"); - DataType = dataType; - } - - public ColumnInfo(string tableName, string columnName, int ordinal, bool isNullable, string dataType) - { - TableName = tableName; - ColumnName = columnName; - Ordinal = ordinal; - IsNullable = isNullable; - DataType = dataType; - } - - public string TableName { get; set; } - public string ColumnName { get; set; } - public int Ordinal { get; set; } - public string? ColumnDefault { get; set; } - public bool IsNullable { get; set; } - public string DataType { get; set; } + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + ColumnDefault = columnDefault; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; } + + public ColumnInfo(string tableName, string columnName, int ordinal, string isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable.Equals("YES"); + DataType = dataType; + } + + public ColumnInfo(string tableName, string columnName, int ordinal, bool isNullable, string dataType) + { + TableName = tableName; + ColumnName = columnName; + Ordinal = ordinal; + IsNullable = isNullable; + DataType = dataType; + } + + public string TableName { get; set; } + + public string ColumnName { get; set; } + + public int Ordinal { get; set; } + + public string? ColumnDefault { get; set; } + + public bool IsNullable { get; set; } + + public string DataType { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs index 18e4791d0b..498b88af61 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypes.cs @@ -1,18 +1,16 @@ -using System; -using System.Collections.Generic; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax -{ - public class DbTypes - { - public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) - { - ColumnTypeMap = columnTypeMap; - ColumnDbTypeMap = columnDbTypeMap; - } +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; - public IReadOnlyDictionary ColumnTypeMap { get; } - public IReadOnlyDictionary ColumnDbTypeMap { get; } +public class DbTypes +{ + public DbTypes(IReadOnlyDictionary columnTypeMap, IReadOnlyDictionary columnDbTypeMap) + { + ColumnTypeMap = columnTypeMap; + ColumnDbTypeMap = columnDbTypeMap; } + + public IReadOnlyDictionary ColumnTypeMap { get; } + + public IReadOnlyDictionary ColumnDbTypeMap { get; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs index bf1e0989f5..343e42d9c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/DbTypesFactory.cs @@ -1,20 +1,17 @@ -using System; -using System.Collections.Generic; using System.Data; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +internal class DbTypesFactory { - internal class DbTypesFactory + private readonly Dictionary _columnDbTypeMap = new(); + private readonly Dictionary _columnTypeMap = new(); + + public void Set(DbType dbType, string fieldDefinition) { - private readonly Dictionary _columnTypeMap = new Dictionary(); - private readonly Dictionary _columnDbTypeMap = new Dictionary(); - - public void Set(DbType dbType, string fieldDefinition) - { - _columnTypeMap[typeof(T)] = fieldDefinition; - _columnDbTypeMap[typeof(T)] = dbType; - } - - public DbTypes Create() => new DbTypes(_columnTypeMap, _columnDbTypeMap); + _columnTypeMap[typeof(T)] = fieldDefinition; + _columnDbTypeMap[typeof(T)] = dbType; } + + public DbTypes Create() => new(_columnTypeMap, _columnDbTypeMap); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 7760f86476..d9b76a4942 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,166 +1,210 @@ -using System; -using System.Collections.Generic; using System.Data; -using System.Linq.Expressions; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using System.Text.RegularExpressions; using NPoco; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Defines an SqlSyntaxProvider +/// +public interface ISqlSyntaxProvider { + string ProviderName { get; } + + string CreateTable { get; } + + string DropTable { get; } + + string AddColumn { get; } + + string DropColumn { get; } + + string AlterColumn { get; } + + string RenameColumn { get; } + + string RenameTable { get; } + + string CreateSchema { get; } + + string AlterSchema { get; } + + string DropSchema { get; } + + string CreateIndex { get; } + + string DropIndex { get; } + + string InsertData { get; } + + string UpdateData { get; } + + string DeleteData { get; } + + string TruncateTable { get; } + + string CreateConstraint { get; } + + string DeleteConstraint { get; } + + string DeleteDefaultConstraint { get; } + /// - /// Defines an SqlSyntaxProvider + /// Gets a regex matching aliased fields. /// - public interface ISqlSyntaxProvider - { - DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => - current; // Default implementation. + /// + /// Matches "(table.column) AS (alias)" where table, column and alias are properly escaped. + /// + Regex AliasRegex { get; } - string ProviderName { get; } + string ConvertIntegerToOrderableString { get; } - string EscapeString(string val); + string ConvertDateToOrderableString { get; } - string GetWildcardPlaceholder(); - string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); - string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); - string GetConcat(params string[] args); + string ConvertDecimalToOrderableString { get; } - string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false); + /// + /// Returns the default isolation level for the database + /// + IsolationLevel DefaultIsolationLevel { get; } - string GetQuotedTableName(string? tableName); - string GetQuotedColumnName(string? columnName); - string GetQuotedName(string? name); - bool DoesTableExist(IDatabase db, string tableName); - string GetIndexType(IndexTypes indexTypes); - string GetSpecialDbType(SpecialDbType dbType); - string CreateTable { get; } - string DropTable { get; } - string AddColumn { get; } - string DropColumn { get; } - string AlterColumn { get; } - string RenameColumn { get; } - string RenameTable { get; } - string CreateSchema { get; } - string AlterSchema { get; } - string DropSchema { get; } - string CreateIndex { get; } - string DropIndex { get; } - string InsertData { get; } - string UpdateData { get; } - string DeleteData { get; } - string TruncateTable { get; } - string CreateConstraint { get; } - string DeleteConstraint { get; } + string DbProvider { get; } - string DeleteDefaultConstraint { get; } - string FormatDateTime(DateTime date, bool includeTime = true); - string Format(TableDefinition table); - string Format(IEnumerable columns); - List Format(IEnumerable indexes); - List Format(IEnumerable foreignKeys); - string FormatPrimaryKey(TableDefinition table); - string GetQuotedValue(string value); - string Format(ColumnDefinition column); - string Format(ColumnDefinition column, string tableName, out IEnumerable sqls); - string Format(IndexDefinition index); - string Format(ForeignKeyDefinition foreignKey); - string FormatColumnRename(string? tableName, string? oldName, string? newName); - string FormatTableRename(string? oldName, string? newName); + IDictionary? ScalarMappers => null; - void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => + current; // Default implementation. - /// - /// Gets a regex matching aliased fields. - /// - /// - /// Matches "(table.column) AS (alias)" where table, column and alias are properly escaped. - /// - Regex AliasRegex { get; } + string EscapeString(string val); - Sql SelectTop(Sql sql, int top); + string GetWildcardPlaceholder(); - bool SupportsClustered(); - bool SupportsIdentityInsert(); + string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType); - string ConvertIntegerToOrderableString { get; } - string ConvertDateToOrderableString { get; } - string ConvertDecimalToOrderableString { get; } + string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType); - /// - /// Returns the default isolation level for the database - /// - IsolationLevel DefaultIsolationLevel { get; } + string GetConcat(params string[] args); - string DbProvider { get; } - IEnumerable GetTablesInSchema(IDatabase db); - IEnumerable GetColumnsInSchema(IDatabase db); + string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false); - /// - /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes) - /// - /// - /// - /// A Tuple containing: TableName, ConstraintName - /// - IEnumerable> GetConstraintsPerTable(IDatabase db); + string GetQuotedTableName(string? tableName); - /// - /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not include indexes) - /// - /// - /// - /// A Tuple containing: TableName, ColumnName, ConstraintName - /// - IEnumerable> GetConstraintsPerColumn(IDatabase db); + string GetQuotedColumnName(string? columnName); - /// - /// Returns all defined Indexes in the database excluding primary keys - /// - /// - /// - /// A Tuple containing: TableName, IndexName, ColumnName, IsUnique - /// - IEnumerable> GetDefinedIndexes(IDatabase db); + string GetQuotedName(string? name); - /// - /// Tries to gets the name of the default constraint on a column. - /// - /// The database. - /// The table name. - /// The column name. - /// The constraint name. - /// A value indicating whether a default constraint was found. - /// - /// Some database engines may not have names for default constraints, - /// in which case the function may return true, but is - /// unspecified. - /// - bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + bool DoesTableExist(IDatabase db, string tableName); + string GetIndexType(IndexTypes indexTypes); - string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null); + string GetSpecialDbType(SpecialDbType dbType); - /// - /// Appends the relevant ForUpdate hint. - /// - Sql InsertForUpdateHint(Sql sql); + string FormatDateTime(DateTime date, bool includeTime = true); - /// - /// Appends the relevant ForUpdate hint. - /// - Sql AppendForUpdateHint(Sql sql); + string Format(TableDefinition table); - /// - /// Handles left join with nested join - /// - Sql.SqlJoinClause LeftJoinWithNestedJoin( - Sql sql, - Func, Sql> nestedJoin, - string? alias = null); + string Format(IEnumerable columns); - IDictionary? ScalarMappers => null; - } + List Format(IEnumerable indexes); + + List Format(IEnumerable foreignKeys); + + string FormatPrimaryKey(TableDefinition table); + + string GetQuotedValue(string value); + + string Format(ColumnDefinition column); + + string Format(ColumnDefinition column, string tableName, out IEnumerable sqls); + + string Format(IndexDefinition index); + + string Format(ForeignKeyDefinition foreignKey); + + string FormatColumnRename(string? tableName, string? oldName, string? newName); + + string FormatTableRename(string? oldName, string? newName); + + void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + + Sql SelectTop(Sql sql, int top); + + bool SupportsClustered(); + + bool SupportsIdentityInsert(); + + IEnumerable GetTablesInSchema(IDatabase db); + + IEnumerable GetColumnsInSchema(IDatabase db); + + /// + /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not + /// include indexes) + /// + /// + /// + /// A Tuple containing: TableName, ConstraintName + /// + IEnumerable> GetConstraintsPerTable(IDatabase db); + + /// + /// Returns all constraints defined in the database (Primary keys, foreign keys, unique constraints...) (does not + /// include indexes) + /// + /// + /// + /// A Tuple containing: TableName, ColumnName, ConstraintName + /// + IEnumerable> GetConstraintsPerColumn(IDatabase db); + + /// + /// Returns all defined Indexes in the database excluding primary keys + /// + /// + /// + /// A Tuple containing: TableName, IndexName, ColumnName, IsUnique + /// + IEnumerable> GetDefinedIndexes(IDatabase db); + + /// + /// Tries to gets the name of the default constraint on a column. + /// + /// The database. + /// The table name. + /// The column name. + /// The constraint name. + /// A value indicating whether a default constraint was found. + /// + /// + /// Some database engines may not have names for default constraints, + /// in which case the function may return true, but is + /// unspecified. + /// + /// + bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + + string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql InsertForUpdateHint(Sql sql); + + /// + /// Appends the relevant ForUpdate hint. + /// + Sql AppendForUpdateHint(Sql sql); + + /// + /// Handles left join with nested join + /// + Sql.SqlJoinClause LeftJoinWithNestedJoin( + Sql sql, + Func, Sql> nestedJoin, + string? alias = null); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs index a3efde1731..79d00e8d96 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerVersionName.cs @@ -1,21 +1,20 @@ -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) +/// +/// +/// see: https://support.microsoft.com/en-us/kb/321185 +/// +internal enum SqlServerVersionName { - /// - /// Represents the version name of SQL server (i.e. the year 2008, 2005, etc...) - /// - /// - /// see: https://support.microsoft.com/en-us/kb/321185 - /// - internal enum SqlServerVersionName - { - Invalid = -1, - V7 = 0, - V2000 = 1, - V2005 = 2, - V2008 = 3, - V2012 = 4, - V2014 = 5, - V2016 = 6, - Other = 100 - } + Invalid = -1, + V7 = 0, + V2000 = 1, + V2005 = 2, + V2008 = 3, + V2012 = 4, + V2014 = 5, + V2016 = 6, + Other = 100, } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index b2bbd4ac5b..e2bd19cb95 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Text; using System.Text.RegularExpressions; @@ -12,603 +9,602 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +/// +/// Represents the Base Sql Syntax provider implementation. +/// +/// +/// All Sql Syntax provider implementations should derive from this abstract class. +/// +/// +public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider + where TSyntax : ISqlSyntaxProvider { - /// - /// Represents the Base Sql Syntax provider implementation. - /// - /// - /// All Sql Syntax provider implementations should derive from this abstract class. - /// - /// - public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider - where TSyntax : ISqlSyntaxProvider + private readonly Lazy _dbTypes; + + protected SqlSyntaxProviderBase() { - private readonly Lazy _dbTypes; - - protected SqlSyntaxProviderBase() + ClauseOrder = new List> { - ClauseOrder = new List> - { - FormatString, - FormatType, - FormatNullable, - FormatConstraint, - FormatDefaultValue, - FormatPrimaryKey, - FormatIdentity - }; + FormatString, + FormatType, + FormatNullable, + FormatConstraint, + FormatDefaultValue, + FormatPrimaryKey, + FormatIdentity, + }; - //defaults for all providers - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); - DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); + // defaults for all providers + StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; + StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); + DecimalColumnDefinition = + string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); - // ReSharper disable VirtualMemberCallInConstructor - // ok to call virtual GetQuotedXxxName here - they don't depend on any state - var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); - var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; - // ReSharper restore VirtualMemberCallInConstructor - AliasRegex = new Regex("(" + fld + @")\s+AS\s+(" + col + ")", RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + // ReSharper disable VirtualMemberCallInConstructor + // ok to call virtual GetQuotedXxxName here - they don't depend on any state + var col = Regex.Escape(GetQuotedColumnName("column")).Replace("column", @"\w+"); + var fld = Regex.Escape(GetQuotedTableName("table") + ".").Replace("table", @"\w+") + col; - _dbTypes = new Lazy(InitColumnTypeMap); + // ReSharper restore VirtualMemberCallInConstructor + AliasRegex = new Regex( + "(" + fld + @")\s+AS\s+(" + col + ")", + RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.Compiled); + + _dbTypes = new Lazy(InitColumnTypeMap); + } + + public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; + + public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; + + public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; + + public string DefaultValueFormat { get; } = "DEFAULT ({0})"; + + public int DefaultStringLength { get; } = 255; + + public int DefaultDecimalPrecision { get; } = 20; + + public int DefaultDecimalScale { get; } = 9; + + // Set by Constructor + public virtual string StringColumnDefinition { get; } + + public string StringLengthColumnDefinitionFormat { get; } + + public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; + + public string IntColumnDefinition { get; protected set; } = "INTEGER"; + + public string LongColumnDefinition { get; protected set; } = "BIGINT"; + + public string GuidColumnDefinition { get; protected set; } = "GUID"; + + public string BoolColumnDefinition { get; protected set; } = "BOOL"; + + public string RealColumnDefinition { get; protected set; } = "DOUBLE"; + + public string DecimalColumnDefinition { get; protected set; } + + public string BlobColumnDefinition { get; protected set; } = "BLOB"; + + public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; + + public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; + + public string TimeColumnDefinition { get; protected set; } = "DATETIME"; + + public virtual string CreateForeignKeyConstraint => + "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; + + protected IList> ClauseOrder { get; } + + protected DbTypes DbTypeMap => _dbTypes.Value; + + public virtual string CreateDefaultConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} DEFAULT ({2}) FOR {3}"; + + public Regex AliasRegex { get; } + + public abstract string ProviderName { get; } + + public abstract IsolationLevel DefaultIsolationLevel { get; } + + public string GetWildcardPlaceholder() => "%"; + + public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => current; + + public virtual string EscapeString(string val) => NPocoDatabaseExtensions.EscapeAtSymbols(val.Replace("'", "''")); + + public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) => + + // use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + $"upper({column}) = upper(@{paramIndex})"; + + public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) => + + // use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. + $"upper({column}) LIKE upper(@{paramIndex})"; + + public virtual string GetConcat(params string[] args) => "concat(" + string.Join(",", args) + ")"; + + public virtual string GetQuotedTableName(string? tableName) => $"\"{tableName}\""; + + public virtual string GetQuotedColumnName(string? columnName) => $"\"{columnName}\""; + + public virtual string GetQuotedName(string? name) => $"\"{name}\""; + + public virtual string GetQuotedValue(string value) => $"'{value}'"; + + public virtual string GetIndexType(IndexTypes indexTypes) + { + string indexType; + + if (indexTypes == IndexTypes.Clustered) + { + indexType = "CLUSTERED"; + } + else + { + indexType = indexTypes == IndexTypes.NonClustered + ? "NONCLUSTERED" + : "UNIQUE NONCLUSTERED"; } - public Regex AliasRegex { get; } + return indexType; + } - public string GetWildcardPlaceholder() => "%"; - - public string StringLengthNonUnicodeColumnDefinitionFormat { get; } = "VARCHAR({0})"; - public virtual string StringLengthUnicodeColumnDefinitionFormat { get; } = "NVARCHAR({0})"; - public string DecimalColumnDefinitionFormat { get; } = "DECIMAL({0},{1})"; - - public string DefaultValueFormat { get; } = "DEFAULT ({0})"; - public int DefaultStringLength { get; } = 255; - public int DefaultDecimalPrecision { get; } = 20; - public int DefaultDecimalScale { get; } = 9; - - //Set by Constructor - public virtual string StringColumnDefinition { get; } - public string StringLengthColumnDefinitionFormat { get; } - - public string AutoIncrementDefinition { get; protected set; } = "AUTOINCREMENT"; - public string IntColumnDefinition { get; protected set; } = "INTEGER"; - public string LongColumnDefinition { get; protected set; } = "BIGINT"; - public string GuidColumnDefinition { get; protected set; } = "GUID"; - public string BoolColumnDefinition { get; protected set; } = "BOOL"; - public string RealColumnDefinition { get; protected set; } = "DOUBLE"; - public string DecimalColumnDefinition { get; protected set; } - public string BlobColumnDefinition { get; protected set; } = "BLOB"; - public string DateTimeColumnDefinition { get; protected set; } = "DATETIME"; - public string DateTimeOffsetColumnDefinition { get; protected set; } = "DATETIMEOFFSET(7)"; - public string TimeColumnDefinition { get; protected set; } = "DATETIME"; - - protected IList> ClauseOrder { get; } - - protected DbTypes DbTypeMap => _dbTypes.Value; - - private DbTypes InitColumnTypeMap() + public virtual string GetSpecialDbType(SpecialDbType dbType) + { + if (dbType == SpecialDbType.NCHAR) { - var dbTypeMap = new DbTypesFactory(); - dbTypeMap.Set(DbType.String, StringColumnDefinition); - dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); - dbTypeMap.Set(DbType.String, StringColumnDefinition); - dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); - dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); - dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); - dbTypeMap.Set(DbType.Time, TimeColumnDefinition); - dbTypeMap.Set(DbType.Time, TimeColumnDefinition); - dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); - - dbTypeMap.Set(DbType.Byte, IntColumnDefinition); - dbTypeMap.Set(DbType.Byte, IntColumnDefinition); - dbTypeMap.Set(DbType.SByte, IntColumnDefinition); - dbTypeMap.Set(DbType.SByte, IntColumnDefinition); - dbTypeMap.Set(DbType.Int16, IntColumnDefinition); - dbTypeMap.Set(DbType.Int16, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); - dbTypeMap.Set(DbType.Int32, IntColumnDefinition); - dbTypeMap.Set(DbType.Int32, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); - - dbTypeMap.Set(DbType.Int64, LongColumnDefinition); - dbTypeMap.Set(DbType.Int64, LongColumnDefinition); - dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); - - dbTypeMap.Set(DbType.Single, RealColumnDefinition); - dbTypeMap.Set(DbType.Single, RealColumnDefinition); - dbTypeMap.Set(DbType.Double, RealColumnDefinition); - dbTypeMap.Set(DbType.Double, RealColumnDefinition); - - dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); - - dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); - - return dbTypeMap.Create(); + return SpecialDbType.NCHAR; } - public virtual DatabaseType GetUpdatedDatabaseType(DatabaseType current, string? connectionString) => current; - - public abstract string ProviderName { get; } - - public virtual string EscapeString(string val) + if (dbType == SpecialDbType.NTEXT) { - return NPocoDatabaseExtensions.EscapeAtSymbols(val.Replace("'", "''")); + return SpecialDbType.NTEXT; } - public virtual string GetStringColumnEqualComparison(string column, int paramIndex, TextColumnType columnType) + if (dbType == SpecialDbType.NVARCHARMAX) { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return $"upper({column}) = upper(@{paramIndex})"; + return "NVARCHAR(MAX)"; } - public virtual string GetStringColumnWildcardComparison(string column, int paramIndex, TextColumnType columnType) + return "NVARCHAR"; + } + + public virtual string GetColumn(DatabaseType dbType, string tableName, string columnName, string? columnAlias, string? referenceName = null, bool forInsert = false) + { + tableName = GetQuotedTableName(tableName); + columnName = GetQuotedColumnName(columnName); + var column = tableName + "." + columnName; + if (columnAlias == null) { - //use the 'upper' method to always ensure strings are matched without case sensitivity no matter what the db setting. - return $"upper({column}) LIKE upper(@{paramIndex})"; - } - - public virtual string GetConcat(params string[] args) - { - return "concat(" + string.Join(",", args) + ")"; - } - - public virtual string GetQuotedTableName(string? tableName) - { - return $"\"{tableName}\""; - } - - public virtual string GetQuotedColumnName(string? columnName) - { - return $"\"{columnName}\""; - } - - public virtual string GetQuotedName(string? name) - { - return $"\"{name}\""; - } - - public virtual string GetQuotedValue(string value) - { - return $"'{value}'"; - } - - public virtual string GetIndexType(IndexTypes indexTypes) - { - string indexType; - - if (indexTypes == IndexTypes.Clustered) - { - indexType = "CLUSTERED"; - } - else - { - indexType = indexTypes == IndexTypes.NonClustered - ? "NONCLUSTERED" - : "UNIQUE NONCLUSTERED"; - } - return indexType; - } - - public virtual string GetSpecialDbType(SpecialDbType dbType) - { - if (dbType == SpecialDbType.NCHAR) - { - return SpecialDbType.NCHAR; - } - else if (dbType == SpecialDbType.NTEXT) - { - return SpecialDbType.NTEXT; - } - else if (dbType == SpecialDbType.NVARCHARMAX) - { - return "NVARCHAR(MAX)"; - } - - return "NVARCHAR"; - } - - public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => $"{GetSpecialDbType(dbType)}({customSize})"; - - public virtual string GetColumn(DatabaseType dbType, string tableName, string columnName, string columnAlias, string? referenceName = null, bool forInsert = false) - { - tableName = GetQuotedTableName(tableName); - columnName = GetQuotedColumnName(columnName); - var column = tableName + "." + columnName; - if (columnAlias == null) return column; - - referenceName = referenceName == null ? string.Empty : referenceName + "__"; - columnAlias = GetQuotedColumnName(referenceName + columnAlias); - column += " AS " + columnAlias; return column; } + referenceName = referenceName == null ? string.Empty : referenceName + "__"; + columnAlias = GetQuotedColumnName(referenceName + columnAlias); + column += " AS " + columnAlias; + return column; + } - public abstract IsolationLevel DefaultIsolationLevel { get; } - public abstract string DbProvider { get; } + public abstract string DbProvider { get; } - public virtual IEnumerable GetTablesInSchema(IDatabase db) + public virtual IDictionary? ScalarMappers => null; + + public virtual string DeleteDefaultConstraint => + throw new NotSupportedException("Default constraints are not supported"); + + public virtual IEnumerable GetTablesInSchema(IDatabase db) => new List(); + + public virtual IEnumerable GetColumnsInSchema(IDatabase db) => new List(); + + public virtual IEnumerable> GetConstraintsPerTable(IDatabase db) => + new List>(); + + public virtual IEnumerable> GetConstraintsPerColumn(IDatabase db) => + new List>(); + + public abstract IEnumerable> GetDefinedIndexes(IDatabase db); + + public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + + public virtual string GetFieldNameForUpdate( + Expression> fieldSelector, + string? tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); + + public virtual Sql InsertForUpdateHint(Sql sql) => sql; + + public virtual Sql AppendForUpdateHint(Sql sql) => sql; + + public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin(Sql sql, Func, Sql> nestedJoin, string? alias = null); + + public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); + + public virtual bool SupportsClustered() => true; + + public virtual bool SupportsIdentityInsert() => true; + + /// + /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) + /// + /// + /// + /// + /// + /// MSSQL has a DateTime standard that is unambiguous and works on all servers: + /// YYYYMMDD HH:mm:ss + /// + public virtual string FormatDateTime(DateTime date, bool includeTime = true) => + + // need CultureInfo.InvariantCulture because ":" here is the "time separator" and + // may be converted to something else in different cultures (eg "." in DK). + date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); + + public virtual string Format(TableDefinition table) + { + var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); + + return statement; + } + + public virtual List Format(IEnumerable indexes) => indexes.Select(Format).ToList(); + + public virtual 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 virtual List Format(IEnumerable foreignKeys) => + foreignKeys.Select(Format).ToList(); + + public virtual string Format(ForeignKeyDefinition foreignKey) + { + var constraintName = string.IsNullOrEmpty(foreignKey.Name) + ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" + : foreignKey.Name; + + return string.Format( + CreateForeignKeyConstraint, + GetQuotedTableName(foreignKey.ForeignTable), + GetQuotedName(constraintName), + GetQuotedColumnName(foreignKey.ForeignColumns.First()), + GetQuotedTableName(foreignKey.PrimaryTable), + GetQuotedColumnName(foreignKey.PrimaryColumns.First()), + FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("UPDATE", foreignKey.OnUpdate)); + } + + public virtual string Format(IEnumerable columns) + { + var sb = new StringBuilder(); + foreach (ColumnDefinition column in columns) { - return new List(); + sb.Append(Format(column) + ",\n"); } - public virtual IEnumerable GetColumnsInSchema(IDatabase db) - { - return new List(); - } + return sb.ToString().TrimEnd(",\n"); + } - public virtual IEnumerable> GetConstraintsPerTable(IDatabase db) - { - return new List>(); - } + public virtual string Format(ColumnDefinition column) => + string.Join(" ", ClauseOrder + .Select(action => action(column)) + .Where(clause => string.IsNullOrEmpty(clause) == false)); - public virtual IEnumerable> GetConstraintsPerColumn(IDatabase db) - { - return new List>(); - } + public virtual string Format(ColumnDefinition column, string tableName, out IEnumerable sqls) + { + var sql = new StringBuilder(); + sql.Append(FormatString(column)); + sql.Append(" "); + sql.Append(FormatType(column)); + sql.Append(" "); + sql.Append("NULL"); // always nullable + sql.Append(" "); + sql.Append(FormatConstraint(column)); + sql.Append(" "); + sql.Append(FormatDefaultValue(column)); + sql.Append(" "); + sql.Append(FormatPrimaryKey(column)); + sql.Append(" "); + sql.Append(FormatIdentity(column)); - public abstract IEnumerable> GetDefinedIndexes(IDatabase db); + // var isNullable = column.IsNullable; - public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName); + // var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); + // var hasConstraint = !string.IsNullOrWhiteSpace(constraint); - public virtual string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias); + // var defaultValue = FormatDefaultValue(column); + // var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); - public virtual Sql InsertForUpdateHint(Sql sql) => sql; + // TODO: This used to exit if nullable but that means this would never work + // to return SQL if the column was nullable?!? I don't get it. This was here + // 4 years ago, I've removed it so that this works for nullable columns. + // if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) + // { + // sqls = Enumerable.Empty(); + // return sql.ToString(); + // } + var msql = new List(); + sqls = msql; - public virtual Sql AppendForUpdateHint(Sql sql) => sql; + var alterSql = new StringBuilder(); + alterSql.Append(FormatString(column)); + alterSql.Append(" "); + alterSql.Append(FormatType(column)); + alterSql.Append(" "); + alterSql.Append(FormatNullable(column)); - public abstract Sql.SqlJoinClause LeftJoinWithNestedJoin(Sql sql, Func, Sql> nestedJoin, string? alias = null); + // alterSql.Append(" "); + // alterSql.Append(FormatPrimaryKey(column)); + // alterSql.Append(" "); + // alterSql.Append(FormatIdentity(column)); + msql.Add(string.Format(AlterColumn, tableName, alterSql)); + // if (hasConstraint) + // { + // var dropConstraintSql = string.Format(DeleteConstraint, tableName, constraint); + // msql.Add(dropConstraintSql); + // var constraintType = hasDefaultValue ? defaultValue : ""; + // var createConstraintSql = string.Format(CreateConstraint, tableName, constraint, constraintType, FormatString(column)); + // msql.Add(createConstraintSql); + // } + return sql.ToString(); + } - public virtual IDictionary? ScalarMappers => null; - - public virtual bool DoesTableExist(IDatabase db, string tableName) => GetTablesInSchema(db).Contains(tableName); - - public virtual bool SupportsClustered() - { - return true; - } - - public virtual bool SupportsIdentityInsert() - { - return true; - } - - /// - /// This is used ONLY if we need to format datetime without using SQL parameters (i.e. during migrations) - /// - /// - /// - /// - /// - /// MSSQL has a DateTime standard that is unambiguous and works on all servers: - /// YYYYMMDD HH:mm:ss - /// - public virtual string FormatDateTime(DateTime date, bool includeTime = true) - { - // need CultureInfo.InvariantCulture because ":" here is the "time separator" and - // may be converted to something else in different cultures (eg "." in DK). - return date.ToString(includeTime ? "yyyyMMdd HH:mm:ss" : "yyyyMMdd", CultureInfo.InvariantCulture); - } - - public virtual string Format(TableDefinition table) - { - var statement = string.Format(CreateTable, GetQuotedTableName(table.Name), Format(table.Columns)); - - return statement; - } - - public virtual List Format(IEnumerable indexes) - { - return indexes.Select(Format).ToList(); - } - - public virtual 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 virtual List Format(IEnumerable foreignKeys) - { - return foreignKeys.Select(Format).ToList(); - } - - public virtual string Format(ForeignKeyDefinition foreignKey) - { - var constraintName = string.IsNullOrEmpty(foreignKey.Name) - ? $"FK_{foreignKey.ForeignTable}_{foreignKey.PrimaryTable}_{foreignKey.PrimaryColumns.First()}" - : foreignKey.Name; - - return string.Format(CreateForeignKeyConstraint, - GetQuotedTableName(foreignKey.ForeignTable), - GetQuotedName(constraintName), - GetQuotedColumnName(foreignKey.ForeignColumns.First()), - GetQuotedTableName(foreignKey.PrimaryTable), - GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), - FormatCascade("UPDATE", foreignKey.OnUpdate)); - } - - public virtual string Format(IEnumerable columns) - { - var sb = new StringBuilder(); - foreach (var column in columns) - { - sb.Append(Format(column) + ",\n"); - } - return sb.ToString().TrimEnd(",\n"); - } - - public virtual string Format(ColumnDefinition column) - { - return string.Join(" ", ClauseOrder - .Select(action => action(column)) - .Where(clause => string.IsNullOrEmpty(clause) == false)); - } - - public virtual string Format(ColumnDefinition column, string tableName, out IEnumerable sqls) - { - var sql = new StringBuilder(); - sql.Append(FormatString(column)); - sql.Append(" "); - sql.Append(FormatType(column)); - sql.Append(" "); - sql.Append("NULL"); // always nullable - sql.Append(" "); - sql.Append(FormatConstraint(column)); - sql.Append(" "); - sql.Append(FormatDefaultValue(column)); - sql.Append(" "); - sql.Append(FormatPrimaryKey(column)); - sql.Append(" "); - sql.Append(FormatIdentity(column)); - - //var isNullable = column.IsNullable; - - //var constraint = FormatConstraint(column)?.TrimStart("CONSTRAINT "); - //var hasConstraint = !string.IsNullOrWhiteSpace(constraint); - - //var defaultValue = FormatDefaultValue(column); - //var hasDefaultValue = !string.IsNullOrWhiteSpace(defaultValue); - - // TODO: This used to exit if nullable but that means this would never work - // to return SQL if the column was nullable?!? I don't get it. This was here - // 4 years ago, I've removed it so that this works for nullable columns. - //if (isNullable /*&& !hasConstraint && !hasDefaultValue*/) - //{ - // sqls = Enumerable.Empty(); - // return sql.ToString(); - //} - - var msql = new List(); - sqls = msql; - - var alterSql = new StringBuilder(); - alterSql.Append(FormatString(column)); - alterSql.Append(" "); - alterSql.Append(FormatType(column)); - alterSql.Append(" "); - alterSql.Append(FormatNullable(column)); - //alterSql.Append(" "); - //alterSql.Append(FormatPrimaryKey(column)); - //alterSql.Append(" "); - //alterSql.Append(FormatIdentity(column)); - msql.Add(string.Format(AlterColumn, tableName, alterSql)); - - //if (hasConstraint) - //{ - // var dropConstraintSql = string.Format(DeleteConstraint, tableName, constraint); - // msql.Add(dropConstraintSql); - // var constraintType = hasDefaultValue ? defaultValue : ""; - // var createConstraintSql = string.Format(CreateConstraint, tableName, constraint, constraintType, FormatString(column)); - // msql.Add(createConstraintSql); - //} - - return sql.ToString(); - } - - public virtual string FormatPrimaryKey(TableDefinition table) - { - var columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); - if (columnDefinition == null) - return string.Empty; - - var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) - ? $"PK_{table.Name}" - : columnDefinition.PrimaryKeyName; - - var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) - ? GetQuotedColumnName(columnDefinition.Name) - : string.Join(", ", columnDefinition.PrimaryKeyColumns - .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) - .Select(GetQuotedColumnName)); - - var primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - - return string.Format(CreateConstraint, - GetQuotedTableName(table.Name), - GetQuotedName(constraintName), - primaryKeyPart, - columns); - } - - public virtual string FormatColumnRename(string? tableName, string? oldName, string? newName) - { - return string.Format(RenameColumn, - GetQuotedTableName(tableName), - GetQuotedColumnName(oldName), - GetQuotedColumnName(newName)); - } - - public virtual string FormatTableRename(string? oldName, string? newName) - { - return string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); - } - - protected virtual string FormatCascade(string onWhat, Rule rule) - { - var action = "NO ACTION"; - switch (rule) - { - case Rule.None: - return ""; - case Rule.Cascade: - action = "CASCADE"; - break; - case Rule.SetNull: - action = "SET NULL"; - break; - case Rule.SetDefault: - action = "SET DEFAULT"; - break; - } - - return $" ON {onWhat} {action}"; - } - - protected virtual string FormatString(ColumnDefinition column) - { - return GetQuotedColumnName(column.Name); - } - - protected virtual string FormatType(ColumnDefinition column) - { - if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) - return column.CustomType; - - if (column.CustomDbType.HasValue) - { - if (column.Size != default) - { - return GetSpecialDbType(column.CustomDbType.Value, column.Size); - } - - return GetSpecialDbType(column.CustomDbType.Value); - } - - var type = column.Type.HasValue - ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key - : column.PropertyType; - - if (type == typeof(string)) - { - var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; - return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); - } - - if (type == typeof(decimal)) - { - var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; - var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; - return string.Format(DecimalColumnDefinitionFormat, precision, scale); - } - - var definition = DbTypeMap.ColumnTypeMap[type!]; - var dbTypeDefinition = column.Size != default - ? $"{definition}({column.Size})" - : definition; - //NOTE Precision is left out - return dbTypeDefinition; - } - - protected virtual string FormatNullable(ColumnDefinition column) - { - return column.IsNullable ? "NULL" : "NOT NULL"; - } - - protected virtual string FormatConstraint(ColumnDefinition column) - { - if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) - return string.Empty; - - return - $"CONSTRAINT {(string.IsNullOrEmpty(column.ConstraintName) ? GetQuotedName($"DF_{column.TableName}_{column.Name}") : column.ConstraintName)}"; - } - - protected virtual string FormatDefaultValue(ColumnDefinition column) - { - if (column.DefaultValue == null) - return string.Empty; - - // HACK: probably not needed with latest changes - if (string.Equals(column.DefaultValue.ToString(), "GETDATE()", StringComparison.OrdinalIgnoreCase)) - column.DefaultValue = SystemMethods.CurrentDateTime; - - // see if this is for a system method - if (column.DefaultValue is SystemMethods) - { - var method = FormatSystemMethods((SystemMethods)column.DefaultValue); - return string.IsNullOrEmpty(method) ? string.Empty : string.Format(DefaultValueFormat, method); - } - - return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString()!)); - } - - protected virtual string FormatPrimaryKey(ColumnDefinition column) + public virtual string FormatPrimaryKey(TableDefinition table) + { + ColumnDefinition? columnDefinition = table.Columns.FirstOrDefault(x => x.IsPrimaryKey); + if (columnDefinition == null) { return string.Empty; } - protected abstract string? FormatSystemMethods(SystemMethods systemMethod); + var constraintName = string.IsNullOrEmpty(columnDefinition.PrimaryKeyName) + ? $"PK_{table.Name}" + : columnDefinition.PrimaryKeyName; - protected abstract string FormatIdentity(ColumnDefinition column); + var columns = string.IsNullOrEmpty(columnDefinition.PrimaryKeyColumns) + ? GetQuotedColumnName(columnDefinition.Name) + : string.Join(", ", columnDefinition.PrimaryKeyColumns + .Split(Constants.CharArrays.CommaSpace, StringSplitOptions.RemoveEmptyEntries) + .Select(GetQuotedColumnName)); - public abstract Sql SelectTop(Sql sql, int top); + var primaryKeyPart = string.Concat("PRIMARY KEY", columnDefinition.IsIndexed ? " CLUSTERED" : " NONCLUSTERED"); - public abstract void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); - - public virtual string DeleteDefaultConstraint => throw new NotSupportedException("Default constraints are not supported"); - - public virtual string CreateTable => "CREATE TABLE {0} ({1})"; - public virtual string DropTable => "DROP TABLE {0}"; - - public virtual string AddColumn => "ALTER TABLE {0} ADD {1}"; - public virtual string DropColumn => "ALTER TABLE {0} DROP COLUMN {1}"; - public virtual string AlterColumn => "ALTER TABLE {0} ALTER COLUMN {1}"; - public virtual string RenameColumn => "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; - - public virtual string RenameTable => "RENAME TABLE {0} TO {1}"; - - public virtual string CreateSchema => "CREATE SCHEMA {0}"; - public virtual string AlterSchema => "ALTER SCHEMA {0} TRANSFER {1}.{2}"; - public virtual string DropSchema => "DROP SCHEMA {0}"; - - public virtual string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; - public virtual string DropIndex => "DROP INDEX {0}"; - - public virtual string InsertData => "INSERT INTO {0} ({1}) VALUES ({2})"; - public virtual string UpdateData => "UPDATE {0} SET {1} WHERE {2}"; - public virtual string DeleteData => "DELETE FROM {0} WHERE {1}"; - public virtual string TruncateTable => "TRUNCATE TABLE {0}"; - - public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; - public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; - public virtual string CreateForeignKeyConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; - 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}, 120)"; - public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; + return string.Format( + CreateConstraint, + GetQuotedTableName(table.Name), + GetQuotedName(constraintName), + primaryKeyPart, + columns); } + + public virtual string FormatColumnRename(string? tableName, string? oldName, string? newName) => + string.Format( + RenameColumn, + GetQuotedTableName(tableName), + GetQuotedColumnName(oldName), + GetQuotedColumnName(newName)); + + public virtual string FormatTableRename(string? oldName, string? newName) => + string.Format(RenameTable, GetQuotedTableName(oldName), GetQuotedTableName(newName)); + + public abstract Sql SelectTop(Sql sql, int top); + + public abstract void HandleCreateTable(IDatabase database, TableDefinition tableDefinition, bool skipKeysAndIndexes = false); + + public virtual string CreateTable => "CREATE TABLE {0} ({1})"; + + public virtual string DropTable => "DROP TABLE {0}"; + + public virtual string AddColumn => "ALTER TABLE {0} ADD {1}"; + + public virtual string DropColumn => "ALTER TABLE {0} DROP COLUMN {1}"; + + public virtual string AlterColumn => "ALTER TABLE {0} ALTER COLUMN {1}"; + + public virtual string RenameColumn => "ALTER TABLE {0} RENAME COLUMN {1} TO {2}"; + + public virtual string RenameTable => "RENAME TABLE {0} TO {1}"; + + public virtual string CreateSchema => "CREATE SCHEMA {0}"; + + public virtual string AlterSchema => "ALTER SCHEMA {0} TRANSFER {1}.{2}"; + + public virtual string DropSchema => "DROP SCHEMA {0}"; + + public virtual string CreateIndex => "CREATE {0}{1}INDEX {2} ON {3} ({4})"; + + public virtual string DropIndex => "DROP INDEX {0}"; + + public virtual string InsertData => "INSERT INTO {0} ({1}) VALUES ({2})"; + + public virtual string UpdateData => "UPDATE {0} SET {1} WHERE {2}"; + + public virtual string DeleteData => "DELETE FROM {0} WHERE {1}"; + + public virtual string TruncateTable => "TRUNCATE TABLE {0}"; + + public virtual string CreateConstraint => "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; + + public virtual string DeleteConstraint => "ALTER TABLE {0} DROP CONSTRAINT {1}"; + + public virtual string ConvertIntegerToOrderableString => "REPLACE(STR({0}, 8), SPACE(1), '0')"; + + public virtual string ConvertDateToOrderableString => "CONVERT(nvarchar, {0}, 120)"; + + public virtual string ConvertDecimalToOrderableString => "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; + + public virtual string GetSpecialDbType(SpecialDbType dbType, int customSize) => + $"{GetSpecialDbType(dbType)}({customSize})"; + + private DbTypes InitColumnTypeMap() + { + var dbTypeMap = new DbTypesFactory(); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.StringFixedLength, StringColumnDefinition); + dbTypeMap.Set(DbType.String, StringColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Boolean, BoolColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.Guid, GuidColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.DateTime, DateTimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.Time, TimeColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + dbTypeMap.Set(DbType.DateTimeOffset, DateTimeOffsetColumnDefinition); + + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.Byte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.SByte, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt16, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.Int32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + dbTypeMap.Set(DbType.UInt32, IntColumnDefinition); + + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.Int64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + dbTypeMap.Set(DbType.UInt64, LongColumnDefinition); + + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Single, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + dbTypeMap.Set(DbType.Double, RealColumnDefinition); + + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + dbTypeMap.Set(DbType.Decimal, DecimalColumnDefinition); + + dbTypeMap.Set(DbType.Binary, BlobColumnDefinition); + + return dbTypeMap.Create(); + } + + protected virtual string FormatCascade(string onWhat, Rule rule) + { + var action = "NO ACTION"; + switch (rule) + { + case Rule.None: + return string.Empty; + case Rule.Cascade: + action = "CASCADE"; + break; + case Rule.SetNull: + action = "SET NULL"; + break; + case Rule.SetDefault: + action = "SET DEFAULT"; + break; + } + + return $" ON {onWhat} {action}"; + } + + protected virtual string FormatString(ColumnDefinition column) => GetQuotedColumnName(column.Name); + + protected virtual string FormatType(ColumnDefinition column) + { + if (column.Type.HasValue == false && string.IsNullOrEmpty(column.CustomType) == false) + { + return column.CustomType; + } + + if (column.CustomDbType.HasValue) + { + if (column.Size != default) + { + return GetSpecialDbType(column.CustomDbType.Value, column.Size); + } + + return GetSpecialDbType(column.CustomDbType.Value); + } + + Type type = column.Type.HasValue + ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key + : column.PropertyType; + + if (type == typeof(string)) + { + var valueOrDefault = column.Size != default ? column.Size : DefaultStringLength; + return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); + } + + if (type == typeof(decimal)) + { + var precision = column.Size != default ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default ? column.Precision : DefaultDecimalScale; + return string.Format(DecimalColumnDefinitionFormat, precision, scale); + } + + var definition = DbTypeMap.ColumnTypeMap[type]; + var dbTypeDefinition = column.Size != default + ? $"{definition}({column.Size})" + : definition; + + // NOTE Precision is left out + return dbTypeDefinition; + } + + protected virtual string FormatNullable(ColumnDefinition column) => column.IsNullable ? "NULL" : "NOT NULL"; + + protected virtual string FormatConstraint(ColumnDefinition column) + { + if (string.IsNullOrEmpty(column.ConstraintName) && column.DefaultValue == null) + { + return string.Empty; + } + + return + $"CONSTRAINT {(string.IsNullOrEmpty(column.ConstraintName) ? GetQuotedName($"DF_{column.TableName}_{column.Name}") : column.ConstraintName)}"; + } + + protected virtual string FormatDefaultValue(ColumnDefinition column) + { + if (column.DefaultValue == null) + { + return string.Empty; + } + + // HACK: probably not needed with latest changes + if (string.Equals(column.DefaultValue.ToString(), "GETDATE()", StringComparison.OrdinalIgnoreCase)) + { + column.DefaultValue = SystemMethods.CurrentDateTime; + } + + // see if this is for a system method + if (column.DefaultValue is SystemMethods) + { + var method = FormatSystemMethods((SystemMethods)column.DefaultValue); + return string.IsNullOrEmpty(method) ? string.Empty : string.Format(DefaultValueFormat, method); + } + + return string.Format(DefaultValueFormat, GetQuotedValue(column.DefaultValue.ToString()!)); + } + + protected virtual string FormatPrimaryKey(ColumnDefinition column) => string.Empty; + + protected abstract string? FormatSystemMethods(SystemMethods systemMethod); + + protected abstract string FormatIdentity(ColumnDefinition column); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 6c816f7c92..f1622c5480 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -1,57 +1,50 @@ -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; -namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax +namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; + +internal enum WhereInType { - internal static class SqlSyntaxProviderExtensions - { - public static IEnumerable GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db) - { - return sql.GetDefinedIndexes(db) - .Select(x => new DbIndexDefinition(x)).ToArray(); - } - - /// - /// Returns the quotes tableName.columnName combo - /// - /// - /// - /// - /// - public static string GetQuotedColumn(this ISqlSyntaxProvider sql, string tableName, string columnName) - { - return sql.GetQuotedTableName(tableName) + "." + sql.GetQuotedColumnName(columnName); - } - - /// - /// This is used to generate a delete query that uses a sub-query to select the data, it is required because there's a very particular syntax that - /// needs to be used to work for all servers - /// - /// - /// - /// See: http://issues.umbraco.org/issue/U4-3876 - /// - public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, Sql subQuery, WhereInType whereInType = WhereInType.In) - { - //TODO: This is no longer necessary since this used to be a specific requirement for MySql! - // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example - - return - new Sql(string.Format( - whereInType == WhereInType.In - ? @"DELETE FROM {0} WHERE {1} IN (SELECT {1} FROM ({2}) x)" - : @"DELETE FROM {0} WHERE {1} NOT IN (SELECT {1} FROM ({2}) x)", - sqlProvider.GetQuotedTableName(tableName), - sqlProvider.GetQuotedColumnName(columnName), - subQuery.SQL), subQuery.Arguments); - } - } - - internal enum WhereInType - { - In, - NotIn - } + In, + NotIn, +} + +internal static class SqlSyntaxProviderExtensions +{ + public static IEnumerable + GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, IDatabase db) => + sql.GetDefinedIndexes(db) + .Select(x => new DbIndexDefinition(x)).ToArray(); + + /// + /// Returns the quotes tableName.columnName combo + /// + /// + /// + /// + /// + public static string GetQuotedColumn(this ISqlSyntaxProvider sql, string tableName, string columnName) => + sql.GetQuotedTableName(tableName) + "." + sql.GetQuotedColumnName(columnName); + + /// + /// This is used to generate a delete query that uses a sub-query to select the data, it is required because there's a + /// very particular syntax that + /// needs to be used to work for all servers + /// + /// + /// + /// See: http://issues.umbraco.org/issue/U4-3876 + /// + public static Sql GetDeleteSubquery(this ISqlSyntaxProvider sqlProvider, string tableName, string columnName, Sql subQuery, WhereInType whereInType = WhereInType.In) => + + // TODO: This is no longer necessary since this used to be a specific requirement for MySql! + // Now we can do a Delete + sub query, see RelationRepository.DeleteByParent for example + new Sql( + string.Format( + whereInType == WhereInType.In + ? @"DELETE FROM {0} WHERE {1} IN (SELECT {1} FROM ({2}) x)" + : @"DELETE FROM {0} WHERE {1} NOT IN (SELECT {1} FROM ({2}) x)", + sqlProvider.GetQuotedTableName(tableName), + sqlProvider.GetQuotedColumnName(columnName), + subQuery.SQL), subQuery.Arguments); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs index fca90e9048..4ab8968389 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntaxExtensions.cs @@ -1,40 +1,40 @@ -using System; using System.Linq.Expressions; using System.Reflection; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to . +/// +public static class SqlSyntaxExtensions { /// - /// Provides extension methods to . + /// Gets a quoted table and field name. /// - public static class SqlSyntaxExtensions + /// The type of the DTO. + /// An . + /// An expression specifying the field. + /// An optional table alias. + /// + public static string GetFieldName( + this ISqlSyntaxProvider sqlSyntax, + Expression> fieldSelector, string? tableAlias = null) { - private static string GetColumnName(this PropertyInfo column) - { - var attr = column.FirstAttribute(); - return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; - } + var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; + var fieldName = field?.GetColumnName(); - /// - /// Gets a quoted table and field name. - /// - /// The type of the DTO. - /// An . - /// An expression specifying the field. - /// An optional table alias. - /// - public static string GetFieldName(this ISqlSyntaxProvider sqlSyntax, Expression> fieldSelector, string? tableAlias = null) - { - var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo; - var fieldName = field?.GetColumnName(); + Type type = typeof(TDto); + var tableName = tableAlias ?? type.GetTableName(); - var type = typeof(TDto); - var tableName = tableAlias ?? type.GetTableName(); + return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName); + } - return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName); - } + private static string GetColumnName(this PropertyInfo column) + { + ColumnAttribute? attr = column.FirstAttribute(); + return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name; } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs b/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs index 716b24bb07..ae2fa58f02 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlTemplate.cs @@ -1,124 +1,131 @@ -using System; using System.Collections; -using System.Collections.Generic; -using System.Linq; using NPoco; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public class SqlTemplate { - public class SqlTemplate + private readonly Dictionary? _args; + private readonly string _sql; + private readonly ISqlContext _sqlContext; + + internal SqlTemplate(ISqlContext sqlContext, string sql, object[] args) { - private readonly ISqlContext _sqlContext; - private readonly string _sql; - private readonly Dictionary? _args; - - // these are created in PocoToSqlExpressionVisitor - internal class TemplateArg + _sqlContext = sqlContext; + _sql = sql; + if (args.Length > 0) { - public TemplateArg(string? name) - { - Name = name; - } - - public string? Name { get; } - - public override string ToString() - { - return "@" + Name; - } + _args = new Dictionary(); } - internal SqlTemplate(ISqlContext sqlContext, string sql, object[] args) + for (var i = 0; i < args.Length; i++) { - _sqlContext = sqlContext; - _sql = sql; - if (args.Length > 0) - _args = new Dictionary(); - for (var i = 0; i < args.Length; i++) - _args![i] = args[i]; - } - - public Sql Sql() - { - return new Sql(_sqlContext, _sql); - } - - // must pass the args, all of them, in the proper order, faster - public Sql Sql(params object[] args) - { - // if the type is an "unspeakable name" it is an anonymous compiler-generated object - // see https://stackoverflow.com/questions/9256594 - // => assume it's an anonymous type object containing named arguments - // (of course this means we cannot use *real* objects here and need SqlNamed - bah) - if (args.Length == 1 && args[0].GetType().Name.Contains("<")) - return SqlNamed(args[0]); - - if (args.Length != _args?.Count) - throw new ArgumentException("Invalid number of arguments.", nameof(args)); - - if (args.Length == 0) - return new Sql(_sqlContext, true, _sql); - - var isBuilt = !args.Any(x => x is IEnumerable); - return new Sql(_sqlContext, isBuilt, _sql, args); - } - - // can pass named args, not necessary all of them, slower - // so, not much different from what Where(...) does (ie reflection) - public Sql SqlNamed(object nargs) - { - var isBuilt = true; - var args = new object[_args?.Count ?? 0]; - var properties = nargs.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(nargs)); - for (var i = 0; i < _args?.Count; i++) - { - object? value; - if (_args[i] is TemplateArg templateArg) - { - if (!properties.TryGetValue(templateArg.Name ?? string.Empty, out value)) - throw new InvalidOperationException($"Missing argument \"{templateArg.Name}\"."); - properties.Remove(templateArg.Name!); - } - else - { - value = _args[i]; - } - - args[i] = value!; - - // if value is enumerable then we'll need to expand arguments - if (value is IEnumerable) - isBuilt = false; - } - if (properties.Count > 0) - throw new InvalidOperationException($"Unknown argument{(properties.Count > 1 ? "s" : "")}: {string.Join(", ", properties.Keys)}"); - return new Sql(_sqlContext, isBuilt, _sql, args); - } - - internal string ToText() - { - var sql = new Sql(_sqlContext, _sql, _args?.Values.ToArray()); - return sql.ToText(); - } - - /// - /// Gets a named argument. - /// - public static object Arg(string name) => new TemplateArg(name); - - /// - /// Gets a WHERE expression argument. - /// - public static T? Arg(string name) => default; - - /// - /// Gets a WHERE IN expression argument. - /// - public static IEnumerable ArgIn(string name) - { - // don't return an empty enumerable, as it breaks NPoco - return new[] { default (T) }; + _args![i] = args[i]; } } + + /// + /// Gets a named argument. + /// + public static object Arg(string name) => new TemplateArg(name); + + public Sql Sql() => new Sql(_sqlContext, _sql); + + // must pass the args, all of them, in the proper order, faster + public Sql Sql(params object[] args) + { + // if the type is an "unspeakable name" it is an anonymous compiler-generated object + // see https://stackoverflow.com/questions/9256594 + // => assume it's an anonymous type object containing named arguments + // (of course this means we cannot use *real* objects here and need SqlNamed - bah) + if (args.Length == 1 && args[0].GetType().Name.Contains("<")) + { + return SqlNamed(args[0]); + } + + if (args.Length != _args?.Count) + { + throw new ArgumentException("Invalid number of arguments.", nameof(args)); + } + + if (args.Length == 0) + { + return new Sql(_sqlContext, true, _sql); + } + + var isBuilt = !args.Any(x => x is IEnumerable); + return new Sql(_sqlContext, isBuilt, _sql, args); + } + + // can pass named args, not necessary all of them, slower + // so, not much different from what Where(...) does (ie reflection) + public Sql SqlNamed(object nargs) + { + var isBuilt = true; + var args = new object[_args?.Count ?? 0]; + var properties = nargs.GetType().GetProperties().ToDictionary(x => x.Name, x => x.GetValue(nargs)); + for (var i = 0; i < _args?.Count; i++) + { + object? value; + if (_args[i] is TemplateArg templateArg) + { + if (!properties.TryGetValue(templateArg.Name ?? string.Empty, out value)) + { + throw new InvalidOperationException($"Missing argument \"{templateArg.Name}\"."); + } + + properties.Remove(templateArg.Name!); + } + else + { + value = _args[i]; + } + + args[i] = value!; + + // if value is enumerable then we'll need to expand arguments + if (value is IEnumerable) + { + isBuilt = false; + } + } + + if (properties.Count > 0) + { + throw new InvalidOperationException( + $"Unknown argument{(properties.Count > 1 ? "s" : string.Empty)}: {string.Join(", ", properties.Keys)}"); + } + + return new Sql(_sqlContext, isBuilt, _sql, args); + } + + internal string ToText() + { + var sql = new Sql(_sqlContext, _sql, _args?.Values.ToArray()); + return sql.ToText(); + } + + /// + /// Gets a WHERE expression argument. + /// + public static T? Arg(string name) => default; + + /// + /// Gets a WHERE IN expression argument. + /// + public static IEnumerable ArgIn(string name) => + + // don't return an empty enumerable, as it breaks NPoco + new[] { default(T) }; + + // these are created in PocoToSqlExpressionVisitor + internal class TemplateArg + { + public TemplateArg(string? name) => Name = name; + + public string? Name { get; } + + public override string ToString() => "@" + Name; + } } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs b/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs index 3c180b439f..6fc0148934 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlTemplates.cs @@ -1,34 +1,26 @@ -using System; using System.Collections.Concurrent; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +public class SqlTemplates { - public class SqlTemplates + private readonly ISqlContext _sqlContext; + private readonly ConcurrentDictionary _templates = new(); + + public SqlTemplates(ISqlContext sqlContext) => _sqlContext = sqlContext; + + public SqlTemplate Get(string key, Func, Sql> sqlBuilder) { - private readonly ConcurrentDictionary _templates = new ConcurrentDictionary(); - private readonly ISqlContext _sqlContext; - - public SqlTemplates(ISqlContext sqlContext) + SqlTemplate CreateTemplate(string _) { - _sqlContext = sqlContext; + Sql sql = sqlBuilder(new Sql(_sqlContext)); + return new SqlTemplate(_sqlContext, sql.SQL, sql.Arguments); } - // for tests - internal void Clear() - { - _templates.Clear(); - } - - public SqlTemplate Get(string key, Func, Sql> sqlBuilder) - { - SqlTemplate CreateTemplate(string _) - { - var sql = sqlBuilder(new Sql(_sqlContext)); - return new SqlTemplate(_sqlContext, sql.SQL, sql.Arguments); - } - - return _templates.GetOrAdd(key, CreateTemplate); - } + return _templates.GetOrAdd(key, CreateTemplate); } + + // for tests + internal void Clear() => _templates.Clear(); } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index 83ab603a35..e0542874d0 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -1,204 +1,201 @@ -using System; -using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Linq; using System.Text; using Microsoft.Extensions.Logging; using NPoco; -using StackExchange.Profiling; using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Extends NPoco Database for Umbraco. +/// +/// +/// +/// Is used everywhere in place of the original NPoco Database object, and provides additional features +/// such as profiling, retry policies, logging, etc. +/// +/// Is never created directly but obtained from the . +/// +public class UmbracoDatabase : Database, IUmbracoDatabase { + private readonly ILogger _logger; + private readonly IBulkSqlInsertProvider? _bulkSqlInsertProvider; + private readonly DatabaseSchemaCreatorFactory? _databaseSchemaCreatorFactory; + private readonly IEnumerable? _mapperCollection; + private readonly Guid _instanceGuid = Guid.NewGuid(); + private List? _commands; + + #region Ctor /// - /// Extends NPoco Database for Umbraco. + /// Initializes a new instance of the class. /// /// - /// Is used everywhere in place of the original NPoco Database object, and provides additional features - /// such as profiling, retry policies, logging, etc. - /// Is never created directly but obtained from the . + /// Used by UmbracoDatabaseFactory to create databases. + /// Also used by DatabaseBuilder for creating databases and installing/upgrading. /// - public class UmbracoDatabase : Database, IUmbracoDatabase + public UmbracoDatabase( + string connectionString, + ISqlContext sqlContext, + DbProviderFactory provider, + ILogger logger, + IBulkSqlInsertProvider? bulkSqlInsertProvider, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + IEnumerable? mapperCollection = null) + : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { - private readonly ILogger _logger; - private readonly IBulkSqlInsertProvider? _bulkSqlInsertProvider; - private readonly DatabaseSchemaCreatorFactory? _databaseSchemaCreatorFactory; - private readonly IEnumerable? _mapperCollection; - private readonly Guid _instanceGuid = Guid.NewGuid(); - private List? _commands; + SqlContext = sqlContext; + _logger = logger; + _bulkSqlInsertProvider = bulkSqlInsertProvider; + _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; + _mapperCollection = mapperCollection; - #region Ctor + Init(); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// 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, - IEnumerable? mapperCollection = null) - : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) + /// + /// Initializes a new instance of the class. + /// + /// Internal for unit tests only. + 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) { - SqlContext = sqlContext; - _logger = logger; - _bulkSqlInsertProvider = bulkSqlInsertProvider; - _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; - _mapperCollection = mapperCollection; - - Init(); + Mappers.AddRange(_mapperCollection); } + } - /// - /// Initializes a new instance of the class. - /// - /// Internal for unit tests only. - internal UmbracoDatabase( - DbConnection connection, - ISqlContext sqlContext, - ILogger logger, - IBulkSqlInsertProvider bulkSqlInsertProvider) - : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) - { - SqlContext = sqlContext; - _logger = logger; - _bulkSqlInsertProvider = bulkSqlInsertProvider; + #endregion - Init(); - } + /// + public ISqlContext SqlContext { get; } - private void Init() - { - EnableSqlTrace = EnableSqlTraceDefault; - NPocoDatabaseExtensions.ConfigureNPocoBulkExtensions(); + #region Testing, Debugging and Troubleshooting - if (_mapperCollection != null) - { - Mappers.AddRange(_mapperCollection); - } - } - - #endregion - - /// - public ISqlContext SqlContext { get; } - - #region Testing, Debugging and Troubleshooting - - private bool _enableCount; + private bool _enableCount; #if DEBUG_DATABASES private int _spid = -1; private const bool EnableSqlTraceDefault = true; #else - private string? _instanceId; - private const bool EnableSqlTraceDefault = false; + private string? _instanceId; + private const bool EnableSqlTraceDefault = false; #endif - /// - public string InstanceId => + /// + public string InstanceId => #if DEBUG_DATABASES _instanceGuid.ToString("N").Substring(0, 8) + ':' + _spid; #else - _instanceId ??= _instanceGuid.ToString("N").Substring(0, 8); + _instanceId ??= _instanceGuid.ToString("N").Substring(0, 8); #endif - /// - public bool InTransaction { get; private set; } + /// + public bool InTransaction { get; private set; } - protected override void OnBeginTransaction() + protected override void OnBeginTransaction() + { + base.OnBeginTransaction(); + InTransaction = true; + } + + protected override void OnAbortTransaction() + { + InTransaction = false; + base.OnAbortTransaction(); + } + + protected override void OnCompleteTransaction() + { + InTransaction = false; + base.OnCompleteTransaction(); + } + + /// + /// Gets or sets a value indicating whether to log all executed Sql statements. + /// + internal bool EnableSqlTrace { get; set; } + + /// + /// Gets or sets a value indicating whether to count all executed Sql statements. + /// + public bool EnableSqlCount + { + get => _enableCount; + set { - base.OnBeginTransaction(); - InTransaction = true; - } + _enableCount = value; - protected override void OnAbortTransaction() - { - InTransaction = false; - base.OnAbortTransaction(); - } - - protected override void OnCompleteTransaction() - { - InTransaction = false; - base.OnCompleteTransaction(); - } - - /// - /// Gets or sets a value indicating whether to log all executed Sql statements. - /// - internal bool EnableSqlTrace { get; set; } - - /// - /// Gets or sets a value indicating whether to count all executed Sql statements. - /// - public bool EnableSqlCount - { - get => _enableCount; - set + if (_enableCount == false) { - _enableCount = value; - - if (_enableCount == false) - { - SqlCount = 0; - } + SqlCount = 0; } } + } - /// - /// Gets the count of all executed Sql statements. - /// - public int SqlCount { get; private set; } + /// + /// Gets the count of all executed Sql statements. + /// + public int SqlCount { get; private set; } - internal bool LogCommands + internal bool LogCommands + { + get => _commands != null; + set => _commands = value ? new List() : null; + } + + internal IEnumerable? Commands => _commands; + + public int BulkInsertRecords(IEnumerable records) => + _bulkSqlInsertProvider?.BulkInsertRecords(this, records) ?? 0; + + /// + /// Returns the for the database + /// + public DatabaseSchemaResult ValidateSchema() + { + DatabaseSchemaCreator? dbSchema = _databaseSchemaCreatorFactory?.Create(this); + DatabaseSchemaResult? databaseSchemaValidationResult = dbSchema?.ValidateSchema(); + + return databaseSchemaValidationResult ?? new DatabaseSchemaResult(); + } + + /// + /// Returns true if Umbraco database tables are detected to be installed + /// + public bool IsUmbracoInstalled() => ValidateSchema().DetermineHasInstalledVersion(); + + #endregion + + #region OnSomething + + protected override DbConnection OnConnectionOpened(DbConnection connection) + { + if (connection == null) { - get => _commands != null; - set => _commands = value ? new List() : null; + throw new ArgumentNullException(nameof(connection)); } - internal IEnumerable? Commands => _commands; - - public int BulkInsertRecords(IEnumerable records) => _bulkSqlInsertProvider?.BulkInsertRecords(this, records) ?? 0; - - /// - /// Returns the for the database - /// - public DatabaseSchemaResult ValidateSchema() - { - var dbSchema = _databaseSchemaCreatorFactory?.Create(this); - var databaseSchemaValidationResult = dbSchema?.ValidateSchema(); - - return databaseSchemaValidationResult ?? new DatabaseSchemaResult(); - } - - /// - /// Returns true if Umbraco database tables are detected to be installed - /// - public bool IsUmbracoInstalled() => ValidateSchema().DetermineHasInstalledVersion(); - - #endregion - - #region OnSomething - - protected override DbConnection OnConnectionOpened(DbConnection connection) - { - if (connection == null) - { - throw new ArgumentNullException(nameof(connection)); - } - - // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. + // TODO: this should probably move to a SQL Server ProviderSpecificInterceptor. #if DEBUG_DATABASES // determines the database connection SPID for debugging if (DatabaseType.IsSqlServer()) @@ -216,8 +213,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence } #endif - return connection; - } + return connection; + } #if DEBUG_DATABASES protected override void OnConnectionClosing(DbConnection conn) @@ -227,27 +224,33 @@ namespace Umbraco.Cms.Infrastructure.Persistence } #endif - protected override void OnException(Exception ex) + protected override void OnException(Exception ex) + { + _logger.LogError(ex, "Exception ({InstanceId}).", InstanceId); + _logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace); + + if (EnableSqlTrace == false) { - _logger.LogError(ex, "Exception ({InstanceId}).", InstanceId); - _logger.LogDebug("At:\r\n{StackTrace}", Environment.StackTrace); - - if (EnableSqlTrace == false) - _logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); - - base.OnException(ex); + _logger.LogDebug("Sql:\r\n{Sql}", CommandToString(LastSQL, LastArgs)); } - private DbCommand? _cmd; + base.OnException(ex); + } - protected override void OnExecutingCommand(DbCommand cmd) + private DbCommand? _cmd; + + protected override void OnExecutingCommand(DbCommand cmd) + { + // if no timeout is specified, and the connection has a longer timeout, use it + if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection?.ConnectionTimeout > 30) { - // if no timeout is specified, and the connection has a longer timeout, use it - if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection?.ConnectionTimeout > 30) - cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + } - if (EnableSqlTrace) - _logger.LogDebug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin + if (EnableSqlTrace) + { + _logger.LogDebug("SQL Trace:\r\n{Sql}", CommandToString(cmd).Replace("{", "{{").Replace("}", "}}")); // TODO: these escapes should be builtin + } #if DEBUG_DATABASES // detects whether the command is already in use (eg still has an open reader...) @@ -256,99 +259,105 @@ namespace Umbraco.Cms.Infrastructure.Persistence if (refsobj != null) _logger.LogDebug("Oops!" + Environment.NewLine + refsobj); #endif - _cmd = cmd; + _cmd = cmd; - base.OnExecutingCommand(cmd); - } + base.OnExecutingCommand(cmd); + } - private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, cmd.Parameters.Cast().Select(x => x.Value).WhereNotNull().ToArray()); + private string CommandToString(DbCommand cmd) => CommandToString(cmd.CommandText, cmd.Parameters.Cast().Select(x => x.Value).WhereNotNull().ToArray()); - private string CommandToString(string? sql, object[]? args) - { - var text = new StringBuilder(); + private string CommandToString(string? sql, object[]? args) + { + var text = new StringBuilder(); #if DEBUG_DATABASES text.Append(InstanceId); text.Append(": "); #endif - NPocoSqlExtensions.ToText(sql, args, text); + NPocoSqlExtensions.ToText(sql, args, text); - return text.ToString(); + return text.ToString(); + } + + protected override void OnExecutedCommand(DbCommand cmd) + { + if (_enableCount) + { + SqlCount++; } - protected override void OnExecutedCommand(DbCommand cmd) + _commands?.Add(new CommandInfo(cmd)); + + base.OnExecutedCommand(cmd); + } + + #endregion + + // used for tracking commands + public class CommandInfo + { + public CommandInfo(IDbCommand cmd) { - if (_enableCount) - SqlCount++; - - _commands?.Add(new CommandInfo(cmd)); - - base.OnExecutedCommand(cmd); - } - - #endregion - - // used for tracking commands - public class CommandInfo - { - public CommandInfo(IDbCommand cmd) + Text = cmd.CommandText; + var parameters = new List(); + foreach (IDbDataParameter parameter in cmd.Parameters) { - Text = cmd.CommandText; - var parameters = new List(); - foreach (IDbDataParameter parameter in cmd.Parameters) - parameters.Add(new ParameterInfo(parameter)); - - Parameters = parameters.ToArray(); + parameters.Add(new ParameterInfo(parameter)); } - public string Text { get; } - - public ParameterInfo[] Parameters { get; } + Parameters = parameters.ToArray(); } - // used for tracking commands - public class ParameterInfo + public string Text { get; } + + public ParameterInfo[] Parameters { get; } + } + + // used for tracking commands + public class ParameterInfo + { + public ParameterInfo(IDbDataParameter parameter) { - public ParameterInfo(IDbDataParameter parameter) - { - Name = parameter.ParameterName; - Value = parameter.Value; - DbType = parameter.DbType; - Size = parameter.Size; - } - - public string Name { get; } - public object? Value { get; } - public DbType DbType { get; } - public int Size { get; } + Name = parameter.ParameterName; + Value = parameter.Value; + DbType = parameter.DbType; + Size = parameter.Size; } - /// - public new T ExecuteScalar(string sql, params object[] args) - => ExecuteScalar(new Sql(sql, args)); + public string Name { get; } - /// - public new T ExecuteScalar(Sql sql) - => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); + public object? Value { get; } - /// - /// - /// Be nice if handled upstream GH issue - /// - public new T ExecuteScalar(string sql, CommandType commandType, params object[] args) + public DbType DbType { get; } + + public int Size { get; } + } + + /// + public new T ExecuteScalar(string sql, params object[] args) + => ExecuteScalar(new Sql(sql, args)); + + /// + public new T ExecuteScalar(Sql sql) + => ExecuteScalar(sql.SQL, CommandType.Text, sql.Arguments); + + /// + /// + /// Be nice if handled upstream GH issue + /// + public new T ExecuteScalar(string sql, CommandType commandType, params object[] args) + { + if (SqlContext.SqlSyntax.ScalarMappers == null) { - if (SqlContext.SqlSyntax.ScalarMappers == null) - { - return base.ExecuteScalar(sql, commandType, args); - } - - if (!SqlContext.SqlSyntax.ScalarMappers.TryGetValue(typeof(T), out IScalarMapper? mapper)) - { - return base.ExecuteScalar(sql, commandType, args); - } - - var result = base.ExecuteScalar(sql, commandType, args); - return (T)mapper.Map(result); + return base.ExecuteScalar(sql, commandType, args); } + + if (!SqlContext.SqlSyntax.ScalarMappers.TryGetValue(typeof(T), out IScalarMapper? mapper)) + { + return base.ExecuteScalar(sql, commandType, args); + } + + var result = base.ExecuteScalar(sql, commandType, args); + return (T)mapper.Map(result); } } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index fff81a322d..78bcc34f2b 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -1,75 +1,76 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using NPoco; using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +internal static class UmbracoDatabaseExtensions { - internal static class UmbracoDatabaseExtensions + public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) { - public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) + if (database is not UmbracoDatabase asDatabase) { - if (database is not UmbracoDatabase asDatabase) - { - throw new Exception("oops: database."); - } - - return asDatabase; + throw new Exception("oops: database."); } - /// - /// Gets a dictionary of key/values directly from the database, no scope, nothing. - /// - /// Used by to determine the runtime state. - public static IReadOnlyDictionary? GetFromKeyValueTable(this IUmbracoDatabase database, string keyPrefix) - { - if (database is null) return null; - - // create the wildcard where clause - ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; - var whereParam = sqlSyntax.GetStringColumnWildcardComparison( - sqlSyntax.GetQuotedColumnName("key"), - 0, - TextColumnType.NVarchar); - - var sql = database.SqlContext.Sql() - .Select() - .From() - .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); - - return database.Fetch(sql) - .ToDictionary(x => x.Key!, x => x.Value); - } - - - /// - /// Returns true if the database contains the specified table - /// - /// - /// - /// - public static bool HasTable(this IUmbracoDatabase database, string tableName) - { - try - { - return database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any(table => table.InvariantEquals(tableName)); - } - catch (Exception) - { - return false; // will occur if the database cannot connect - } - } - - /// - /// Returns true if the database contains no tables - /// - /// - /// - public static bool IsDatabaseEmpty(this IUmbracoDatabase database) - => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; - + return asDatabase; } + + /// + /// Gets a dictionary of key/values directly from the database, no scope, nothing. + /// + /// Used by to determine the runtime state. + public static IReadOnlyDictionary? GetFromKeyValueTable( + this IUmbracoDatabase? database, + string keyPrefix) + { + if (database is null) + { + return null; + } + + // create the wildcard where clause + ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; + var whereParam = sqlSyntax.GetStringColumnWildcardComparison( + sqlSyntax.GetQuotedColumnName("key"), + 0, + TextColumnType.NVarchar); + + Sql? sql = database.SqlContext.Sql() + .Select() + .From() + .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); + + return database.Fetch(sql) + .ToDictionary(x => x.Key, x => x.Value); + } + + /// + /// Returns true if the database contains the specified table + /// + /// + /// + /// + public static bool HasTable(this IUmbracoDatabase database, string tableName) + { + try + { + return database.SqlContext.SqlSyntax.GetTablesInSchema(database) + .Any(table => table.InvariantEquals(tableName)); + } + catch (Exception) + { + return false; // will occur if the database cannot connect + } + } + + /// + /// Returns true if the database contains no tables + /// + /// + /// + public static bool IsDatabaseEmpty(this IUmbracoDatabase database) + => database.SqlContext.SqlSyntax.GetTablesInSchema(database).Any() == false; } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index 6257f7367f..7530ab7854 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -1,301 +1,302 @@ -using System; using System.Data.Common; -using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; using NPoco.FluentMappings; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Migrations.Install; -using Umbraco.Cms.Infrastructure.Persistence.FaultHandling; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; +using MapperCollection = NPoco.MapperCollection; -namespace Umbraco.Cms.Infrastructure.Persistence +namespace Umbraco.Cms.Infrastructure.Persistence; + +/// +/// Default implementation of . +/// +/// +/// +/// This factory implementation creates and manages an "ambient" database connection. When running +/// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to +/// the current thread". In this latter case, note that the database connection object is not thread safe. +/// +/// +/// It wraps an NPoco UmbracoDatabaseFactory which is initializes with a proper IPocoDataFactory to ensure +/// that NPoco's plumbing is cached appropriately for the whole application. +/// +/// +// TODO: these comments are not true anymore +// TODO: this class needs not be disposable! +public class UmbracoDatabaseFactory : DisposableObjectSlim, IUmbracoDatabaseFactory { + private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; + private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; + private readonly IOptions _globalSettings; + private readonly ILogger _logger; + private readonly ILoggerFactory _loggerFactory; + private readonly IMapperCollection _mappers; + private readonly NPocoMapperCollection _npocoMappers; + private IBulkSqlInsertProvider? _bulkSqlInsertProvider; + private DatabaseType? _databaseType; + + private DbProviderFactory? _dbProviderFactory; + private bool _initialized; + + private object _lock = new(); + + private DatabaseFactory? _npocoDatabaseFactory; + private IPocoDataFactory? _pocoDataFactory; + private MapperCollection? _pocoMappers; + private SqlContext _sqlContext = null!; + private ISqlSyntaxProvider? _sqlSyntax; + + private ConnectionStrings? _umbracoConnectionString; + private bool _upgrading; + + #region Constructors /// - /// Default implementation of . + /// Initializes a new instance of the . /// - /// - /// This factory implementation creates and manages an "ambient" database connection. When running - /// within an Http context, "ambient" means "associated with that context". Otherwise, it means "static to - /// the current thread". In this latter case, note that the database connection object is not thread safe. - /// It wraps an NPoco UmbracoDatabaseFactory which is initializes with a proper IPocoDataFactory to ensure - /// that NPoco's plumbing is cached appropriately for the whole application. - /// - // TODO: these comments are not true anymore - // TODO: this class needs not be disposable! - public class UmbracoDatabaseFactory : DisposableObjectSlim, IUmbracoDatabaseFactory + /// Used by the other ctor and in tests. + public UmbracoDatabaseFactory( + ILogger logger, + ILoggerFactory loggerFactory, + IOptions globalSettings, + IOptionsMonitor connectionStrings, + IMapperCollection mappers, + IDbProviderFactoryCreator dbProviderFactoryCreator, + DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, + NPocoMapperCollection npocoMappers) { - private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; - private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; - private readonly NPocoMapperCollection _npocoMappers; - private readonly IOptions _globalSettings; - private readonly IMapperCollection _mappers; - private readonly ILogger _logger; - private readonly ILoggerFactory _loggerFactory; + _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; - private object _lock = new object(); - - private DatabaseFactory? _npocoDatabaseFactory; - private IPocoDataFactory? _pocoDataFactory; - private DatabaseType? _databaseType; - private ISqlSyntaxProvider? _sqlSyntax; - private IBulkSqlInsertProvider? _bulkSqlInsertProvider; - private NPoco.MapperCollection? _pocoMappers; - private SqlContext _sqlContext = null!; - private bool _upgrading; - private bool _initialized; - - private ConnectionStrings? _umbracoConnectionString; - - private DbProviderFactory? _dbProviderFactory = null; - - private DbProviderFactory? DbProviderFactory + ConnectionStrings umbracoConnectionString = connectionStrings.CurrentValue; + if (!umbracoConnectionString.IsConnectionStringConfigured()) { - get - { - if (_dbProviderFactory == null) - { - _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) - ? null - : _dbProviderFactoryCreator.CreateFactory(ProviderName); - } - - return _dbProviderFactory; - } + logger.LogDebug("Missing connection string, defer configuration."); + return; // not configured } - #region Constructors + Configure(umbracoConnectionString); + } + #endregion - - - /// - /// Initializes a new instance of the . - /// - /// Used by the other ctor and in tests. - public UmbracoDatabaseFactory( - ILogger logger, - ILoggerFactory loggerFactory, - IOptions globalSettings, - IOptionsMonitor connectionStrings, - IMapperCollection mappers, - IDbProviderFactoryCreator dbProviderFactoryCreator, - DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - NPocoMapperCollection npocoMappers) + private DbProviderFactory? DbProviderFactory + { + get { - _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; - - ConnectionStrings umbracoConnectionString = connectionStrings.CurrentValue; - if (!umbracoConnectionString.IsConnectionStringConfigured()) + if (_dbProviderFactory == null) { - logger.LogDebug("Missing connection string, defer configuration."); - return; // not configured + _dbProviderFactory = string.IsNullOrWhiteSpace(ProviderName) + ? null + : _dbProviderFactoryCreator.CreateFactory(ProviderName); } - Configure(umbracoConnectionString); - } - - #endregion - - /// - public bool Configured - { - get - { - lock (_lock) - { - return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); - } - } - } - - /// - public bool Initialized => Volatile.Read(ref _initialized); - - /// - public string? ConnectionString => _umbracoConnectionString?.ConnectionString; - - /// - public string? ProviderName => _umbracoConnectionString?.ProviderName; - - /// - public bool CanConnect => - // actually tries to connect to the database (regardless of configured/initialized) - !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && - DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); - - /// - public ISqlContext SqlContext - { - get - { - // must be initialized to have a context - EnsureInitialized(); - - return _sqlContext; - } - } - - /// - public IBulkSqlInsertProvider? BulkSqlInsertProvider - { - get - { - // must be initialized to have a bulk insert provider - EnsureInitialized(); - - return _bulkSqlInsertProvider; - } - } - - /// - public void ConfigureForUpgrade() => _upgrading = true; - - /// - public void Configure(ConnectionStrings umbracoConnectionString) - { - if (umbracoConnectionString is null) - { - throw new ArgumentNullException(nameof(umbracoConnectionString)); - } - - lock (_lock) - { - if (Volatile.Read(ref _initialized)) - { - throw new InvalidOperationException("Already initialized."); - } - - _umbracoConnectionString = umbracoConnectionString; - } - - // rest to be lazy-initialized - } - - 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 (DbProviderFactory == null) - { - throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); - } - - _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!); - - _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); - - // 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(); - // add all registered mappers for NPoco - _pocoMappers.AddRange(_npocoMappers); - - _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName!)); - - var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); - _pocoDataFactory = factory; - var config = new FluentConfig(xmappers => factory); - - // create the database factory - _npocoDatabaseFactory = DatabaseFactory.Config(cfg => - { - cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances - .WithFluentConfig(config); // with proper configuration - - foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator.GetProviderSpecificInterceptors(ProviderName!)) - { - cfg.WithInterceptor(interceptor); - } - }); - - if (_npocoDatabaseFactory == null) - { - throw new NullReferenceException("The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); - } - - _logger.LogDebug("Initialized."); - - return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); - } - - /// - public IUmbracoDatabase CreateDatabase() - { - // must be initialized to create a database - EnsureInitialized(); - return (IUmbracoDatabase) _npocoDatabaseFactory!.GetDatabase(); - } - - // gets initialized poco data builders - private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) - => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); - - // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance - private UmbracoDatabase? CreateDatabaseInstance() - { - if (ConnectionString is null || SqlContext is null || DbProviderFactory is null) - { - return null; - } - - return new UmbracoDatabase( - ConnectionString, - SqlContext, - DbProviderFactory, - _loggerFactory.CreateLogger(), - _bulkSqlInsertProvider, - _databaseSchemaCreatorFactory, - _pocoMappers); - } - - protected override void DisposeResources() - { - // this is weird, because hybrid accessors store different databases per - // thread, so we don't really know what we are disposing here... - // besides, we don't really want to dispose the factory, which is a singleton... - - // TODO: the class does not need be disposable - //var db = _umbracoDatabaseAccessor.UmbracoDatabase; - //_umbracoDatabaseAccessor.UmbracoDatabase = null; - //db?.Dispose(); - Volatile.Write(ref _initialized, false); + return _dbProviderFactory; } } + + /// + public bool Configured + { + get + { + lock (_lock) + { + return !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace(); + } + } + } + + /// + public bool Initialized => Volatile.Read(ref _initialized); + + /// + public string? ConnectionString => _umbracoConnectionString?.ConnectionString; + + /// + public string? ProviderName => _umbracoConnectionString?.ProviderName; + + /// + public bool CanConnect => + + // actually tries to connect to the database (regardless of configured/initialized) + !ConnectionString.IsNullOrWhiteSpace() && !ProviderName.IsNullOrWhiteSpace() && + DbConnectionExtensions.IsConnectionAvailable(ConnectionString, DbProviderFactory); + + /// + public ISqlContext SqlContext + { + get + { + // must be initialized to have a context + EnsureInitialized(); + + return _sqlContext; + } + } + + /// + public IBulkSqlInsertProvider? BulkSqlInsertProvider + { + get + { + // must be initialized to have a bulk insert provider + EnsureInitialized(); + + return _bulkSqlInsertProvider; + } + } + + /// + public void ConfigureForUpgrade() => _upgrading = true; + + /// + public void Configure(ConnectionStrings umbracoConnectionString) + { + if (umbracoConnectionString is null) + { + throw new ArgumentNullException(nameof(umbracoConnectionString)); + } + + lock (_lock) + { + if (Volatile.Read(ref _initialized)) + { + throw new InvalidOperationException("Already initialized."); + } + + _umbracoConnectionString = umbracoConnectionString; + } + + // rest to be lazy-initialized + } + + /// + public IUmbracoDatabase CreateDatabase() + { + // must be initialized to create a database + EnsureInitialized(); + return (IUmbracoDatabase)_npocoDatabaseFactory!.GetDatabase(); + } + + 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 (DbProviderFactory == null) + { + throw new Exception($"Can't find a provider factory for provider name \"{ProviderName}\"."); + } + + _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!); + + _databaseType = _sqlSyntax.GetUpdatedDatabaseType(_databaseType, ConnectionString); + + // 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 MapperCollection(); + + // add all registered mappers for NPoco + _pocoMappers.AddRange(_npocoMappers); + + _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(ProviderName!)); + + var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); + _pocoDataFactory = factory; + var config = new FluentConfig(xmappers => factory); + + // create the database factory + _npocoDatabaseFactory = DatabaseFactory.Config(cfg => + { + cfg.UsingDatabase(CreateDatabaseInstance) // creating UmbracoDatabase instances + .WithFluentConfig(config); // with proper configuration + + foreach (IProviderSpecificInterceptor interceptor in _dbProviderFactoryCreator + .GetProviderSpecificInterceptors(ProviderName!)) + { + cfg.WithInterceptor(interceptor); + } + }); + + if (_npocoDatabaseFactory == null) + { + throw new NullReferenceException( + "The call to UmbracoDatabaseFactory.Config yielded a null UmbracoDatabaseFactory instance."); + } + + _logger.LogDebug("Initialized."); + + return new SqlContext(_sqlSyntax, _databaseType, _pocoDataFactory, _mappers); + } + + // gets initialized poco data builders + private InitializedPocoDataBuilder GetPocoDataFactoryResolver(Type type, IPocoDataFactory factory) + => new UmbracoPocoDataBuilder(type, _pocoMappers, _upgrading).Init(); + + // method used by NPoco's UmbracoDatabaseFactory to actually create the database instance + private UmbracoDatabase? CreateDatabaseInstance() + { + if (ConnectionString is null || SqlContext is null || DbProviderFactory is null) + { + return null; + } + + return new UmbracoDatabase( + ConnectionString, + SqlContext, + DbProviderFactory, + _loggerFactory.CreateLogger(), + _bulkSqlInsertProvider, + _databaseSchemaCreatorFactory, + _pocoMappers); + } + + protected override void DisposeResources() => + + // this is weird, because hybrid accessors store different databases per + // thread, so we don't really know what we are disposing here... + // besides, we don't really want to dispose the factory, which is a singleton... + // TODO: the class does not need be disposable + // var db = _umbracoDatabaseAccessor.UmbracoDatabase; + // _umbracoDatabaseAccessor.UmbracoDatabase = null; + // db?.Dispose(); + Volatile.Write(ref _initialized, false); } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs index 753563faff..7b62c212e3 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs @@ -1,31 +1,33 @@ -using System; using NPoco; -namespace Umbraco.Cms.Infrastructure.Persistence -{ - /// - /// Umbraco's implementation of NPoco . - /// - /// - /// NPoco PocoDataBuilder analyzes DTO classes and returns infos about the tables and - /// their columns. - /// In some very special occasions, a class may expose a column that we do not want to - /// use. This is essentially when adding a column to the User table: if the code wants the - /// column to exist, and it does not exist yet in the database, because a given migration has - /// not run, then the user cannot log into the site, and cannot upgrade = catch 22. - /// So far, this is very manual. We don't try to be clever and figure out whether the - /// columns exist already. We just ignore it. - /// Beware, the application MUST restart when this class behavior changes. - /// You can override the GetColmunnInfo method to control which columns this includes - /// - internal class UmbracoPocoDataBuilder : PocoDataBuilder - { - private readonly bool _upgrading; +namespace Umbraco.Cms.Infrastructure.Persistence; - public UmbracoPocoDataBuilder(Type type, MapperCollection? mapper, bool upgrading) - : base(type, mapper) - { - _upgrading = upgrading; - } - } +/// +/// Umbraco's implementation of NPoco . +/// +/// +/// +/// NPoco PocoDataBuilder analyzes DTO classes and returns infos about the tables and +/// their columns. +/// +/// +/// In some very special occasions, a class may expose a column that we do not want to +/// use. This is essentially when adding a column to the User table: if the code wants the +/// column to exist, and it does not exist yet in the database, because a given migration has +/// not run, then the user cannot log into the site, and cannot upgrade = catch 22. +/// +/// +/// So far, this is very manual. We don't try to be clever and figure out whether the +/// columns exist already. We just ignore it. +/// +/// Beware, the application MUST restart when this class behavior changes. +/// You can override the GetColmunnInfo method to control which columns this includes +/// +internal class UmbracoPocoDataBuilder : PocoDataBuilder +{ + private readonly bool _upgrading; + + public UmbracoPocoDataBuilder(Type type, MapperCollection? mapper, bool upgrading) + : base(type, mapper) => + _upgrading = upgrading; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index 8b6663051e..6b2967c781 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -1,10 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Umbraco.Cms.Core.IO; @@ -16,428 +13,486 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Abstract class for block editor based editors +/// +public abstract class BlockEditorPropertyEditor : DataEditor { - /// - /// Abstract class for block editor based editors - /// - public abstract class BlockEditorPropertyEditor : DataEditor + public const string ContentTypeKeyPropertyKey = "contentTypeKey"; + public const string UdiPropertyKey = "udi"; + + public BlockEditorPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors) + : base(dataValueEditorFactory) + => PropertyEditors = propertyEditors; + + private PropertyEditorCollection PropertyEditors { get; } + + #region Value Editor + + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); + + internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference { - public const string ContentTypeKeyPropertyKey = "contentTypeKey"; - public const string UdiPropertyKey = "udi"; + private readonly BlockEditorValues _blockEditorValues; + private readonly IDataTypeService _dataTypeService; + private readonly ILogger _logger; + private readonly PropertyEditorCollection _propertyEditors; - public BlockEditorPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors) - : base(dataValueEditorFactory) - => PropertyEditors = propertyEditors; - - private PropertyEditorCollection PropertyEditors { get; } - - #region Value Editor - - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - - internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference + public BlockEditorPropertyValueEditor( + DataEditorAttribute attribute, + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + IContentTypeService contentTypeService, + ILocalizedTextService textService, + ILogger logger, + IShortStringHelper shortStringHelper, + IJsonSerializer jsonSerializer, + IIOHelper ioHelper, + IPropertyValidationService propertyValidationService) + : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; - private readonly ILogger _logger; - private readonly BlockEditorValues _blockEditorValues; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _logger = logger; - public BlockEditorPropertyValueEditor( - DataEditorAttribute attribute, - PropertyEditorCollection propertyEditors, - IDataTypeService dataTypeService, - IContentTypeService contentTypeService, - ILocalizedTextService textService, - ILogger logger, - IShortStringHelper shortStringHelper, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - IPropertyValidationService propertyValidationService) - : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) + _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); + Validators.Add(new BlockEditorValidator(propertyValidationService, _blockEditorValues, contentTypeService)); + Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); + } + + public IEnumerable GetReferences(object? value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); + if (blockEditorData == null) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; - _logger = logger; - - _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); - Validators.Add(new BlockEditorValidator(propertyValidationService, _blockEditorValues,contentTypeService)); - Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); + return Enumerable.Empty(); } - public IEnumerable GetReferences(object? value) + // loop through all content and settings data + foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue + .SettingsData)) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData == null) - return Enumerable.Empty(); - - // loop through all content and settings data - foreach (var row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) + foreach (KeyValuePair prop in row.PropertyValues) { - foreach (var prop in row.PropertyValues) + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + + IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) { - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - - var valueEditor = propEditor?.GetValueEditor(); - if (!(valueEditor is IDataValueReference reference)) continue; - - var val = prop.Value.Value?.ToString(); - - var refs = reference.GetReferences(val); - - result.AddRange(refs); + continue; } - } - return result; + var val = prop.Value.Value?.ToString(); + + IEnumerable refs = reference.GetReferences(val); + + result.AddRange(refs); + } } - #region Convert database // editor + return result; + } - // note: there is NO variant support here + #region Convert database // editor - /// - /// Ensure that sub-editor values are translated through their ToEditor methods - /// - /// - /// - /// - /// - /// - public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + // note: there is NO variant support here + + /// + /// Ensure that sub-editor values are translated through their ToEditor methods + /// + /// + /// + /// + /// + /// + public override object ToEditor(IProperty property, string? culture = null, string? segment = null) + { + var val = property.GetValue(culture, segment); + var valEditors = new Dictionary(); + + BlockEditorData? blockEditorData; + try { - var val = property.GetValue(culture, segment); - var valEditors = new Dictionary(); + blockEditorData = _blockEditorValues.DeserializeAndClean(val); + } + catch (JsonSerializationException) + { + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; + } - BlockEditorData? blockEditorData; - try - { - blockEditorData = _blockEditorValues.DeserializeAndClean(val); - } - catch (JsonSerializationException) - { - // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. - return string.Empty; - } + if (blockEditorData == null) + { + return string.Empty; + } - if (blockEditorData == null) - return string.Empty; - - void MapBlockItemData(List items) + void MapBlockItemData(List items) + { + foreach (BlockItemData row in items) { - foreach (var row in items) + foreach (KeyValuePair prop in row.PropertyValues) { - foreach (var prop in row.PropertyValues) + // create a temp property with the value + // - force it to be culture invariant as the block editor can't handle culture variant element properties + prop.Value.PropertyType.Variations = ContentVariation.Nothing; + var tempProp = new Property(prop.Value.PropertyType); + tempProp.SetValue(prop.Value.Value); + + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) - { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); - continue; - } - - var dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); - if (dataType == null) - { - // deal with weird situations by ignoring them (no comment) - row.PropertyValues.Remove(prop.Key); - _logger.LogWarning( - "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", - prop.Key, row.Key, property.PropertyType.Alias); - continue; - } - - if (!valEditors.TryGetValue(dataType.Id, out var valEditor)) - { - var tempConfig = dataType.Configuration; - valEditor = propEditor.GetValueEditor(tempConfig); - - valEditors.Add(dataType.Id, valEditor); - } - - var convValue = valEditor.ToEditor(tempProp); - + // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. + // if the property editor doesn't exist I think everything will break anyways? // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; + row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + continue; } - } - } - MapBlockItemData(blockEditorData.BlockValue.ContentData); - MapBlockItemData(blockEditorData.BlockValue.SettingsData); - - // return json convertable object - return blockEditorData.BlockValue; - } - - /// - /// Ensure that sub-editor values are translated through their FromEditor methods - /// - /// - /// - /// - public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) - { - if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) - return null; - - BlockEditorData? blockEditorData; - try - { - blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value); - } - catch (JsonSerializationException) - { - // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. - return string.Empty; - } - - if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) - return string.Empty; - - void MapBlockItemData(List items) - { - foreach (var row in items) - { - foreach (var prop in row.PropertyValues) + IDataType? dataType = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); + if (dataType == null) { - // Fetch the property types prevalue - var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId)?.Configuration; - - // Lookup the property editor - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) continue; - - // Create a fake content property data object - var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); - - // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); - - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = newValue; + // deal with weird situations by ignoring them (no comment) + row.PropertyValues.Remove(prop.Key); + _logger.LogWarning( + "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", prop.Key, row.Key, property.PropertyType.Alias); + continue; } + + if (!valEditors.TryGetValue(dataType.Id, out IDataValueEditor? valEditor)) + { + var tempConfig = dataType.Configuration; + valEditor = propEditor.GetValueEditor(tempConfig); + + valEditors.Add(dataType.Id, valEditor); + } + + var convValue = valEditor.ToEditor(tempProp); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = convValue; } } - - MapBlockItemData(blockEditorData.BlockValue.ContentData); - MapBlockItemData(blockEditorData.BlockValue.SettingsData); - - // return json - return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } - #endregion + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); + + // return json convertable object + return blockEditorData.BlockValue; } /// - /// Validates the min/max of the block editor + /// Ensure that sub-editor values are translated through their FromEditor methods /// - private class MinMaxValidator : IValueValidator + /// + /// + /// + public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) { - private readonly BlockEditorValues _blockEditorValues; - private readonly ILocalizedTextService _textService; - - public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService) + if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) { - _blockEditorValues = blockEditorValues; - _textService = textService; + return null; } - public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + BlockEditorData? blockEditorData; + try { - var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; - if (blockConfig == null) yield break; - - var validationLimit = blockConfig.ValidationLimit; - if (validationLimit == null) yield break; - - var blockEditorData = _blockEditorValues.DeserializeAndClean(value); - - if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0) - || (blockEditorData != null && validationLimit.Min.HasValue && blockEditorData.Layout?.Count() < validationLimit.Min)) - { - yield return new ValidationResult( - _textService.Localize("validation", "entriesShort", new[] - { - validationLimit.Min.ToString(), - (validationLimit.Min - (blockEditorData?.Layout?.Count() ?? 0)).ToString() - }), - new[] { "minCount" }); - } - - if (blockEditorData != null && validationLimit.Max.HasValue && blockEditorData.Layout?.Count() > validationLimit.Max) - { - yield return new ValidationResult( - _textService.Localize("validation", "entriesExceed", new[] - { - validationLimit.Max.ToString(), - (blockEditorData.Layout.Count() - validationLimit.Max).ToString() - }), - new[] { "maxCount" }); - } + blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value); } - } - - internal class BlockEditorValidator : ComplexEditorValidator - { - private readonly BlockEditorValues _blockEditorValues; - private readonly IContentTypeService _contentTypeService; - - public BlockEditorValidator(IPropertyValidationService propertyValidationService, BlockEditorValues blockEditorValues, IContentTypeService contentTypeService) - : base(propertyValidationService) + catch (JsonSerializationException) { - _blockEditorValues = blockEditorValues; - _contentTypeService = contentTypeService; + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; } - protected override IEnumerable GetElementTypeValidation(object? value) + if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) { - var blockEditorData = _blockEditorValues.DeserializeAndClean(value); - if (blockEditorData != null) - { - // There is no guarantee that the client will post data for every property defined in the Element Type but we still - // need to validate that data for each property especially for things like 'required' data to work. - // Lookup all element types for all content/settings and then we can populate any empty properties. - var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData).ToList(); - var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); + return string.Empty; + } - foreach (var row in allElements) + void MapBlockItemData(List items) + { + foreach (BlockItemData row in items) + { + foreach (KeyValuePair prop in row.PropertyValues) { - if (!allElementTypes.TryGetValue(row.ContentTypeKey, out var elementType)) - throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId) + ?.Configuration; - // now ensure missing properties - foreach (var elementTypeProp in elementType.CompositionPropertyTypes) + // Lookup the property editor + IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) { - if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) - { - // set values to null - row.PropertyValues[elementTypeProp.Alias] = new BlockItemData.BlockPropertyValue(null, elementTypeProp); - row.RawPropertyValues[elementTypeProp.Alias] = null; - } + continue; } - var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); - foreach (var prop in row.PropertyValues) - { - elementValidation.AddPropertyTypeValidation( - new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); - } - yield return elementValidation; + // Create a fake content property data object + var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); + + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = newValue; } } } - } - /// - /// Used to deserialize json values and clean up any values based on the existence of element types and layout structure - /// - internal class BlockEditorValues - { - private readonly Lazy> _contentTypes; - private readonly BlockEditorDataConverter _dataConverter; - private readonly ILogger _logger; + MapBlockItemData(blockEditorData.BlockValue.ContentData); + MapBlockItemData(blockEditorData.BlockValue.SettingsData); - public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) - { - _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); - _dataConverter = dataConverter; - _logger = logger; - } - - private IContentType? GetElementType(BlockItemData item) - { - _contentTypes.Value.TryGetValue(item.ContentTypeKey, out var contentType); - return contentType; - } - - public BlockEditorData? DeserializeAndClean(object? propertyValue) - { - if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - return null; - - var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()!); - - if (blockEditorData.BlockValue.ContentData.Count == 0) - { - // if there's no content ensure there's no settings too - blockEditorData.BlockValue.SettingsData.Clear(); - return null; - } - - var contentTypePropertyTypes = new Dictionary>(); - - // filter out any content that isn't referenced in the layout references - foreach (var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) - { - ResolveBlockItemData(block, contentTypePropertyTypes); - } - // filter out any settings that isn't referenced in the layout references - foreach (var block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) - { - ResolveBlockItemData(block, contentTypePropertyTypes); - } - - // remove blocks that couldn't be resolved - blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); - blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); - - return blockEditorData; - } - - private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes) - { - var contentType = GetElementType(block); - if (contentType == null) - return false; - - // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating - // objects on each iteration. - if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes)) - propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); - - var propValues = new Dictionary(); - - // find any keys that are not real property types and remove them - foreach (var prop in block.RawPropertyValues.ToList()) - { - // doesn't exist so remove it - if (!propertyTypes.TryGetValue(prop.Key, out var propType)) - { - block.RawPropertyValues.Remove(prop.Key); - _logger.LogWarning("The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", - prop.Key, block.Key, prop.Key, contentType.Alias); - } - else - { - // set the value to include the resolved property type - propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); - } - } - - block.ContentTypeAlias = contentType.Alias; - block.PropertyValues = propValues; - - return true; - } + // return json + return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } #endregion - } + + internal class BlockEditorValidator : ComplexEditorValidator + { + private readonly BlockEditorValues _blockEditorValues; + private readonly IContentTypeService _contentTypeService; + + public BlockEditorValidator( + IPropertyValidationService propertyValidationService, + BlockEditorValues blockEditorValues, + IContentTypeService contentTypeService) + : base(propertyValidationService) + { + _blockEditorValues = blockEditorValues; + _contentTypeService = contentTypeService; + } + + protected override IEnumerable GetElementTypeValidation(object? value) + { + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); + if (blockEditorData != null) + { + // There is no guarantee that the client will post data for every property defined in the Element Type but we still + // need to validate that data for each property especially for things like 'required' data to work. + // Lookup all element types for all content/settings and then we can populate any empty properties. + var allElements = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData) + .ToList(); + var allElementTypes = _contentTypeService.GetAll(allElements.Select(x => x.ContentTypeKey).ToArray()) + .ToDictionary(x => x.Key); + + foreach (BlockItemData row in allElements) + { + if (!allElementTypes.TryGetValue(row.ContentTypeKey, out IContentType? elementType)) + { + throw new InvalidOperationException($"No element type found with key {row.ContentTypeKey}"); + } + + // now ensure missing properties + foreach (IPropertyType elementTypeProp in elementType.CompositionPropertyTypes) + { + if (!row.PropertyValues.ContainsKey(elementTypeProp.Alias)) + { + // set values to null + row.PropertyValues[elementTypeProp.Alias] = + new BlockItemData.BlockPropertyValue(null, elementTypeProp); + row.RawPropertyValues[elementTypeProp.Alias] = null; + } + } + + var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); + foreach (KeyValuePair prop in row.PropertyValues) + { + elementValidation.AddPropertyTypeValidation( + new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); + } + + yield return elementValidation; + } + } + } + } + + /// + /// Validates the min/max of the block editor + /// + private class MinMaxValidator : IValueValidator + { + private readonly BlockEditorValues _blockEditorValues; + private readonly ILocalizedTextService _textService; + + public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService) + { + _blockEditorValues = blockEditorValues; + _textService = textService; + } + + public IEnumerable Validate(object? value, string? valueType, object? dataTypeConfiguration) + { + var blockConfig = (BlockListConfiguration?)dataTypeConfiguration; + if (blockConfig == null) + { + yield break; + } + + BlockListConfiguration.NumberRange? validationLimit = blockConfig.ValidationLimit; + if (validationLimit == null) + { + yield break; + } + + BlockEditorData? blockEditorData = _blockEditorValues.DeserializeAndClean(value); + + if ((blockEditorData == null && validationLimit.Min.HasValue && validationLimit.Min > 0) + || (blockEditorData != null && validationLimit.Min.HasValue && + blockEditorData.Layout?.Count() < validationLimit.Min)) + { + yield return new ValidationResult( + _textService.Localize( + "validation", + "entriesShort", + new[] + { + validationLimit.Min.ToString(), + (validationLimit.Min - (blockEditorData?.Layout?.Count() ?? 0)).ToString(), + }), + new[] { "minCount" }); + } + + if (blockEditorData != null && validationLimit.Max.HasValue && + blockEditorData.Layout?.Count() > validationLimit.Max) + { + yield return new ValidationResult( + _textService.Localize( + "validation", + "entriesExceed", + new[] + { + validationLimit.Max.ToString(), + (blockEditorData.Layout.Count() - validationLimit.Max).ToString(), + }), + new[] { "maxCount" }); + } + } + } + + /// + /// Used to deserialize json values and clean up any values based on the existence of element types and layout + /// structure + /// + internal class BlockEditorValues + { + private readonly Lazy> _contentTypes; + private readonly BlockEditorDataConverter _dataConverter; + private readonly ILogger _logger; + + public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) + { + _contentTypes = + new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); + _dataConverter = dataConverter; + _logger = logger; + } + + public BlockEditorData? DeserializeAndClean(object? propertyValue) + { + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + { + return null; + } + + BlockEditorData blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()!); + + if (blockEditorData.BlockValue.ContentData.Count == 0) + { + // if there's no content ensure there's no settings too + blockEditorData.BlockValue.SettingsData.Clear(); + return null; + } + + var contentTypePropertyTypes = new Dictionary>(); + + // filter out any content that isn't referenced in the layout references + foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x => + blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } + + // filter out any settings that isn't referenced in the layout references + foreach (BlockItemData block in blockEditorData.BlockValue.SettingsData.Where(x => + blockEditorData.References.Any(r => + r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } + + // remove blocks that couldn't be resolved + blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + + return blockEditorData; + } + + private IContentType? GetElementType(BlockItemData item) + { + _contentTypes.Value.TryGetValue(item.ContentTypeKey, out IContentType? contentType); + return contentType; + } + + private bool ResolveBlockItemData( + BlockItemData block, + Dictionary> contentTypePropertyTypes) + { + IContentType? contentType = GetElementType(block); + if (contentType == null) + { + return false; + } + + // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating + // objects on each iteration. + if (!contentTypePropertyTypes.TryGetValue( + contentType.Alias, + out Dictionary? propertyTypes)) + { + propertyTypes = contentTypePropertyTypes[contentType.Alias] = + contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + } + + var propValues = new Dictionary(); + + // find any keys that are not real property types and remove them + foreach (KeyValuePair prop in block.RawPropertyValues.ToList()) + { + // doesn't exist so remove it + if (!propertyTypes.TryGetValue(prop.Key, out IPropertyType? propType)) + { + block.RawPropertyValues.Remove(prop.Key); + _logger.LogWarning( + "The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", + prop.Key, + block.Key, + prop.Key, + contentType.Alias); + } + else + { + // set the value to include the resolved property type + propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); + } + } + + block.ContentTypeAlias = contentType.Alias; + block.PropertyValues = propValues; + + return true; + } + } + + #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs index 28691af7ba..f8e9053722 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs @@ -1,217 +1,226 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A handler for Block editors used to bind to notifications +/// +public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler { - /// - /// A handler for Block editors used to bind to notifications - /// - public class BlockEditorPropertyHandler : ComplexPropertyEditorContentNotificationHandler + private readonly BlockListEditorDataConverter _converter = new(); + private readonly ILogger _logger; + + public BlockEditorPropertyHandler(ILogger logger) => _logger = logger; + + protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList; + + // internal for tests + internal string ReplaceBlockListUdis(string rawJson, Func? createGuid = null) { - private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter(); - private readonly ILogger _logger; - - public BlockEditorPropertyHandler(ILogger logger) + // used so we can test nicely + if (createGuid == null) { - _logger = logger; + createGuid = () => Guid.NewGuid(); } - protected override string EditorAlias => Constants.PropertyEditors.Aliases.BlockList; - - protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) { - // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process - if (onlyMissingKeys) - return rawJson; - - return ReplaceBlockListUdis(rawJson, null); + return rawJson; } - // internal for tests - internal string ReplaceBlockListUdis(string rawJson, Func? createGuid = null) + // Parse JSON + // This will throw a FormatException if there are null UDIs (expected) + BlockEditorData blockListValue = _converter.Deserialize(rawJson); + + UpdateBlockListRecursively(blockListValue, createGuid); + + return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); + } + + protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) + { + // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process + if (onlyMissingKeys) { - // used so we can test nicely - if (createGuid == null) - createGuid = () => Guid.NewGuid(); - - if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) - return rawJson; - - // Parse JSON - // This will throw a FormatException if there are null UDIs (expected) - var blockListValue = _converter.Deserialize(rawJson); - - UpdateBlockListRecursively(blockListValue, createGuid); - - return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); + return rawJson; } - private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) - { - var oldToNew = new Dictionary(); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); + return ReplaceBlockListUdis(rawJson); + } - for (var i = 0; i < blockListData.References.Count; i++) + private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) + { + var oldToNew = new Dictionary(); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); + + for (var i = 0; i < blockListData.References.Count; i++) + { + ContentAndSettingsReference reference = blockListData.References[i]; + var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out Udi? contentMap); + Udi? settingsMap = null; + var hasSettingsMap = reference.SettingsUdi is not null && + oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); + + if (hasContentMap) { - var reference = blockListData.References[i]; - var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out var contentMap); - Udi? settingsMap = null; - var hasSettingsMap = reference.SettingsUdi is not null && oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); - - if (hasContentMap) - { - // replace the reference - blockListData.References.RemoveAt(i); - blockListData.References.Insert(i, new ContentAndSettingsReference(contentMap!, hasSettingsMap ? settingsMap : null)); - } + // replace the reference + blockListData.References.RemoveAt(i); + blockListData.References.Insert( + i, + new ContentAndSettingsReference(contentMap!, hasSettingsMap ? settingsMap : null)); } - - // build the layout with the new UDIs - var layout = (JArray?)blockListData.Layout; - layout?.Clear(); - foreach (var reference in blockListData.References) - { - layout?.Add(JObject.FromObject(new BlockListLayoutItem - { - ContentUdi = reference.ContentUdi, - SettingsUdi = reference.SettingsUdi - })); - } - - - RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); - RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); } - private void RecursePropertyValues(IEnumerable blockData, Func createGuid) + // build the layout with the new UDIs + var layout = (JArray?)blockListData.Layout; + layout?.Clear(); + foreach (ContentAndSettingsReference reference in blockListData.References) { - foreach (var data in blockData) + layout?.Add(JObject.FromObject(new BlockListLayoutItem { - // check if we need to recurse (make a copy of the dictionary since it will be modified) - foreach (var propertyAliasToBlockItemData in new Dictionary(data.RawPropertyValues)) + ContentUdi = reference.ContentUdi, + SettingsUdi = reference.SettingsUdi, + })); + } + + RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); + RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); + } + + private void RecursePropertyValues(IEnumerable blockData, Func createGuid) + { + foreach (BlockItemData data in blockData) + { + // check if we need to recurse (make a copy of the dictionary since it will be modified) + foreach (KeyValuePair propertyAliasToBlockItemData in new Dictionary( + data.RawPropertyValues)) + { + if (propertyAliasToBlockItemData.Value is JToken jtoken) { - if (propertyAliasToBlockItemData.Value is JToken jtoken) + if (ProcessJToken(jtoken, createGuid, out JToken result)) { - if (ProcessJToken(jtoken, createGuid, out var result)) + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; + } + } + else + { + var asString = propertyAliasToBlockItemData.Value?.ToString(); + + if (asString != null && asString.DetectIsJson()) + { + // this gets a little ugly because there could be some other complex editor that contains another block editor + // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor + // of our type + JToken? json = null; + try + { + json = JToken.Parse(asString); + } + catch (Exception) + { + // See issue https://github.com/umbraco/Umbraco-CMS/issues/10879 + // We are detecting JSON data by seeing if a string is surrounded by [] or {} + // If people enter text like [PLACEHOLDER] JToken parsing fails, it's safe to ignore though + // Logging this just in case in the future we find values that are not safe to ignore + _logger.LogWarning( + "The property {PropertyAlias} on content type {ContentTypeKey} has a value of: {BlockItemValue} - this was recognized as JSON but could not be parsed", + data.Key, propertyAliasToBlockItemData.Key, asString); + } + + if (json != null && ProcessJToken(json, createGuid, out JToken result)) { // need to re-save this back to the RawPropertyValues data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; } } - else - { - var asString = propertyAliasToBlockItemData.Value?.ToString(); - - if (asString != null && asString.DetectIsJson()) - { - // this gets a little ugly because there could be some other complex editor that contains another block editor - // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor - // of our type - JToken? json = null; - try - { - json = JToken.Parse(asString); - } - catch (Exception) - { - // See issue https://github.com/umbraco/Umbraco-CMS/issues/10879 - // We are detecting JSON data by seeing if a string is surrounded by [] or {} - // If people enter text like [PLACEHOLDER] JToken parsing fails, it's safe to ignore though - // Logging this just in case in the future we find values that are not safe to ignore - _logger.LogWarning( "The property {PropertyAlias} on content type {ContentTypeKey} has a value of: {BlockItemValue} - this was recognized as JSON but could not be parsed", - data.Key, propertyAliasToBlockItemData.Key, asString); - } - - if (json != null && ProcessJToken(json, createGuid, out var result)) - { - // need to re-save this back to the RawPropertyValues - data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; - } - } - } } } } - - private bool ProcessJToken(JToken json, Func createGuid, out JToken result) - { - var updated = false; - result = json; - - // select all tokens (flatten) - var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); - foreach (var prop in allProperties) - { - if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) - { - // get it's parent 'layout' and it's parent's container - var layout = prop.Parent?.Parent as JProperty; - if (layout != null && layout.Parent is JObject layoutJson) - { - // recurse - var blockListValue = _converter.ConvertFrom(layoutJson); - UpdateBlockListRecursively(blockListValue, createGuid); - - // set new value - if (layoutJson.Parent != null) - { - // we can replace the object - layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); - updated = true; - } - else - { - // if there is no parent it means that this json property was the root, in which case we just return - result = JObject.FromObject(blockListValue.BlockValue); - return true; - } - } - } - else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey") - { - // this is an arbitrary property that could contain a nested complex editor - var propVal = prop.Value?.ToString(); - // check if this might contain a nested Block Editor - if (!propVal.IsNullOrWhiteSpace() && (propVal?.DetectIsJson() ?? false) && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) - { - if (_converter.TryDeserialize(propVal, out var nestedBlockData)) - { - // recurse - UpdateBlockListRecursively(nestedBlockData, createGuid); - // set the value to the updated one - prop.Value = JObject.FromObject(nestedBlockData.BlockValue); - updated = true; - } - } - } - } - - return updated; - } - - private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, Func createGuid) - { - foreach (var data in blockData) - { - // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here - if (data.Udi is null) - throw new InvalidOperationException("Block data cannot contain a null UDI"); - - // replace the UDIs - var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid()); - oldToNew[data.Udi] = newUdi; - data.Udi = newUdi; - } - } + } + + private bool ProcessJToken(JToken json, Func createGuid, out JToken result) + { + var updated = false; + result = json; + + // select all tokens (flatten) + var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (JProperty prop in allProperties) + { + if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) + { + // get it's parent 'layout' and it's parent's container + if (prop.Parent?.Parent is JProperty layout && layout.Parent is JObject layoutJson) + { + // recurse + BlockEditorData blockListValue = _converter.ConvertFrom(layoutJson); + UpdateBlockListRecursively(blockListValue, createGuid); + + // set new value + if (layoutJson.Parent != null) + { + // we can replace the object + layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); + updated = true; + } + else + { + // if there is no parent it means that this json property was the root, in which case we just return + result = JObject.FromObject(blockListValue.BlockValue); + return true; + } + } + } + else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && + prop.Name != "contentTypeKey") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value.ToString(); + + // check if this might contain a nested Block Editor + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && + propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) + { + if (_converter.TryDeserialize(propVal, out BlockEditorData? nestedBlockData)) + { + // recurse + UpdateBlockListRecursively(nestedBlockData, createGuid); + + // set the value to the updated one + prop.Value = JObject.FromObject(nestedBlockData.BlockValue); + updated = true; + } + } + } + } + + return updated; + } + + private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, + Func createGuid) + { + foreach (BlockItemData data in blockData) + { + // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here + if (data.Udi is null) + { + throw new InvalidOperationException("Block data cannot contain a null UDI"); + } + + // replace the UDIs + var newUdi = Udi.Create(Constants.UdiEntityType.Element, createGuid()); + oldToNew[data.Udi] = newUdi; + data.Udi = newUdi; + } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs index a3b3d62338..431b006b1e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListConfigurationEditor.cs @@ -1,19 +1,15 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class BlockListConfigurationEditor : ConfigurationEditor { - internal class BlockListConfigurationEditor : ConfigurationEditor + public BlockListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - public BlockListConfigurationEditor(IIOHelper ioHelper, IEditorConfigurationParser editorConfigurationParser) : base(ioHelper, editorConfigurationParser) - { - } - } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index c8be6adf40..70a0aa35dc 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -1,58 +1,53 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// Represents a block list property editor. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.BlockList, + "Block List", + "blocklist", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-thumbnail-list")] +public class BlockListPropertyEditor : BlockEditorPropertyEditor { - /// - /// Represents a block list property editor. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.BlockList, - "Block List", - "blocklist", - ValueType = ValueTypes.Json, - Group = Constants.PropertyEditors.Groups.Lists, - Icon = "icon-thumbnail-list")] - public class BlockListPropertyEditor : BlockEditorPropertyEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public BlockListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IIOHelper ioHelper) + : this(dataValueEditorFactory, propertyEditors, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public BlockListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors, - IIOHelper ioHelper) - : this(dataValueEditorFactory, propertyEditors, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - public BlockListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - PropertyEditorCollection propertyEditors, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory, propertyEditors) - { - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - #region Pre Value Editor - - protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(_ioHelper, _editorConfigurationParser); - - #endregion } + + public BlockListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + PropertyEditorCollection propertyEditors, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory, propertyEditors) + { + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } + + #region Pre Value Editor + + protected override IConfigurationEditor CreateConfigurationEditor() => + new BlockListConfigurationEditor(_ioHelper, _editorConfigurationParser); + + #endregion } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs index 226024f8b9..baf620e859 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -1,59 +1,59 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +/// +/// A property editor to allow multiple checkbox selection of pre-defined items. +/// +[DataEditor( + Constants.PropertyEditors.Aliases.CheckBoxList, + "Checkbox list", + "checkboxlist", + Icon = "icon-bulleted-list", + Group = Constants.PropertyEditors.Groups.Lists)] +public class CheckBoxListPropertyEditor : DataEditor { - /// - /// A property editor to allow multiple checkbox selection of pre-defined items. - /// - [DataEditor( - Constants.PropertyEditors.Aliases.CheckBoxList, - "Checkbox list", - "checkboxlist", - Icon = "icon-bulleted-list", - Group = Constants.PropertyEditors.Groups.Lists)] - public class CheckBoxListPropertyEditor : DataEditor + private readonly IEditorConfigurationParser _editorConfigurationParser; + private readonly IIOHelper _ioHelper; + private readonly ILocalizedTextService _textService; + + // Scheduled for removal in v12 + [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] + public CheckBoxListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + ILocalizedTextService textService, + IIOHelper ioHelper) + : this(dataValueEditorFactory, textService, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly ILocalizedTextService _textService; - private readonly IIOHelper _ioHelper; - private readonly IEditorConfigurationParser _editorConfigurationParser; - - // Scheduled for removal in v12 - [Obsolete("Please use constructor that takes an IEditorConfigurationParser instead")] - public CheckBoxListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - ILocalizedTextService textService, - IIOHelper ioHelper) - : this(dataValueEditorFactory, textService, ioHelper, StaticServiceProvider.Instance.GetRequiredService()) - { - } - - /// - /// The constructor will setup the property editor based on the attribute if one is found - /// - public CheckBoxListPropertyEditor( - IDataValueEditorFactory dataValueEditorFactory, - ILocalizedTextService textService, - IIOHelper ioHelper, - IEditorConfigurationParser editorConfigurationParser) - : base(dataValueEditorFactory) - { - _textService = textService; - _ioHelper = ioHelper; - _editorConfigurationParser = editorConfigurationParser; - } - - /// - protected override IConfigurationEditor CreateConfigurationEditor() => new ValueListConfigurationEditor(_textService, _ioHelper, _editorConfigurationParser); - - /// - protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); } + + /// + /// The constructor will setup the property editor based on the attribute if one is found + /// + public CheckBoxListPropertyEditor( + IDataValueEditorFactory dataValueEditorFactory, + ILocalizedTextService textService, + IIOHelper ioHelper, + IEditorConfigurationParser editorConfigurationParser) + : base(dataValueEditorFactory) + { + _textService = textService; + _ioHelper = ioHelper; + _editorConfigurationParser = editorConfigurationParser; + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => + new ValueListConfigurationEditor(_textService, _ioHelper, _editorConfigurationParser); + + /// + protected override IDataValueEditor CreateValueEditor() => + DataValueEditorFactory.Create(Attribute!); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index ff72a77788..8c8455ce86 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -1,10 +1,7 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Runtime.Serialization; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; @@ -13,172 +10,190 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; -namespace Umbraco.Cms.Core.PropertyEditors +namespace Umbraco.Cms.Core.PropertyEditors; + +internal class ColorPickerConfigurationEditor : ConfigurationEditor { - internal class ColorPickerConfigurationEditor : ConfigurationEditor + private readonly IJsonSerializer _jsonSerializer; + + public ColorPickerConfigurationEditor(IIOHelper ioHelper, IJsonSerializer jsonSerializer, + IEditorConfigurationParser editorConfigurationParser) + : base(ioHelper, editorConfigurationParser) { - private readonly IJsonSerializer _jsonSerializer; - public ColorPickerConfigurationEditor(IIOHelper ioHelper, IJsonSerializer jsonSerializer, IEditorConfigurationParser editorConfigurationParser) - : base(ioHelper, editorConfigurationParser) - { - _jsonSerializer = jsonSerializer; - var items = Fields.First(x => x.Key == "items"); + _jsonSerializer = jsonSerializer; + ConfigurationField items = Fields.First(x => x.Key == "items"); - // customize the items field - items.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; - items.Description = "Add, remove or sort colors"; - items.Name = "Colors"; - items.Validators.Add(new ColorListValidator()); + // customize the items field + items.View = "views/propertyeditors/colorpicker/colorpicker.prevalues.html"; + items.Description = "Add, remove or sort colors"; + items.Name = "Colors"; + items.Validators.Add(new ColorListValidator()); + } + + public override Dictionary ToConfigurationEditor(ColorPickerConfiguration? configuration) + { + List? configuredItems = configuration?.Items; // ordered + object editorItems; + + if (configuredItems == null) + { + editorItems = new object(); + } + else + { + var d = new Dictionary(); + editorItems = d; + var sortOrder = 0; + foreach (ValueListConfiguration.ValueListItem item in configuredItems) + { + d[item.Id.ToString()] = GetItemValue(item, configuration!.UseLabel, sortOrder++); + } } - public override Dictionary ToConfigurationEditor(ColorPickerConfiguration? configuration) + var useLabel = configuration?.UseLabel ?? false; + + return new Dictionary { { "items", editorItems }, { "useLabel", useLabel } }; + } + + // send: { "items": { "": { "value": "", "label": "